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 有以下几个好处:

  1. 有序性:List中的元素是按照插入顺序排列的,可以通过索引来访问和操作元素。

  2. 可重复性:List允许存储重复的元素,这在某些场景下非常有用。

  3. 动态性:List的大小是可变的,我们可以根据需要动态地添加、删除元素。当容量不够时,会自动扩容。

  4. 丰富的方法: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 的实现类有三个是比较常用的,分别是 ArrayListLinkedListVector 。上面的代码示例使用 new ArrayList 演示,因为继承自相同接口,所以不同实现类的操作方法一致。

ArrayList

  1. 特点:ArrayList是基于数组实现的List。它具有随机访问的特点,可以通过索引快速访问元素。
  2. 适用场景:当需要快速访问元素,并且不需要频繁地进行插入和删除操作时,可以选择ArrayList。
  3. 性能: ArrayList 在随机访问元素时性能较好,但在插入和删除元素时性能较差,需要进行元素的移动。

根据开发经验,大多数时候都应该选择使用 ArrayList

LinkedList

  1. 特点: LinkedList 是基于链表实现的List。它具有快速的插入和删除元素的特点。
  2. 适用场景: 当需要频繁地进行插入和删除操作时,可以选择 LinkedList。
  3. 性能: LinkedList 在插入和删除元素时性能较好,但在随机访问元素时性能较差,需要遍历链表。

关于 ArrayListLinkedList 的实现原理,可以查看:Java ArrayList 和 LinkedList 实现原理

Vector

  1. 特点: Vector 是线程安全的List,可以在多线程环境下使用。
  2. 适用场景: 当需要在多线环境应用场景时,可以选择使用 Vector。
  3. 性能:Vector 在性能上相对较差,因为它实现了线程安全,需要进行同步操作。在单线程环境下,通常使用 ArrayList 来替代 Vector。

CopyOnWriteArrayList

CopyOnWriteArrayList 也是Java中的一个线程安全的 List 集合类。

特点:写时复制CopyOnWriteArrayList 的特点是在写入数据时,会创建一个新的数组来存储数据,而原来的数组保持不变。这意味着写操作不会影响到读操作,读操作可以在不加锁的情况下进行。因此,CopyOnWriteArrayList 非常适用于读多写少的场景

适用场景:线程安全CopyOnWriteArrayList 的写操作是通过加锁来实现的,所以它是线程安全的。对于读操作,由于不涉及数据的修改,所以可以在多个线程之间并发进行,提高了读取的效率。

性能:CopyOnWriteArrayList 非常适用于读多写少的场景,例如缓存、观察者列表等。它的读操作是无锁的,因此非常适合在读多写少的场景中使用。但是需要注意的是,由于每次写操作都会创建一个新的数组,所以会消耗额外的内存,如果内存资源有限,可能不适合使用。

扩展:Java CopyOnWriteArrayList 源码分析

List 注意事项

线程安全问题

当多个线程同时操作一个 List 时,可能会出现线程安全问题。如果需要在多线程环境下使用List,可以考虑使用线程安全的实现类,如 VectorCopyOnWriteArrayList 或使用 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编程中非常常用。我们可以根据具体的需求选择合适的实现类,如 ArrayListLinkedListVector。在使用 List 的过程中,需要注意线程安全、空指针异常和对元素顺序和索引的正确使用。希望本文对你理解和运用 Java List 有所帮助!

一如既往,文章中代码存放在 Github.com/niumoo/javaNotes.