Java 中使用 List
Java List
是Java语言中最为常用的集合类之一。它提供了一种有序的、可重复且可以自由操作的数据存储方式,可以存储任意类型的对象。List
可以视为一个可以进行增删改查自动调整大小操作方便的数组。
为什么使用 List
存储一组相同类型的数据是编码过程中很常见的需求,如果没有集合类,我们通常会选择使用数组来实现,但是使用数组有很多不便,比如长度固定,数组的长度一旦确定,就不能修改。
这是如果再新增元素,会直接报错。
// arr[7] = "c";
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 7 out of bounds for length 6
at com.wdbyte.collection.ArrayListTest.main(ArrayListTest.java:14)
在数组中删除一个元素也会遇到问题,删除一个数组的中间元素时数组不会自动变得紧凑,这样即使空出了一个位置,再新增元素时也要指定这个位置才能填上。但是使用 List
不会有这些问题。
List 的概念
List 是Java 提供的一个接口,它继承自 SequencedCollection
(Java 21)接口,代表一组有序、可重复的元素。在 Java 中,我们可以使用 List 来存储任意类型的对象,比如整数、字符串等。
在 Java 21 中,新增了 SequencedCollection 接口,List 继承自
SequencedCollection
接口,在此之前,继承自 Collection 接口。
使用 List 有以下几个好处:
-
有序性:List中的元素是按照插入顺序排列的,可以通过索引来访问和操作元素。
-
可重复性:List允许存储重复的元素,这在某些场景下非常有用。
-
动态性:List的大小是可变的,我们可以根据需要动态地添加、删除元素。当容量不够时,会自动扩容。
-
丰富的方法:List 提供了许多常用的方法,方便我们进行数据的操作和处理。
List 常用方法
创建 List
可以直接 new
一个 List。
List<String> list = new ArrayList();
也可以在创建 List 时指定值,但是这种方式创建的 List
不能修改。
List<String> list = List.of("www", "wdbyte", "com");
添加元素
List提供了多种添加元素的方法,比如 add()
、addAll()
等。我们可以向List中添加单个元素,也可以同时添加多个元素。
List<String> list = new ArrayList();
list.add("a");
list.add("b");
List<String> list2 = new ArrayList();
list2.add("x");
list2.addAll(list);
System.out.println(list2); // 输出: [x, a, b]
获取元素
可以通过索引来获取List中的元素,使用 get()
方法。索引从0开始,最后一个元素的索引是 size() - 1
。
list.get(0)
修改元素
可以通过索引来修改List中的元素,使用 set()
方法。
list.set(0, "x");
删除元素
可以通过索引或者元素值来删除List中的元素,使用 remove()
方法。
list.remove(1);
判断元素是否存在
可以使用 contains()
方法来判断List中是否包含某个元素。
list.contains("x");
获取List的大小
使用 size()
方法可以获取List的大小,即包含的元素个数。
list.size();
遍历 List
可以使用 FOR
循环、迭代器或者Java 8引入的 Lambda 表达式等方式来遍历 List 中的元素。
方式1:
for (String string : list) {
System.out.print(string);
}
方式2:
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i));
}
方式3:
list.forEach(s -> {
System.out.println(s);
});
方式4:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
测试代码:
List<String> list = new ArrayList();
list.add("a");
list.add("b");
System.out.println("当前集合:"+list);
System.out.println("获取第一个元素:"+list.get(0));
list.set(0, "x");
System.out.println("修改第一个元素为 x:"+list);
list.remove(1);
System.out.println("移除第2个元素后剩余:"+list);
System.out.println("判断元素 x 是否存在:" + list.contains("x"));
System.out.println("判断元素 a 是否存在:" + list.contains("a"));
System.out.println("当前 list 大小:" + list.size());
list.add("y");
list.add("z");
System.out.println("添加两个元素:"+list);
System.out.println("遍历元素:");
for (String string : list) {
System.out.print(string);
}
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i));
}
list.forEach(System.out::print);
输出:
当前集合:[a, b]
获取第一个元素:a
修改第一个元素为 x:[x, b]
移除第2个元素后剩余:[x]
判断元素 x 是否存在:true
判断元素 a 是否存在:false
当前 list 大小:1
添加两个元素:[x, y, z]
遍历元素
xyzxyzxyz
排序 List
方式1:
List<String> list = new ArrayList();
list.add("b");
list.add("c");
list.add("a");
list.add("d");
list = list.stream().sorted().collect(Collectors.toList());
System.out.println(list); // [a, b, c, d]
// 排序时指定降序还是升序:Comparator.reverseOrder() 降序
list = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println(list); // [d, c, b, a]
方式2:
Collections.sort(list);
System.out.println(list); // [a, b, c, d]
// 降序
Collections.sort(list,Comparator.reverseOrder());
System.out.println(list); // [d, c, b, a]
方式3:
list.sort(Comparator.comparing(Function.identity()));
System.out.println(list); // [a, b, c, d]
List 常用转换
List 与 数组
List 转数组:
String[] array = list.toArray(new String[0]);
System.out.println(Arrays.toString(array));
String[] array2 = list.stream().toArray(String[]::new);
System.out.println(Arrays.toString(array2));
数组转 List:
// 方式1
List<String> list0 = Lists.newArrayList(array);
list0.add("e");
System.out.println(list0);
// 方式2
List<String> list1 = Arrays.stream(array).toList();
// list1.add("e"); 报错,不能修改的 List
System.out.println(list1);
// 方式3
List<String> list2 = Arrays.asList(array);
//list2.add("e"); 报错,不能修改的 List
System.out.println(list2);
// 方式4
List<String> list3 = new ArrayList<>(Arrays.asList(array));
list3.add("e");
System.out.println(list3);
// 方式5
List<String> list4 = Arrays.stream(array).collect(Collectors.toList());
list4.add("e");
System.out.println(list4);
List 与 Map
List 转 Map,适用于选择一个属性作为 Map 的 Key 的情况。
class Dog {
String name;
Integer age;
public Dog(String name, Integer age) {
this.name = name;
this.age = age;
}
// get set method
// toString method
}
测试:
List<Dog> list = new ArrayList();
list.add(new Dog("大黄", 1));
list.add(new Dog("小黑", 2));
// 方式1
Map<String, Dog> dogMap = list.stream()
.collect(Collectors.toMap(Dog::getName, Function.identity(), (o, n) -> o));
System.out.println(dogMap.get("大黄"));
// 方式2
Map<String, Dog> dogMap2 = new HashMap<>();
for (Dog dog : list) {
dogMap2.put(dog.getName(), dog);
}
System.out.println(dogMap.get("大黄"));
输出:
Dog{name='大黄', age=1}
Dog{name='大黄', age=1}
List 实现类
List 的实现类有三个是比较常用的,分别是 ArrayList
、LinkedList
、Vector
。上面的代码示例使用 new ArrayList
演示,因为继承自相同接口,所以不同实现类的操作方法一致。
ArrayList
- 特点:ArrayList是基于数组实现的List。它具有随机访问的特点,可以通过索引快速访问元素。
- 适用场景:当需要快速访问元素,并且不需要频繁地进行插入和删除操作时,可以选择ArrayList。
- 性能: ArrayList 在随机访问元素时性能较好,但在插入和删除元素时性能较差,需要进行元素的移动。
根据开发经验,大多数时候都应该选择使用 ArrayList
。
LinkedList
- 特点: LinkedList 是基于链表实现的List。它具有快速的插入和删除元素的特点。
- 适用场景: 当需要频繁地进行插入和删除操作时,可以选择 LinkedList。
- 性能: LinkedList 在插入和删除元素时性能较好,但在随机访问元素时性能较差,需要遍历链表。
关于 ArrayList
和 LinkedList
的实现原理,可以查看:Java ArrayList 和 LinkedList 实现原理
Vector
- 特点: Vector 是线程安全的List,可以在多线程环境下使用。
- 适用场景: 当需要在多线环境应用场景时,可以选择使用 Vector。
- 性能:Vector 在性能上相对较差,因为它实现了线程安全,需要进行同步操作。在单线程环境下,通常使用 ArrayList 来替代 Vector。
CopyOnWriteArrayList
CopyOnWriteArrayList
也是Java中的一个线程安全的 List 集合类。
特点:写时复制,CopyOnWriteArrayList
的特点是在写入数据时,会创建一个新的数组来存储数据,而原来的数组保持不变。这意味着写操作不会影响到读操作,读操作可以在不加锁的情况下进行。因此,CopyOnWriteArrayList
非常适用于读多写少的场景。
适用场景:线程安全,CopyOnWriteArrayList
的写操作是通过加锁来实现的,所以它是线程安全的。对于读操作,由于不涉及数据的修改,所以可以在多个线程之间并发进行,提高了读取的效率。
性能:CopyOnWriteArrayList
非常适用于读多写少的场景,例如缓存、观察者列表等。它的读操作是无锁的,因此非常适合在读多写少的场景中使用。但是需要注意的是,由于每次写操作都会创建一个新的数组,所以会消耗额外的内存,如果内存资源有限,可能不适合使用。
扩展:Java CopyOnWriteArrayList 源码分析
List 注意事项
线程安全问题
当多个线程同时操作一个 List 时,可能会出现线程安全问题。如果需要在多线程环境下使用List,可以考虑使用线程安全的实现类,如 Vector
或 CopyOnWriteArrayList
或使用 Collections
类中提供的 synchronizedList()
方法创建线程安全的List。
空指针异常
在操作List时,需要注意空指针异常。当 List 为空时,调用它的方法会抛出 NullPointerException
异常。在使用List之前,应该先进行空值判断。不仅要判断 null
值,还要判断空值。
if (list != null && !list.isEmpty()) {
System.out.println(list.get(0));
}
ConcurrentModificationException
如果在 List 遍历过程中那个修改 List,你可能会遇到 ConcurrentModificationException
。因为线程安全问题,这是不被允许的。
List<String> list = new ArrayList();
list.add("a");
list.add("b");
for (String string : list) {
if (string.equals("b")) {
list.add("c");
}
}
运行报错:
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
at com.wdbyte.collection.ArrayListTest3.main(ArrayListTest3.java:16)
如果确实想在遍历时删除某个元素,可以使用迭代器。
List<String> arrayList = new ArrayList();
arrayList.add("a");
arrayList.add("b");
Iterator<String> iterator = arrayList.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if ("a".equals(next)) {
iterator.remove();
}
}
System.out.println(arrayList); // output: [b]
元素的顺序
元素的顺序和索引 List中的元素是有序的,并且每个元素都有一个对应的索引。在操作List时,需要注意元素的顺序和索引的关系。插入、删除元素会导致索引的变化,需要相应地调整索引的使用。
List<String> list = new ArrayList();
list.add("a");
list.add("b");
list.remove(0);
String res = list.get(1); // 报错: Index 1 out of bounds for length 1
List 常见问题
List 与Set的区别
List允许存储重复的元素,而Set不允许存储重复的元素。List是有序的,可以通过索引访问元素,而Set是无序的,不能通过索引访问元素。
List与数组的区别
List 是动态的数据结构,大小可变,并且提供了丰富的操作方法。数组是静态的,大小固定,并且操作相对有限。
List 总结
通过本文的介绍,我们了解了Java List的基本概念、用法和常见实现类。List作为一种有序、可重复的数据结构,在Java编程中非常常用。我们可以根据具体的需求选择合适的实现类,如 ArrayList、LinkedList
或 Vector
。在使用 List 的过程中,需要注意线程安全、空指针异常和对元素顺序和索引的正确使用。希望本文对你理解和运用 Java List
有所帮助!
一如既往,文章中代码存放在 Github.com/niumoo/javaNotes.