Java List 接口(java.util.List)用于表示对象的有序序列。在 List 中,元素的插入、访问、迭代和删除操作均可以按照其在集合内部的出现顺序进行,这种有序性正是其被称为 "List" 的核心原因。
List 中的每个元素都对应一个索引值:第一个元素的索引为 0,第二个为 1,以此类推。索引本质上代表 "与列表起始位置的元素间隔数"—— 第一个元素因直接位于列表开端,故与起始位置的间隔为 0。
List 可以存储任何 Java 对象。若未通过泛型指定元素类型,甚至能在同一个 List 中混合存放不同类的对象,但这种用法在实际开发中并不常见。
作为 Java 标准接口,List 继承自 Collection 接口,是 Collection 体系中的重要子类型。
Java List 和 Set 接口非常相似,它们都表示元素集合。不过,它们之间也有一些明显的区别。这些差异反映在 List 和 Set 接口提供的方法中。主要区别如下:
(1)元素的重复性:List 允许同一元素多次出现,而 Set 则要求每个元素具有唯一性,不可重复存在。
(2) 元素的顺序性:List 中的元素存在明确的顺序,可按此顺序对元素进行迭代。Set 对其内部元素的存储顺序不做任何保证。
作为 Collection 的子类型,Collection 接口中的所有方法在 List 接口中也都可用。
由于 List 是一个接口,因此你需要实例化该接口的具体实现才能使用它。你可以在 Java 集合框架的 API 中选择以下 List 实现:
java.util.ArrayList:基于动态数组实现,查询效率高,增删元素(尤其中间位置)效率较低,线程不安全,是日常开发中最常用的 List 实现。
java.util.LinkedList:基于双向链表实现,增删元素效率高(尤其首尾操作),查询效率较低,同时可作为队列、栈使用,线程不安全。
java.util.Vector:与 ArrayList 类似的动态数组实现,但属于线程安全类(方法加锁),因此性能略低,如今已较少推荐使用。
java.util.Stack:继承自 Vector,是基于数组实现的栈结构,遵循后进先出(LIFO)原则,提供 push、pop、peek 等栈操作方法,线程安全但性能一般。
在这些实现中,ArrayList 是最常用的。
注意,在 java.util.concurrent 包中也有并发 List 实现,这些 List 将在 Java 并发编程教程中介绍。
创建 List 的实例很简单,可以通过创建实现了 List 接口的某个类的实例就可以了。例如:
List listA = new ArrayList(); List listB = new LinkedList(); List listC = new Vector(); List listD = new Stack();
上面代码创建了四个 List 实例,分别是不同的 List 子类。
默认情况下,你可以将任何对象放入列表中,但从 Java 5 开始,Java 泛型使得限制可以插入列表的对象类型成为可能。以下是一个示例:
List<String> list = new ArrayList<String>();
这个列表现在只能插入 String 的实例。然后,你可以访问并迭代其元素,而无需对它们进行强制类型转换。示例如下:
import java.util.*; public class Demo { public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("one"); list.add("two"); String val = list.get(0); for(String item : list){ System.out.println(item); } } }
如果没有泛型,上面的示例将会是这样的:
import java.util.*; public class Demo { public static void main(String[] args) { List list = new ArrayList(); list.add("one"); list.add("two"); String val = (String)list.get(0); for(Object item : list){ System.out.println((String)item); } } }
需要注意的是,若要从 List 中检索 String 实例,需将其强制转换为 String 类型。这是因为:若 List 变量声明时未指定泛型类型,Java 编译器仅能识别该 List 存储的是 Object 实例,因此必须手动将其强转为你已知的具体类(或接口)类型。
在声明 List 变量时尽可能指定泛型类型,是一种推荐的编程实践。这样做有三大优势:
(1)能避免向 List 中插入错误类型的对象。
(2)从 List 检索对象时,无需再将其强转为实际类型。
(3)能让代码阅读者直观了解该 List 应存储的对象类型。
只有在具备充分且合理的理由时,才建议省略泛型类型。在本 Java List 教程的后续内容中,我将尽可能采用带有泛型的 List 示例进行讲解。
你可以使用 List 的 add() 方法向其中插入元素(对象)。例如:
List<String> listA = new ArrayList<>(); listA.add("element 1"); listA.add("element 2"); listA.add("element 3");
上述代码向 List 中添加了 3 个字符串。
List 允许插入空(null)值,例如:
Object element = null; List<Object> list = new ArrayList<>(); list.add(element);
甚至,还能在指定的位置插入元素。List 接口有一个 add() 方法的版本,它将索引作为第一个参数,要插入的元素作为第二个参数。例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> listA = new ArrayList<>(); listA.add("element 1"); listA.add("element 2"); listA.add("element 3"); // 插入到下标0的位置 listA.add(0, "element 4"); System.out.println(listA); } }
运行结果:
[element 4, element 1, element 2, element 3]
如果列表(List)中已经包含元素,这些元素现在会在列表的内部序列中被进一步后移。在新元素插入到索引 0 之前原本位于索引 0 的元素,将会被移至索引 1,以此类推。
除了上述方法,List 还提供了一个批量添加元素的方法 addAll()。它会将参数列表中的元素逐一添加到当前列表,得到的列表(List)是这两个列表的并集。例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> listSource = new ArrayList<>(); listSource.add("123"); listSource.add("456"); List<String> listDest = new ArrayList<>(); listSource.add("789"); listDest.addAll(listSource); System.out.println("listDest=" + listDest); } }
运行结果:
listDest=[123, 456, 789]
此示例将 listSource 中的所有元素添加到 listDest 中。
注意,addAll() 方法以一个 Collection 作为参数,因此你可以传入 List 或 Set 作为参数。换句话说,你可以使用 addAll() 将 List 或 Set 中的所有元素添加到一个 List 中。
你可以通过元素的索引从 List 中获取元素(索引从 0 开始),使用 get(int index) 方法来实现。例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> listA = new ArrayList<>(); listA.add("element 0"); listA.add("element 1"); listA.add("element 2"); for(int i = 0; i < listA.size(); i++) { System.out.println(listA.get(i)); } } }
运行结果:
element 0 element 1 element 2
你可以使用以下两个方法中任意一个在 List 中查找元素:
indexOf() 用于查找给定元素在列表(List)中首次出现的索引。
lastIndexOf() 用于查找指定元素在列表中最后一次出现的索引位置。
例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); // 0 list.add("element 3"); // 1 list.add("element 2"); // 2 list.add("element 3"); // 3 int index1 = list.indexOf("element 3"); // 首次出现 int index2 = list.lastIndexOf("element 3"); // 最后一次出现 System.out.println("index1 = " + index1); // index1 = 1 System.out.println("index2 = " + index2); // index2 = 3 } }
运行结果:
index1 = 1 index2 = 3
你可以使用 List 的 contains() 方法来检查 List 是否包含某个特定元素。例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); list.add("element 3"); boolean containsElement = list.contains("element 2"); System.out.println(containsElement); containsElement = list.contains("element 4"); System.out.println(containsElement); } }
运行结果:
true false
为了确定列表是否包含某个元素,列表会在内部遍历其元素,并将每个元素与作为参数传入的对象进行比较。这种比较会使用元素的 equals 方法来检查该元素是否与参数相等。
由于可以向列表(List)中添加空(null)值,因此实际上可以检查列表(List)中是否包含空(null)值。以下是检查列表(List)中是否包含空(null)值的方法:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); list.add(null); boolean containsElement = list.contains(null); System.out.println(containsElement); } }
显然,如果传给 contains() 的输入参数是 null,那么 contains() 方法不会使用 equals() 方法来与每个元素进行比较,而是会使用 == 运算符。如下图:
你可以通过以下两种方法从 List 中移除元素:
remove(Object element) 用于从 List 中移除首次出现的指定元素(通过 equals() 方法判断相等性)。若元素存在且被成功移除,返回 true;若列表中无此元素,返回 false。此方法会改变元素的索引(移除位置后的元素索引自动减 1)。
remove(int index) 用于移除 List 中指定索引位置的元素,返回被移除的元素。索引需在 0 到 size()-1 范围内,否则会抛出 IndexOutOfBoundsException。同样,移除后该位置后的元素索引会减 1。
例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); list.add("element 3"); System.out.println("移除前:" + list); boolean removeFlag = list.remove("element 2"); System.out.println(removeFlag ? "移除成功" : "移除失败"); removeFlag = list.remove("element 4"); // 不存在的元素 System.out.println(removeFlag ? "移除成功" : "移除失败"); System.out.println("移除后:" + list); } }
运行结果:
移除前:[element 1, element 2, element 3] 移除成功 移除失败 移除后:[element 1, element 3]
以下是一个根据索引从 List 中移除元素的示例:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); list.add("element 3"); System.out.println("移除前:" + list); String removeFlag = list.remove(2); System.out.println(removeFlag + "移除成功"); try { list.remove(3); // 不存在的元素 } catch (IndexOutOfBoundsException e) { System.out.println("移除元素失败,下标越界。" + e.getMessage()); } System.out.println("移除后:" + list); } }
运行结果:
移除前:[element 1, element 2, element 3] element 3移除成功 移除元素失败,下标越界。Index: 3, Size: 2 移除后:[element 1, element 2]
List 接口包含一个 clear() 方法,调用该方法时会从列表中移除所有元素。从 List 中移除所有元素也称为清空 List。例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); System.out.println("清空前 list=" + list); list.clear(); // 清空 System.out.println("清空后 list=" + list); } }
运行结果:
清空前 list=[element 1, element 2] 清空后 list=[]
List 接口提供了 retainAll() 方法,其核心作用是保留当前 List 中所有同时存在于另一个 List 中的元素。换而言之,该方法会从调用它的 “目标 List” 中,移除所有在 “另一个 List” 里不存在的元素,最终使目标 List 成为两个列表的交集(仅包含两者共有的元素)。
需要注意的是,方法名是 retainAll()(末尾带有 All),而非 retain()。以下是调用 List 的 retainAll() 方法的 Java 示例:
package com.hxstrive.java_collection.list; import java.util.ArrayList; import java.util.List; public class ListRetainAllExample { public static void main(String[] args) { // 1. 创建目标 List(待处理的列表) List<String> fruits = new ArrayList<>(); fruits.add("apple"); fruits.add("banana"); fruits.add("orange"); fruits.add("grape"); // 2. 创建用于比对的 List(仅保留两者共有的元素) List<String> favoriteFruits = new ArrayList<>(); favoriteFruits.add("apple"); favoriteFruits.add("grape"); favoriteFruits.add("mango"); // 3. 调用 retainAll() 方法:保留 fruits 中存在于 favoriteFruits 的元素 boolean isChanged = fruits.retainAll(favoriteFruits); // 4. 输出结果 System.out.println("列表是否发生修改:" + isChanged); // 输出 true(有元素被移除) System.out.println("处理后的 fruits 列表(交集):" + fruits); // 输出 [apple, grape] } }
运行结果:
列表是否发生修改:true 处理后的 fruits 列表(交集):[apple, grape]
通过调用 List 的 size() 方法可以获取列表(List)中元素的数量。例如:
List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); int size = list.size(); // 2
List 接口提供了 subList() 方法,其核心功能是从原始 List 中截取部分元素,生成一个新的子列表(子列表仍是原列表的视图,非独立副本)。
subList() 方法需传入两个参数,分别对应截取范围的起始索引与结束索引,规则如下:
起始索引:指定原始 List 中要纳入子列表的第一个元素的位置(包含该元素),索引值需满足 0 ≤ 起始索引 ≤ 结束索引。
结束索引:指定子列表的截止位置(不包含该索引对应的元素),索引值需满足 起始索引 ≤ 结束索引 ≤ 原始 List 长度。
这一取值逻辑与 Java 中 String 类的 substring() 方法完全一致,均遵循 “左闭右开” 的范围规则。
例如:
package com.hxstrive.java_collection.list; import java.util.ArrayList; import java.util.List; public class SubListExample { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); list.add("element 3"); list.add("element 4"); // 从索引1(包含)到索引3(不包含)截取子列表 List<String> sublist = list.subList(1, 3); // 输出原始列表和子列表内容 System.out.println("原始列表: " + list); // [element 1, element 2, element 3, element 4] System.out.println("子列表(sublist): " + sublist); // [element 2, element 3] // 注意:子列表是原始列表的视图,修改子列表会影响原始列表 sublist.set(0, "modified element"); System.out.println("修改子列表后,原始列表: " + list); // [element 1, modified element, element 3, element 4] } }
运行结果:
原始列表: [element 1, element 2, element 3, element 4] 子列表(sublist): [element 2, element 3] 修改子列表后,原始列表: [element 1, modified element, element 3, element 4]
若需将 List 转换为 Set,可通过创建新的 Set 实例,并将 List 中的所有元素传入该 Set 来实现。由于 Set 本身具有 “元素不可重复” 的特性,在转换过程中会自动移除 List 中的重复元素,最终得到的 Set 会包含 List 中的全部非重复元素,且每个元素仅出现一次。
例如:
package com.hxstrive.java_collection.list; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; public class ListToSetExample { public static void main(String[] args) { // 1. 创建包含重复元素的 List List<String> fruitList = new ArrayList<>(); fruitList.add("apple"); fruitList.add("banana"); fruitList.add("apple"); // 重复元素 fruitList.add("orange"); fruitList.add("banana"); // 重复元素 System.out.println("原始 List(含重复): " + fruitList); // 2. 转换为 HashSet(不保证元素顺序) Set<String> fruitHashSet = new HashSet<>(fruitList); System.out.println("转换为 HashSet(去重,无序): " + fruitHashSet); // 3. 转换为 LinkedHashSet(保留元素在 List 中的首次出现顺序) Set<String> fruitLinkedHashSet = new LinkedHashSet<>(fruitList); System.out.println("转换为 LinkedHashSet(去重,保序): " + fruitLinkedHashSet); } }
运行结果:
原始 List(含重复): [apple, banana, apple, orange, banana] 转换为 HashSet(去重,无序): [banana, orange, apple] 转换为 LinkedHashSet(去重,保序): [apple, banana, orange]
你可以使用 List 的 toArray() 方法将 List 转换为 Java 数组。如果调用 toArray() 方法没有指定参数,则返回的是一个 Object[] 数组类型,需要手动进行类型强转。推荐使用携带参数的 toArray() 方法,这样就可以将一个 List 转换为特定类型的数组。 例如:
package com.hxstrive.java_collection.list; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class ListToArrayExample { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("element 1"); list.add("element 2"); list.add("element 3"); list.add("element 3"); // 将List转换为Object数组 Object[] objects = list.toArray(); System.out.println("数组长度: " + objects.length); // 输出:4 System.out.println("数组元素: " + Arrays.toString(objects)); // 推荐:使用带参数的toArray()获取指定类型数组(避免强制转换) String[] strings = list.toArray(new String[0]); System.out.println("指定类型数组元素: " + Arrays.toString(strings)); } }
运行结果:
数组长度: 4 数组元素: [element 1, element 2, element 3, element 3] 指定类型数组元素: [element 1, element 2, element 3, element 3]
请注意,即使我们向 toArray() 方法传递一个大小为 0 的 String 数组,返回的数组也会包含列表中的所有元素。它的元素数量将与该列表的元素数量相同。
不仅可以将 List 转换为数组,数组也可以转换为 List,需要使用 Arrays.asList(),例如:
String[] values = new String[]{ "one", "two", "three" }; List<String> list = Arrays.asList(values); System.out.println("list=" + list); // list=[one, two, three]
可以使用 Collections 的 sort() 方法对 List 进行排序。在前面“Java 集合:Collections 工具类中”中已经了解了,但在接下来的部分中,我将向你展示几种对 List 进行排序的方法。
如果 List 中包含的是实现了 java.lang.Comparable 接口的对象,那么这些对象可以相互比较。在这种情况下,你可以像这样对列表进行排序:
List<String> list = new ArrayList<>(); list.add("c"); list.add("b"); list.add("a"); Collections.sort(list); System.out.println(list); // [a, b, c]
注意, Java 的 String 类实现了 Comparable 接口,因此你可以使用 Collections 的 sort() 方法按照它们的自然顺序对其进行排序。
如果 List 中的对象没有实现 Comparable 接口,或者你希望按照不同于它们的 compare() 实现的顺序对对象进行排序,那么你需要使用 Comparator 实现(java.util.Comparator)。例如:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<Car> list = new ArrayList<>(); list.add(new Car("Volvo V40" , "XYZ 201845", 5)); list.add(new Car("Citroen C1", "ABC 164521", 4)); list.add(new Car("Dodge Ram" , "KLM 845990", 2)); Collections.sort(list, new Comparator<Car>() { @Override public int compare(Car car1, Car car2) { return car1.brand.compareTo(car2.brand); } }); list.forEach(System.out::println); } } class Car{ public String brand; public String numberPlate; public int noOfDoors; public Car(String brand, String numberPlate, int noOfDoors) { this.brand = brand; this.numberPlate = numberPlate; this.noOfDoors = noOfDoors; } @Override public String toString() { return "Car{" + "brand='" + brand + '\'' + ", numberPlate='" + numberPlate + '\'' + ", noOfDoors=" + noOfDoors + '}'; } }
运行结果:
Car{brand='Citroen C1', numberPlate='ABC 164521', noOfDoors=4} Car{brand='Dodge Ram', numberPlate='KLM 845990', noOfDoors=2} Car{brand='Volvo V40', numberPlate='XYZ 201845', noOfDoors=5}
注意,上面示例中的 Comparator 实现。该实现仅比较 Car 对象的 brand 字段。我们可以创建另一个Comparator 实现,用于比较车牌号,甚至是汽车的车门数量。
还可以使用 Lambda 来实现 Comparator。下面是一个示例,它使用 Comparator 接口的三种不同 lambda 实现对 Car 对象的 List 进行排序,每种实现都通过不同的字段来比较 Car 实例:
package com.hxstrive.java_collection; import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<Car> list = new ArrayList<>(); list.add(new Car("Volvo V40" , "XYZ 201845", 5)); list.add(new Car("Citroen C1", "ABC 164521", 4)); list.add(new Car("Dodge Ram" , "KLM 845990", 2)); Comparator<Car> carBrandComparatorLambda = (car1, car2) -> car1.brand.compareTo(car2.brand); Comparator<Car> carNumberPlatComparatorLambda = (car1, car2) -> car1.numberPlate.compareTo(car2.numberPlate); Comparator<Car> carNoOfDoorsComparatorLambda = (car1, car2) -> car1.noOfDoors - car2.noOfDoors; System.out.println("根据brand排序:"); Collections.sort(list, carBrandComparatorLambda); list.forEach(System.out::println); System.out.println("根据numberPlate排序:"); Collections.sort(list, carNumberPlatComparatorLambda); list.forEach(System.out::println); System.out.println("根据noOfDoors排序:"); Collections.sort(list, carNoOfDoorsComparatorLambda); list.forEach(System.out::println); } } class Car{ public String brand; public String numberPlate; public int noOfDoors; public Car(String brand, String numberPlate, int noOfDoors) { this.brand = brand; this.numberPlate = numberPlate; this.noOfDoors = noOfDoors; } @Override public String toString() { return "Car{" + "brand='" + brand + '\'' + ", numberPlate='" + numberPlate + '\'' + ", noOfDoors=" + noOfDoors + '}'; } }
运行结果:
根据brand排序: Car{brand='Citroen C1', numberPlate='ABC 164521', noOfDoors=4} Car{brand='Dodge Ram', numberPlate='KLM 845990', noOfDoors=2} Car{brand='Volvo V40', numberPlate='XYZ 201845', noOfDoors=5} 根据numberPlate排序: Car{brand='Citroen C1', numberPlate='ABC 164521', noOfDoors=4} Car{brand='Dodge Ram', numberPlate='KLM 845990', noOfDoors=2} Car{brand='Volvo V40', numberPlate='XYZ 201845', noOfDoors=5} 根据noOfDoors排序: Car{brand='Dodge Ram', numberPlate='KLM 845990', noOfDoors=2} Car{brand='Citroen C1', numberPlate='ABC 164521', noOfDoors=4} Car{brand='Volvo V40', numberPlate='XYZ 201845', noOfDoors=5}
你可以通过几种不同的方式迭代一个 List。常见的方式如下:
使用迭代器 Iterator
使用增强型for循环 for-each
使用 for 循环
使用 Stream API
迭代列表的第一种方法是使用迭代器(Iterator),例如:
List<String> list = new ArrayList<>(); list.add("first"); list.add("second"); list.add("third"); Iterator<String> iterator = list.iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); }
运行结果:
first second third
上述示例通过调用 List 接口的 iterator() 方法可以获得一个 Iterator。一旦获得了一个迭代器,就可以不断调用它的 hasNext() 方法,直到该方法返回 false 为止。在 while 循环内部,你调用 Iterator 接口的next() 方法来获取迭代器所指向的下一个元素。
迭代列表的第二种方法是使用 Java5 中新增的 for循环(也称为“for each”循环)。例如:
List<String> list = new ArrayList<>(); list.add("first"); list.add("second"); list.add("third"); for(String element : list) { System.out.println(element); }
上述示例代码,for 循环会针对列表中的每个元素执行一次。
迭代列表的第三种方法是使用像这样的标准for循环,例如:
List<String> list = new ArrayList<>(); list.add("first"); list.add("second"); list.add("third"); for(int i=0; i < list.size(); i++) { System.out.println(list.get(i)); }
上述示例代码中,for 循环会创建一个 int 变量并将其初始化为 0。然后,只要 int 变量 i 小于 List 的大小,循环就会继续执行。在每次迭代中,变量 i 都会递增。在 for 循环内部,通过 List 的 get() 方法访问其中的元素,并将递增变量 i 作为参数传入。
迭代 List 的第四种方法是通过 Stream API。要迭代 List,你必须首先从该 List 中获取一个 Stream。在 Java 中,从 List 获取 Stream 是通过调用 List 的 stream() 方法来实现的。例如:
List<String> list = new ArrayList<>(); list.add("first"); list.add("second"); list.add("third"); list.stream().forEach(item -> { System.out.println(item); });
上诉示例中,通过调用 List 的 stream() 方法,获取了表示该 List 中元素的 Stream。一旦获取到 List 中元素的 Stream,就可以通过调用其 forEach() 方法来迭代这个 Stream。
调用 forEach() 方法会使流在内部迭代流中的所有元素,并为流中的每个元素调用作为参数传递给 forEach() 方法的 Consumer 实例。