列表,数组,终端输入
集合
集合 是一种可以存储多个值的数据类型。
有时也把它叫做容器类型。就像一个容器,可以装很多东西。
这在编程开发中非常常见。
Java 中,集合类型有很多种,常用的有:列表(List),数组(Array),映射(Map) 等。
这一章,我们先介绍 列表 和 数组 这两种。
列表 (List)
列表定义
前面我们介绍过字符串,它是存储 字符 组成的序列,是一种 引用类型。
在 Java 中,还有 其它类型数据 的 序列,其中最常用就是 列表。
列表 是一个可以动态调整大小的容器,经常被用来存储一组对象。
你可以把它想象成一连串的储物格,每个格子都可以存放一个指定类型的对象。
在 Java 中,最常用的列表类型是 ArrayList<E> 。
ArrayList<E>是一个泛型 (Generic) 类型。关于 泛型 ,我们会在后面章节中详细学习。
这里我们只需要知道,ArrayList<E> 中的 E 是一个类型参数 (Type Parameter),用于指定列表中要存放的对象类型。
例如: ArrayList<Integer> 表示一个存放整数的列表,ArrayList<String> 表示一个存放字符串的列表。
Java 的泛型集合只能存放对象,不能存放基本数据类型(如 int, double)。如果要存放整数,必须使用其包装类 Integer;存放小数用 Double。
ArrayList初始化时,如果里面有值,通常需要用到 List 接口。 关于接口,也在后面详细学习,这里我们把它看作一种特殊的 “类” 。
因为 ArrayList 和 List 都定义在 java.util 包里,所以,要使用 List 和 ArrayList,所以需要引入 java.util 包。
import java.util.ArrayList; // 引入 ArrayList 类
import java.util.List; // 引入 List 接口
public class Main {
public static void main(String[] args) {
// 创建一个用于存放字符串的空列表
// 推荐使用 List 接口作为变量类型
List<String> nameList1 = new ArrayList<>();
// 使用 var 关键字 (Java 10+)
var nameList2 = new ArrayList<String>();
// 创建一个初始化好字符串的列表
// List.of 是 Java 9 引入的,生成的是不可变列表
// 为了让它可变(能添加删除),我们需要把它传给 ArrayList 的构造函数
List<String> nameList3 = new ArrayList<>(List.of("张三", "李四", "王五"));
System.out.println(nameList3);
// 创建一个初始化好整数的列表
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4));
System.out.println(numbers);
}
}
也可以先定义变量,再赋值:
List<String> nameList4;
// 其它代码
nameList4 = new ArrayList<>();
nameList4.add("张三");
这种主要适用于: 开始不知道初始值,后面根据条件再赋值的场景。
如果希望像 Python 的列表那样 存放多种不同类型的对象,可以指定类型为 Object,因为 Object 是所有 Java 类型的基类(基类的概念后面会学到)。
// Object 类型的列表可以存放任何类型的数据
List<Object> mixedList = new ArrayList<>();
mixedList.add(1);
mixedList.add(3.14);
mixedList.add("hello");
虽然 List<Object> 很灵活,但在常规 Java 编程中,我们更推荐使用强类型列表(如 List<Integer>),因为这样更安全、性能更好。
✳️ 列表是可变的
ArrayList 的内容是可以改变的, 可以添加,删除,修改,清空等操作。
添加元素:add
add(element):在列表的末尾添加一个元素。add(index, element):在指定索引位置插入一个元素。
List<Object> a = new ArrayList<>(List.of(1, 2, 3.14));
a.add("hello"); // a 变为 [1, 2, 3.14, "hello"]
a.add(0, "你好"); // a 变为 ["你好", 1, 2, 3.14, "hello"]
删除元素:remove,clear
remove(int index):移除指定索引处的元素,并返回被移除的元素。remove(Object o):移除第一个与指定值相等的元素,返回布尔值表示是否成功。clear():清空列表。
List<String> a = new ArrayList<>(List.of("a", "b", "c", "a"));
// 按索引移除
a.remove(1); // 移除 "b",a 变为 ["a", "c", "a"]
// 按值移除
a.remove("a"); // 移除第一个 "a",a 变为 ["c", "a"]
// 清空列表
a.clear(); // a 变为一个空列表 []
索引/查找:get,indexOf
和 Python 一样,List 也是通过从 0 开始的索引来访问元素的。
注意:Java 的列表不能像数组那样使用 [] 语法访问,必须使用 get() 方法。
List<Object> a = new ArrayList<>();
a.add(1);
a.add(2);
a.add(3.14);
a.add("hello");
System.out.println(a.get(0)); // 输出: 1
// 获取最后一个元素
System.out.println(a.get(a.size() - 1));
// Java 21+ 可以使用 getLast()
// System.out.println(a.getLast());
可以通过 :
-
indexOf(item)方法 查找元素的索引,如果不存在则返回 -1。如果有多个相同元素,返回第一个匹配项的索引。
-
lastIndexOf(item)方法 查找元素最后一次出现的索引,如果不存在则返回 -1。
List<Integer> numbers = new ArrayList<>(List.of(2, 3, 7, 8, 9, 7));
System.out.println(numbers.indexOf(8)); // 输出: 3
System.out.println(numbers.lastIndexOf(7)); // 输出: 5
修改指定元素:set
Java 列表修改元素必须使用 set(index, element) 方法。
List<Object> a = new ArrayList<>(List.of(1, 2, 3.14, "hello"));
System.out.println(a); // 输出: [1, 2, 3.14, hello]
// 通过索引直接修改元素
a.set(0, "你好");
System.out.println(a); // 输出: [你好, 2, 3.14, hello]
获取列表长度:size()
可以通过 size() 方法获取列表中元素的数量。
List<Object> a = new ArrayList<>(List.of(1, 2, 3.14, "hello"));
System.out.println(a.size()); // 输出: 4
注意:size 是一个方法,所以需要加括号 ()。这与数组的 length 属性不同。
判断是否存在某元素 contains
判断列表中是否存在某个元素,可以使用 contains() 方法。
List<String> list1 = new ArrayList<>(List.of("apple", "banana", "orange"));
if (list1.contains("banana")) {
System.out.println("列表中存在 banana");
}
if (!list1.contains("grape")) {
System.out.println("列表中不存在 grape");
}
排序与反转 : Collections.sort, Collections.reverse
Java 的 List 接口本身没有 sort 或 reverse 方法(List.sort 需要传入比较器),通常我们使用 Collections 工具类。
import java.util.Collections;
List<Integer> numbers = new ArrayList<>(List.of(7, 3, 8, 2, 9));
// 从小到大排序
Collections.sort(numbers); // numbers 变为 [2, 3, 7, 8, 9]
// 反转列表顺序
Collections.reverse(numbers); // numbers 变为 [9, 8, 7, 3, 2]
// 获取列表中元素的数量
int count = numbers.size(); // count 的值是 5
注意:Collections.reverse() 和 Collections.sort() 都是“就地”操作,它们会直接修改原始列表。
列表的切片:subList
通过 subList(int fromIndex, int toIndex) 方法获取列表的一个视图。
注意:toIndex 是不包含的(左闭右开区间)。
List<Object> a = new ArrayList<>(List.of(1, 2, 3.14, "hello", 7, 8, 9));
// 从索引0开始,获取到索引3(不含3),即 0, 1, 2
List<Object> sliceView = a.subList(0, 3); // 结果是 [1, 2, 3.14]
// 注意:subList 返回的是原列表的一个“视图”。
// 如果修改 sliceView,原列表 a 也会被修改!
sliceView.set(0, "World");
System.out.println(a.get(0)); // 输出: World
// 如果想要一个独立的列表,需要重新构造
List<Object> sliceCopy = new ArrayList<>(a.subList(0, 3));
切片赋值
所谓切片赋值,指的是将列表中某个范围的元素替换为新的元素。
Java 可以通过 subList 配合 clear 和 addAll 实现。
List<Integer> list1 = new ArrayList<>(List.of(0, 1, 2, 3, 4, 5));
// 替换索引 1, 2, 3 处的元素 (即索引 1 到 4)
List<Integer> sub = list1.subList(1, 4);
sub.clear(); // 移除原列表对应范围的元素
sub.addAll(List.of(8, 9, 10)); // 在该位置插入新元素
// list1 现在是 [0, 8, 9, 10, 4, 5]
System.out.println(list1);
合并两个列表:addAll
使用 addAll 方法可以将一个列表的所有元素添加到另一个列表的末尾。
List<Integer> a = new ArrayList<>(List.of(1, 2, 3));
List<Integer> b = List.of(4, 5, 6);
a.addAll(b);
// a 现在是 [1, 2, 3, 4, 5, 6]
越界错误
如果访问或修改列表中不存在的索引位置,会抛出 IndexOutOfBoundsException 异常。
如果没有特别捕获处理,程序会终止并显示错误信息。
List<Integer> list1 = new ArrayList<>(List.of(1, 2, 3));
// list1.get(5); // 抛出异常:java.lang.IndexOutOfBoundsException
数组 (Array)
数组定义
和列表类似,数组也是“一排连续的格子”,每个格子都能装同一个类型的东西。
数组也是一种 引用类型
索引: 0 1 2 3 4
┌──────┬──────┬──────┬──────┬──────┐
数组 → │ 10 │ 20 │ 30 │ 40 │ 50 │
└──────┴──────┴──────┴──────┴──────┘
但是,数组的长度是固定的,创建后就不能再增加或减少元素了。
✳️ 没有初始值 的定义 :
写法①:先声明,再 new
int[] scores; // 只是声明了一个数组变量,不分配内存
scores = new int[5]; // 真正开辟 5 个格子(默认都是 0)
写法②:一步到位
int[] scores = new int[5]; // 5 个格子,全是 0
✳️ 有初始值 的定义 :
// 传统写法
int[] scores = new int[] { 98, 76, 85, 99, 88 }; // 自动就是 5 个长度,用new
// 简写(最常用)
int[] scores2 = { 98, 76, 85, 99, 88 }; // 省略new
String[] names = { "小明", "小红", "小刚", "小美" }; // 字符串数组
char[] chars = { '白', '月', '黑', '羽' }; // 字符数组
char[] arr = "白月黑羽".toCharArray(); // 通过把字符串转换成字符数组 设置初始值
前面说过:数组 和 字符串 是 Java 里2种特殊的 “引用类型”,却能省略 new 的特例!
访问和修改数组元素(用下标/索引)
数组是固定大小的,一旦创建,就不能改变其大小。所以不能添加,删除元素。
但是可以修改现有元素的值。
int[] arr = { 10, 20, 30, 40, 50 };
System.out.println(arr[0]); // 输出 10(第一个)
System.out.println(arr[2]); // 输出 30(第三个)
arr[1] = 999; // 把第二个改成 999
System.out.println(arr[1]); // 输出 999
注意:索引从 0 开始,最后一个是 长度-1!
length 字段,获取数组长度
通过 length 字段(注意不是方法,没有括号)可以获取数组的长度(元素个数)。
int[] nums = { 95, 87, 91, 100, 78 };
System.out.println("数组长度: " + nums.length); // 输出: 数组长度: 5
打印数组
直接打印数组变量,通常会得到一个看不懂的内存地址哈希码(如 [I@1b6d3586)。
要打印数组的内容,需要使用 Arrays.toString() 方法。
import java.util.Arrays;
int[] nums = { 95, 87, 91, 100, 78 };
System.out.println(nums); // 输出类似 [I@...
System.out.println(Arrays.toString(nums)); // 输出: [95, 87, 91, 100, 78]
二维数组(表格)
Java 的二维数组实际上是“数组的数组”。
// 3 行 4 列的成绩表
int[][] matrix = {
{ 95, 86, 77, 88 },
{ 67, 92, 81, 90 },
{ 100, 78, 89, 94 }
};
System.out.println(matrix[0][1]); // 第1行第2列 → 86
System.out.println(matrix[2][0]); // 第3行第1列 → 100
越界错误
如果访问或修改数组中不存在的索引位置,会抛出 ArrayIndexOutOfBoundsException 异常。
如果没有特别捕获处理,程序会终止并显示错误信息。
int[] arr = { 1, 2, 3 };
// arr[5] = 99; // 抛出异常:java.lang.ArrayIndexOutOfBoundsException
什么时候用数组?(真实开发场景)
| 场景 | 为什么用数组 |
|---|---|
方法参数是可变参数 ... | void print(String... arr) 本质是数组 |
| 需要极致性能(游戏主循环) | 连续内存,CPU 缓存最友好 |
存储基本类型 int 等 | 避免 Integer 包装类的开销 |
数组 vs ArrayList
| 你要做什么? | 用谁? | 一句话理由 |
|---|---|---|
| 长度永远不变(比如每月 12 个月) | 数组 | 最简洁 |
| 长度会变(购物车、聊天记录、玩家列表) | ArrayList | 不炸,还自带一堆好用方法 |
| 不知道放多少个,先占个坑 | ArrayList | 数组要先指定长度,太麻烦 |
| 要极致性能(每帧上万次循环) | 数组 | 快一点点,省内存 |
| 需要排序,查找等操作 | ArrayList | 数组操作比较麻烦,List 配合 Collections 很方便 |
真实项目使用率 : 数组 10% , List 90%
命令行参数
Java 程序的 main 方法定义了一个 字符串数组 参数 String[] args,用于接收命令行传入的参数。
public class Program {
public static void main(String[] args) {
System.out.println("命令行参数个数: " + args.length);
for (int i = 0; i < args.length; i++) {
System.out.println("参数 " + i + ": " + args[i]);
}
}
}
然后执行命令
java Program --name byhy --site www.byhy.net
输出结果是:
命令行参数个数: 4
参数 0: --name
参数 1: byhy
参数 2: --site
参数 3: www.byhy.net
可变数量的参数 (Varargs)
假设我们有这样的一个 Map,存储学生年龄,如下:
import java.util.Map;
Map<String, Integer> studentInfo = Map.of(
"张飞", 18,
"赵云", 17,
"许褚", 16,
"典韦", 18,
"关羽", 19
);
要实现一个方法 printAge ,它的输入参数是一些学生的姓名,方法需要打印出这些输入学生的年龄。
这里有个问题,就是调用 printAge 时, 输入的学生名字个数是不固定的,可能是1个、2个、100个, 甚至也可能是0个。
在 Java 中,我们可以使用 ... 语法来定义 可变数量的参数 (Variable Arguments,简称 Varargs)。
void printAge(String... students) {
// 在方法内部,students 就是一个 String[] 数组
for (String student : students) {
System.out.println("学生:" + student + ", 年龄 " + studentInfo.get(student));
}
}
String... 告诉编译器,这个参数可以接受零个或多个 String 类型的参数。
这些参数在方法内部会被组织成一个 String 数组。
然后我们就可以这样调用该方法了:
printAge("张飞", "典韦", "关羽");
System.out.println("---");
printAge("赵云");
效果和我们期望的一样。
在调用该方法的时候,Java 编译器会创建一个数组来存储传入的参数。
✳️ 规则限制
- 可变参数 必须是参数列表的最后一个参数
void log(String prefix, String... messages) { } // ✅ 正确
// void bad(int... nums, String suffix) { } // ❌ 编译错误!可变参数不在最后
- 一个方法只能有一个可变参数
✳️ 现成数组
printAge 使用上面这样的可变参数,假如我们要传入的参数恰好已经在一个数组中,比如
String[] onebatch = {"张飞", "典韦", "关羽"};
怎么调用 printAge 呢?
在 Java 中,直接把数组传递给可变参数即可:
printAge(onebatch);
这和 printAge("张飞", "典韦", "关羽") 的调用效果是完全一样的。
终端输入
现在我们 再来看一下 System.out.println 方法。
System 是一个类,out 是它的一个静态字段,println 是 out 对象的一个方法。
你可能会问,什么是终端?
简单来说,就是我们运行程序时看到的那个命令行窗口,通常是黑色的背景。
我们目前编写的程序,都是使用这个窗口来 输出信息 和 输入信息 的。
想象一下,你如果要开发一个计算税后薪资的软件,需要用户输入员工的薪资,怎么做?
当然可以开发一个带图形界面的程序,但是图形界面的开发需要更多的基础知识。
我们先学习如何在终端上,让用户用键盘输入信息。
前面我们已经学过,使用 System.out.println() 方法可以 输出 信息到屏幕上。
那么,我们怎么让用户 输入 信息给程序呢?
Scanner 类
在 Java 中,读取控制台输入最常用的方式是使用 java.util.Scanner 类。
import java.util.Scanner; // 必须导入 Scanner 类
void main() {
// 创建一个 Scanner 对象,扫描 System.in (标准输入流)
Scanner scanner = new Scanner(System.in);
// 打印提示信息
System.out.print("请输入张三的薪资:");
// 等待用户输入一行文本
String salaryStr = scanner.nextLine();
System.out.println("你输入的薪资是:" + salaryStr);
scanner.close(); // 使用完 Scanner 后关闭它
}
代码解释:
-
System.out.print():和println()类似,但它打印后不会换行,这样用户的输入光标会紧跟在提示信息后面,体验更好。 -
scanner.nextLine():当程序执行到这一行时,会暂停执行,等待用户在终端中输入内容。用户输入完毕后,敲一个回车键,nextLine()方法就会返回用户输入的全部内容(字符串)。程序才会继续执行下面的代码。
注意:scanner.nextLine() 方法返回的用户输入内容,永远都是 String(字符串) 类型。
如果我们想根据用户输入的薪资计算出税后薪资(假设税率25%,即扣税后剩余75%),下面的代码是错误的:
System.out.print("请输入张三的薪资:");
String salary = scanner.nextLine();
// 下面这行代码无法通过编译!
// double realSalary = salary * 0.75;
因为 Java 是强类型语言,字符串 String 类型不能直接和数字 double 类型进行数学运算。
要解决这个问题,我们必须进行类型转换。
我们需要把用户输入的、代表数字的字符串,显式地转换为真正的数字类型(如 int, double)。
Double.parseDouble(String):这个静态方法可以将一个内容为数字的字符串,转换为double类型的数字。Integer.parseInt(String):转换为int。
正确的代码应该这样写:
import java.util.Scanner;
void main() {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入薪资:");
String salaryStr = scanner.nextLine();
// 1. 先将字符串转化为 double 数字类型
double salary = Double.parseDouble(salaryStr);
// 2. 进行数学计算
double realSalary = salary * 0.75;
// 3. 将计算结果拼接成最终的输出字符串
System.out.println("税后薪资是:" + realSalary);
}
读取单个字符
System.in.read() 方法用于从终端读取单个字符,并返回该字符的 Unicode 编码(int 类型)。
System.out.println("按任意键继续...");
int charCode = System.in.read(); // 读取单个字符的 Unicode 编码
char ch = (char)charCode; // 将编码转换为字符
System.out.println("你按下的键是: " + ch);