横扫Java Collections系列 —— List

本文整理了Java中List结构的不同实现,典型的列表操作及实现方式。

List常用实现

ArrayList

简介

在这篇文章中,要学习的是Java集合框架中的ArrayList,下面会讨论其属性、通用场景以及其优缺点。

ArrayList在Java的核心库中,因此你不需要引入任何额外的库,只需要import语句就可以使用它:

import java.util.ArrayList
复制代码

List表示有序的值序列,其中某些值可以出现多次。

ArrayList是在数组基础上的一种List的实现,会随着添加/删除元素而动态伸缩,其中的元素可以通过索引(从0开始)容易地访问。该实现具有以下特性:

  • 随机访问时间复杂度为O(1)
  • 添加元素均摊时间复杂度为O(1)
  • 插入/删除时间复杂度为O(n)
  • 在无序数组上搜索的时间复杂度为O(n),有序数组上耗时为O(log n)
创建

ArrayList具有多个构造器,我们会在本小节一一介绍。

首先,需要注意ArrayList是一个泛型类,因此你可以通过参数指定任何你需要的类型,编译器会对其进行验证。举例来说,你无法将一个Integer对象插入String类的集合中。同样,当你需要从集合中获取对象时也无需做转换。

使用通用接口List作为变量的类型是一种比较好的做法,这样可以将变量与特定的实现进行解耦。

默认无参构造函数
List<String> list = new ArrayList<>();
assertTrue(list.isEmpty());
复制代码

很容易就可以创建一个空的ArrayLis实例。

接受初始容量参数的构造函数
List<String> list = new ArrayList<>(20);
复制代码

在该方法中,可以指定底层数组的初始化长度,有助于避免在增加新元素时出现不必要的扩容操作。

接受集合参数的构造函数
Collection<Integer> number 
  = IntStream.range(0, 10).boxed().collect(toSet());
 
List<Integer> list = new ArrayList<>(numbers);
assertEquals(10, list.size());
assertTrue(numbers.containsAll(list));
复制代码

注意,Collection实例中的元素会填充入底层数组中。

添加元素

ArrayList中,可以在末尾或者指定位置插入元素:

List<Long> list = new ArrayList<>();
 
list.add(1L);
list.add(2L);
list.add(1, 3L);
 
assertThat(Arrays.asList(1L, 3L, 2L), equalTo(list));
复制代码

也可以同时插入集合或者多个元素:

List<Long> list = new ArrayList<>(Arrays.asList(1L, 2L, 3L));
LongStream.range(4, 10).boxed()
  .collect(collectingAndThen(toCollection(ArrayList::new), ys -> list.addAll(0, ys)));
assertThat(Arrays.asList(4L, 5L, 6L, 7L, 8L, 9L, 1L, 2L, 3L), equalTo(list));
复制代码
列表迭代

ArrayList可以使用两种类型的迭代器:IteratorListIterator,通过第一类迭代器可以按照一个方向遍历列表,第二类迭代器可以对列表进行双向遍历。

下面是ListIterator的使用方式:

List<Integer> list = new ArrayList<>(
  IntStream.range(0, 10).boxed().collect(toCollection(ArrayList::new))
);
ListIterator<Integer> it = list.listIterator(list.size());
List<Integer> result = new ArrayList<>(list.size());
while (it.hasPrevious()) {
    result.add(it.previous());
}
 
Collections.reverse(list);
assertThat(result, equalTo(list));
复制代码

也可以使用迭代器对元素进行查询、添加或者删除操作。

列表搜索

接下来演示如何在集合中进行搜索操作:

List<String> list = LongStream.range(0,16)
	.boxed()
	.map(Long::toHexString)
	.collect(toCollection(ArrayList::new));
List<String> stringsToSearch = new ArrayList<>(list);
stringsToSearch.addAll(list);
复制代码
搜索无序数组

如果需要在列表中查找某个元素,可以使用indexOf()或者lastIndexOf()方法,这两个方法都接受一个对象为入参,并返回int值。

assertEquals(10, stringsToSearch.indexOf("a"));
assertEquals(26, stringsToSearch.lastIndexOf("a"));
复制代码

如果你想要找到满足条件的所有元素,可以使用Java8中的Stream APIPredicate)来实现:

Set<String> matchingStrings = new HashSet<>(Arrays.asList("a", "c", "9"));
 
List<String> result = stringsToSearch
  .stream()
  .filter(matchingStrings::contains)
  .collect(toCollection(ArrayList::new));
 
assertEquals(6, result.size());
复制代码

也可以使用for循环或者迭代器来实现:

Iterator<String> it = stringsToSearch.iterator();
Set<String> matchingStrings = new HashSet<>(Arrays.asList("a", "c", "9"));
 
List<String> result = new ArrayList<>();
while (it.hasNext()) {
    String s = it.next();
    if (matchingStrings.contains(s)) {
        result.add(s);
    }
}
复制代码
搜索有序数组

如果数组是有序的,则使用二分搜索会比线性搜索更快:

List<String> copy = new ArrayList<>(stringsToSearch);
Collections.sort(copy);
int index = Collections.binarySearch(copy, "f");
assertThat(index, not(equalTo(-1)));
复制代码

如果元素不存在则会返回-1。

删除元素

如果你要删除一个元素,你首先需要找到该元素的索引,如何通过调用remove()方法删除该元素。remove()方法的一个重载方法会接受一个对象作为参数,在列表中搜索并删除与其相等的第一个元素:

List<Integer> list = new ArrayList<>(
  IntStream.range(0, 10).boxed().collect(toCollection(ArrayList::new))
);
Collections.reverse(list);
 
list.remove(0);
assertThat(list.get(0), equalTo(8));
 
list.remove(Integer.valueOf(0));
assertFalse(list.contains(0));
复制代码

但是在处理Integer之类的装箱类型时需要注意,如果要删除某个元素,你需要首先将int值进行装箱操作,否则删除的将是该索引对应的元素。

同样,你也可以使用前面提到的Stream API来删除一些元素,这里不做展示。下面是使用迭代器的操作方法:

Set<String> matchingStrings
 = HashSet<>(Arrays.asList("a", "b", "c", "d", "e", "f"));
 
Iterator<String> it = stringsToSearch.iterator();
while (it.hasNext()) {
    if (matchingStrings.contains(it.next())) {
        it.remove();
    }
}
复制代码

LinkedList

简介

LinkedListListDeque接口的双向链表实现,它实现了所有的列表操作,并且可以容纳所有的元素(包括null)。

特性

下面是LinkedList的主要特性:

  • 需要索引到链表内的操作将从头部或尾部对链表进行遍历,从离索引位置更近的一端开始;
  • 不保证线程安全的,即非synchronized
  • 其中的IteratorListIterator迭代器都采用了快速失败机制(也就是说,在迭代器创建之后,如果链表被修改则会抛出ConcurrentModificationException 异常)。
  • 其中的每一个元素就是一个节点,每个节点中保存着指向前驱节点与后继节点的索引。
  • 保持插入顺序。

尽管LinkedList是非同步的,但是可以通过调用*Collections.synchronizedList*获得同步版本,如下所示:

List list = Collections.synchronizedList(new LinkedList(...));
复制代码
ArrayList的比较

虽然这两种都是List接口的实现,但是却有着不同的语义,而这会影响我们对数据结构的选择。

结构

ArrayList是在Array基础上的一种基于索引的数据结构,随机访问其元素的性能为O(1)。

LinkedList是以链表的形式存储数据的,每个元素与其前后的节点缀连。在这种情况下,对一个元素的搜索操作需要的时间复杂度为O(n)。

操作

LinkedList中对元素的插入、添加和删除操作执行速度更快,因为不需要调整数组大小,当元素插入到集合中的任意位置时也不需要调整索引,只有前后的元素需要修改。

内存使用

LinkedList相比ArrayList要消耗更多的内存,因为其中的每个节点都存储两个引用,分别指向前驱节点以及后继节点,而ArrayList中只保存数据和索引。

使用

下面是一些关于如何使用LinkedList的示例代码

创建
LinkedList<Object> linkedList = new LinkedList<>();
复制代码
增加元素

LinkedList实现了ListDeque接口,除了标准的*add()addAll()方法之外,还有addFirst()addLast()*方法,分别会在头尾添加新元素。

删除元素

与增加元素类似,LinkedList提供了removeFirst()removeLast()方法。同样,也有便利的方法removeFirstOccurence()removeLastOccurence(),它们的返回值为boolean(如果集合中包含指定元元素,则返回true)。

队列操作

Deque接口提供了队列类的操作(实际上Deque继承了Queue接口):

linkedList.poll();
linkedList.pop();
复制代码

这两个方法会获取链表中的第一个元素,并从链表中删除该元素。

poll()pop()方法的区别在于,当操作空链表时,pop会抛出NoSuchElementException()异常,而poll会返回NULLpollFirst()pollLast() 也是可用的。

下面是push方法的示例:

linkedList.push(Object o);
复制代码

该方法会在集合的头部插入元素。

LinkedList 还有很多其它的方法,对于使用过Lists的用户而言,其中大多数方法已经熟悉了。其它的方法是Deque接口提供的,是“标准”方法的便利替代方案。

总结

ArrayList通常是List接口的默认实现。

但是,在一些重要的使用场景中,LinkedList是更好的选择,如追求常数级的插入/删除耗时,而不要求常数级访问耗时和高效的内存使用(例如,频繁的插入/删除/更新)。

CopyOnWriteArrayList

简介

*CopyOnWriteArrayList*是一个在多线程编程中很有用的数据结构,我们可以在无需显式同步操作的前提下,对一个列表进行线程安全的遍历操作。

CopyOnWriteArrayList API

CopyOnWriteArrayList的设计中使用了一个有趣的技巧,不需要同步操作即具有线程安全特性。当我们使用任何会对列表结构进行修改的方法(比如add()或者remove())时,CopyOnWriteArrayList中的所有内容将被复制到一个内部副本中。

通过这个简单的实现,我们可以安全地对列表进行迭代,即使会有并发的修改操作

当我们在对CopyOnWriteArrayList调用iterator()方法时,我们会得到一个由CopyOnWriteArrayList内容的不可变快照备份的迭代器。其中的内容是创建迭代器时ArrayList中数据的精确副本。即使在此期间,其他线程在列表中添加或删除了某个元素,该修改也会生成数据的新副本,该副本将用于对该列表进行的后续数据查找。

CopyOnWriteArrayList这种数据结构的特点使它在迭代操作多于修改时更加有用,如果在应用场景中存在频繁的增加元素操作,那么CopyOnWriteArrayList就不是好的选择,因为多余的副本会导致性能的急剧下降。

插入时对CopyOnWriteArrayList进行迭代

首先,创建一个存储整型数的CopyOnWriteArrayList实例:

CopyOnWriteArrayList<Integer> numbers = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 8});
复制代码

接下来,我们要对数组进行迭代,所以创建一个Iterator实例:

Iterator<Integer> iterator = numbers.iterator();
复制代码

迭代器创建之后,我们向列表中增加一个新元素:

numbers.add(10);
复制代码

注意,当我们创建CopyOnWriteArrayList的迭代器时,得到的是调用iterator()方法时列表中的数据的一个不可变快照。

因此,当我们对其进行迭代时,其中没有数字10:

List<Integer> result = new LinkedList<>();
iterator.forEachRemaining(result::add);
 
assertThat(result).containsOnly(1, 3, 5, 8);
复制代码

接着使用新创建的Iterator进行迭代会发现其中包含新加入的数字10:

Iterator<Integer> iterator2 = numbers.iterator();
List<Integer> result2 = new LinkedList<>();
iterator2.forEachRemaining(result2::add);
 
assertThat(result2).containsOnly(1, 3, 5, 8, 10);
复制代码
不允许在迭代时进行删除

CopyOnWriteArrayList设计的目的,在于即使底层列表数据被修改,仍然允许用户安全地对元素进行遍历。

由于迭代器的拷贝机制,对于返回的Iterator执行remove()方法是不被允许的,这会导致UnsupportedOperationException

@Test(expected = UnsupportedOperationException.class)
public void IterateOverItAndTryToRemoveElement() {
     
    CopyOnWriteArrayList<Integer> numbers
      = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 8});
 
    Iterator<Integer> iterator = numbers.iterator();
    while (iterator.hasNext()) {
        iterator.remove();
    }
}
复制代码

List的常用操作及实现

不可变列表

集合类在Java中是引用类型,在操作的时候可能不经意间被程序修改,类似的“失误”经常会在程序的其它地方导致错误,往往很难定位问题根源。让ArrayList不可改变,是一个防御性编程技术,可以使用以下方式实现。

JDK

首先,JDK提供了一个方法,可以根据已有的列表获得一个不可变的集合对象:

Collections.unmodifiableList(list);
复制代码

获得的新集合对象将不能被修改:

@Test(expected = UnsupportedOperationException.class)
public void UnmodifiableListIsCreated() {
    List<String> list = new ArrayList<String>(Arrays.asList("one", "two", "three"));
    List<String> unmodifiableList = Collections.unmodifiableList(list);
    unmodifiableList.add("four");
}
复制代码

JDK1.8的Collections类中使用了一个静态内部类UnmodifiableList对底层数组进行了封装,当调用get()方法时,会返回对应的元素,如果调用set()add()等修改方法时,则会直接抛出异常UnsupportedOperationException

Guava

Guava中通过ImmutableList提供了一个类似的功能:

ImmutableList.copyOf(list);
复制代码

同样的,得到的结果列表也是不可修改的:

@Test(expected = UnsupportedOperationException.class)
public void UnmodifiableListIsCreatedUsingGuava() {
    List<String> list = new ArrayList<String>(Arrays.asList("one", "two", "three"));
    List<String> unmodifiableList = ImmutableList.copyOf(list);
    unmodifiableList.add("four");
}
复制代码

注意这里的操作实际上返回的是原始列表的一个副本,而不只是一个视图。

Guava同样提供了一个构建器,该构建器将返回强类型的ImmutableList而不是简单的List

@Test(expected = UnsupportedOperationException.class)
public void UnmodifiableListIsCreatedUsingGuavaBuilder() {
    List<String> list = new ArrayList<String>(Arrays.asList("one", "two", "three"));
    ImmutableList<Object> unmodifiableList = ImmutableList.builder().addAll(list).build();
    unmodifiableList.add("four");
}
复制代码
Apache Commons Collections

Commons Collection也提供了一个API用于创建不可变列表:

ListUtils.unmodifiableList(list);
复制代码

同样,对结果列表进行修改操作会导致UnsupportedOperationException异常:

@Test(expected = UnsupportedOperationException.class)
public void UnmodifiableListIsCreatedUsingCommonsCollections() {
    List<String> list = new ArrayList<String>(Arrays.asList("one", "two", "three"));
    List<String> unmodifiableList = ListUtils.unmodifiableList(list);
    unmodifiableList.add("four");
}
复制代码

划分列表

这里讨论的划分是将列表拆分为多个给定大小的子列表。这样一个相对简单的需求,Java的标准集合框架API中居然不支持,还好Guava和Apache Commons Collections都实现了该操作。

Guava

Guava通过**Lists.partition**操作来完成列表的划分:

@Test
public void ParitioningIntoNSublists() {
    List<Integer> intList = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8);
    List<List<Integer>> subSets = Lists.partition(intList, 3);
 
    List<Integer> lastPartition = subSets.get(2);
    List<Integer> expectedLastPartition = Lists.<Integer> newArrayList(7, 8);
    assertThat(subSets.size(), equalTo(3));
    assertThat(lastPartition, equalTo(expectedLastPartition));
}
复制代码

Guava中也可以对其它的集合类型进行划分:

@Test
public void ParitioningIntoNSublists() {
    Collection<Integer> intCollection = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8);
 
    Iterable<List<Integer>> subSets = Iterables.partition(intCollection, 3);
 
    List<Integer> firstPartition = subSets.iterator().next();
    List<Integer> expectedLastPartition = Lists.<Integer> newArrayList(1, 2, 3);
    assertThat(firstPartition, equalTo(expectedLastPartition));
}
复制代码

需要注意划分后的子列表只是源集合类型的视图,对源集合的操作会影响子列表:

@Test
public void OriginalListModified() {
    // Given
    List<Integer> intList = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8);
    List<List<Integer>> subSets = Lists.partition(intList, 3);
 
    // When
    intList.add(9);
 
    // Then
    List<Integer> lastPartition = subSets.get(2);
    List<Integer> expectedLastPartition = Lists.<Integer> newArrayList(7, 8, 9);
    assertThat(lastPartition, equalTo(expectedLastPartition));
}
复制代码
Apache Commons Collections
@Test
public void ParitioningIntoNSublists() {
    List<Integer> intList = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8);
    List<List<Integer>> subSets = ListUtils.partition(intList, 3);
 
    List<Integer> lastPartition = subSets.get(2);
    List<Integer> expectedLastPartition = Lists.<Integer> newArrayList(7, 8);
    assertThat(subSets.size(), equalTo(3));
    assertThat(lastPartition, equalTo(expectedLastPartition));
}
复制代码

该方法不能像Guava一样对原生集合类进行划分,与Guava一样的是,该方法返回的是源列表的一个视图。

Java 8 API

使用Collectors.partitioningBy()将列表分为两个子列表:

@Test
public void ParitioningIntoSublistsUsingPartitionBy {
    List<Integer> intList = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8);
 
    Map<Boolean, List<Integer>> groups = 
      intList.stream().collect(Collectors.partitioningBy(s -> s > 6));
    List<List<Integer>> subSets = new ArrayList<List<Integer>>(groups.values());
 
    List<Integer> lastPartition = subSets.get(1);
    List<Integer> expectedLastPartition = Lists.<Integer> newArrayList(7, 8);
    assertThat(subSets.size(), equalTo(2));
    assertThat(lastPartition, equalTo(expectedLastPartition));
}
复制代码

此外,也可以使用Collectors.groupingBy():

@Test
public final void ParitioningIntoNSublistsUsingGroupingBy() {
    List<Integer> intList = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7, 8);
 
    Map<Integer, List<Integer>> groups = 
      intList.stream().collect(Collectors.groupingBy(s -> (s - 1) / 3));
    List<List<Integer>> subSets = new ArrayList<List<Integer>>(groups.values());
 
    List<Integer> lastPartition = subSets.get(2);
    List<Integer> expectedLastPartition = Lists.<Integer> newArrayList(7, 8);
    assertThat(subSets.size(), equalTo(3));
    assertThat(lastPartition, equalTo(expectedLastPartition));
}
复制代码

注意,这里返回的划分结果不是源列表的视图,所以源列表的改动不会影响划分的子列表。

我们还可以使用分隔符来划分列表:

@Test
public void SplittingBySeparator() {
    List<Integer> intList = Lists.newArrayList(1, 2, 3, 0, 4, 5, 6, 0, 7, 8);
 
    int[] indexes = 
      Stream.of(IntStream.of(-1), IntStream.range(0, intList.size())
      .filter(i -> intList.get(i) == 0), IntStream.of(intList.size()))
      .flatMapToInt(s -> s).toArray();
    List<List<Integer>> subSets = 
      IntStream.range(0, indexes.length - 1)
               .mapToObj(i -> intList.subList(indexes[i] + 1, indexes[i + 1]))
               .collect(Collectors.toList());
 
    List<Integer> lastPartition = subSets.get(2);
    List<Integer> expectedLastPartition = Lists.<Integer> newArrayList(7, 8);
    assertThat(subSets.size(), equalTo(3));
    assertThat(lastPartition, equalTo(expectedLastPartition));
}
复制代码

这个例子中我们使用“0”作为分隔符,首先我们获取所有“0”元素的索引位置,然后将列表在这些位置上进行划分。

删除列表中所有的null

JDK

Java集合框架提供了一个简单的方法来删除列表中的所有null元素,使用while循环:

@Test
public void RemovingNullsWithPlainJava() {
    List<Integer> list = Lists.newArrayList(null, 1, null);
    while (list.remove(null));
 
    assertThat(list, hasSize(1));
}
复制代码

也可以使用稍微简单一些的方法:

@Test
public void RemovingNullsWithPlainJavaAlternative() {
    List<Integer> list = Lists.newArrayList(null, 1, null);
    list.removeAll(Collections.singleton(null));
 
    assertThat(list, hasSize(1));
}
复制代码

注意这两种方式都会修改源列表。

Google Guava

我们也可以使用Guava来删除列表中的null元素,通过谓词可以有一个更函数式的实现:

@Test
public void RemovingNullsWithGuavaV1() {
    List<Integer> list = Lists.newArrayList(null, 1, null);
    Iterables.removeIf(list, Predicates.isNull());
 
    assertThat(list, hasSize(1));
}
复制代码

此外,如果不想修改源列表,Guava中也可以创建一个新的过滤后的列表:

@Test
public void RemovingNullsWithGuavaV2() {
    List<Integer> list = Lists.newArrayList(null, 1, null, 2, 3);
    List<Integer> listWithoutNulls = Lists.newArrayList(
      Iterables.filter(list, Predicates.notNull()));
 
    assertThat(listWithoutNulls, hasSize(3));
}
复制代码
Apache Commons Collections

接下来看一下使用Apache Commons Collections库的实现方法,使用函数式风格:

@Test
public void RemovingNullsWithCommonsCollections() {
    List<Integer> list = Lists.newArrayList(null, 1, 2, null, 3, null);
    CollectionUtils.filter(list, PredicateUtils.notNullPredicate());
 
    assertThat(list, hasSize(3));
}
复制代码

注意这个方法也会修改源列表

使用Lambda表达式(Java 8)

最后,看一下使用Lambda表达式对列表进行过滤的方法,过滤操作可以并行或者串行执行:

@Test
public void FilteringParallel() {
    List<Integer> list = Lists.newArrayList(null, 1, 2, null, 3, null);
    List<Integer> listWithoutNulls = list.parallelStream()
      .filter(Objects::nonNull)
      .collect(Collectors.toList());
}
 
@Test
public void FilteringSerial() {
    List<Integer> list = Lists.newArrayList(null, 1, 2, null, 3, null);
    List<Integer> listWithoutNulls = list.stream()
      .filter(Objects::nonNull)
      .collect(Collectors.toList());
}
 
public void RemovingNullsWithRemoveIf() {
    List<Integer> listWithoutNulls = Lists.newArrayList(null, 1, 2, null, 3, null);
    listWithoutNulls.removeIf(Objects::isNull);
 
    assertThat(listWithoutNulls, hasSize(3));
}
复制代码

删除列表中的重复项

JDK

使用标准的Java集合框架删除列表中的重复元素,可以通过Set集合完成:

public void RemovingDuplicatesWithPlainJava() {
    List<Integer> listWithDuplicates = Lists.newArrayList(0, 1, 2, 3, 0, 0);
    List<Integer> listWithoutDuplicates = new ArrayList<>(
      new HashSet<>(listWithDuplicates));
 
    assertThat(listWithoutDuplicates, hasSize(4));
}
复制代码

可以看出,该方法不会更改源列表中的数据。

Guava

使用Guava的方法是类似的:

public void RemovingDuplicatesWithGuava() {
    List<Integer> listWithDuplicates = Lists.newArrayList(0, 1, 2, 3, 0, 0);
    List<Integer> listWithoutDuplicates = Lists.newArrayList(Sets.newHashSet(listWithDuplicates));
 
    assertThat(listWithoutDuplicates, hasSize(4));
}
复制代码

同样,源列表的数据没有被修改。

Lambda表达式

还有一个方法是使用Lambda表达式,我们可以使用*Stream API中的*distinct()方法,该方法会返回一个由不同元素组成的数据流,其中通过*equals()*判断元素是否相同。

public void RemovingDuplicatesWithJava8() {
    List<Integer> listWithDuplicates = Lists.newArrayList(1, 1, 2, 2, 3, 3);
    List<Integer> listWithoutDuplicates = listWithDuplicates.stream()
     .distinct()
     .collect(Collectors.toList());
}
复制代码

查找列表中的某个元素

为了方便后续说明,我们首先定义一个Customer POJO对象:

public class Customer{
    private int id;
    private String name;
    
    //getter/setter 代码省略
    
    @Override
    public boolean equals(Object obj){
        if(obj == null){
            return null;
        }
        if(this == obj){
            return true;
        }
        if(getClass() != obj.getClass()){
            return false;
        }
        final Customer other = (Customer)obj;
        if(id == other.getId()){
            return true;
        }
        return false;
    }

    @Override
    public int hashCode(){
        return this.id;
    }
}
复制代码

再定义一个存储Customer对象的ArrayList

List<Customer> customers = new ArrayList<>();
customers.add(new Customer(1, "zhao"));
customers.add(new Customer(2, "qian"));
customers.add(new Customer(3, "sun"));
复制代码

注意,我们在Customer类中重写了equals()hashCode(),在我们的实现中,具有相同id的两个Customer对象就认为是相等的。

Java API

Java本身提供了多种方法来查找列表中的某个元素。List提供了名为***contains()***的方法:

boolean contains(Object element)
复制代码

如果列表中包含某个元素,该方法会返回true,否则返回false。所以只需要检查列表中是否存在某个元素,我们可以这样做:

Customer qian = new Customer(2, "qian");
if (customers.contains(qian)) {
    // ...
}
复制代码

indexOf()也是一个用于查找元素的方法:

int indexOf(Object element)
复制代码

该方法会返回给定列表中某元素第一次出现的位置索引,如果列表中不包含该元素则返回*-1*。因此,如果该方法返回的不是-1,就说明列表中不包含该元素:

if(customers.indexOf(qian) != -1) {
    // ...
}
复制代码

这个方法否主要优点就是可以获得元素在列表中的位置

如果我们需要基于某个字段来搜索列表中的元素呢?比如通过名字来指定某个用户。对于这一类基于字段的搜索,我们就需要使用迭代。

对列表进行遍历的最典型的方法就是使用循环,在每个迭代中,将列表中的当前元素与我们搜索的元素进行对比,确认是否匹配:

public Customer findUsingEnhancedForLoop(String name, List<Customer> customers) {
    for (Customer customer : customers) {
        if (customer.getName().equals(name)) {
            return customer;
        }
    }
    return null;
}
复制代码

这里的name对应的就是我们所搜索的对象的名称,该方法会返回匹配该字段的第一个Customer对象,如果没有匹配项则返回null

同样可以使用Iterator来遍历列表中的项:

public Customer findUsingIterator(
  String name, List<Customer> customers) {
    Iterator<Customer> iterator = customers.iterator();
    while (iterator.hasNext()) {
        Customer customer = iterator.next();
        if (customer.getName().equals(name)) {
            return customer;
        }
    }
    return null;
}
复制代码
Stream API

要使用Stream API在给定列表中查询匹配条件的元素时,可以通过以下步骤完成:

  • 在列表上执行stream()
  • 调用包含特定筛选条件的*filter()*方法
  • 调用findAny()方法,该方法会将**匹配筛选条件的第一个元素封装为Optional**并返回
Customer james = customers.stream()
  .filter(customer -> "zhao".equals(customer.getName()))
  .findAny()
  .orElse(null);
复制代码

如果Optional为空的话,则默认返回null

Guava

Guava中的做法与stream操作类似:

Customer sun = Iterables.tryFind(customers,
  new Predicate<Customer>() {
      public boolean apply(Customer customer) {
          return "sun".equals(customer.getName());
      }
  }).orNull();
复制代码

如果列表或者过滤条件为空,Guava会抛出一个NullPointerException

Apache Commons Collections

常规操作:

Customer zhang = IterableUtils.find(customers,
  new Predicate<Customer>() {
      public boolean evaluate(Customer customer) {
          return "zhang".equals(customer.getName());
      }
  });
复制代码

但是,这里有两点区别:

  • 如果传入列表为null,Apache Commons会返回null
  • 该方法不像Guava中的tryFind()一样可以设置默认值

列表复制

复制列表的一个简单方法就是使用以集合为参数的构造器:

List<T> copy = new ArrayList<>(list);
复制代码

由于该方法是复制引用而非克隆对象,因此对任何元素的修改都会影响两个列表。因此,该方法适合于复制不可变对象:

List<Integer> copy = new ArrayList<>(list);
复制代码

Integer是一个不可变类,在创建实例时会指定取值,并且对应取值不可更改。因此,整数引用可以由多个列表和线程共享,任何人都无法更改它的值。

还有一个复制元素的方案就是使用addAll()方法:

List<Integer> copy = new ArrayList<>();
copy.addAll(list);
复制代码

同样,这里两个列表中的元素引用了相同的对象。如果我们在复制列表的同时,另外的线程中对其进行修改,则会抛出***ConcurrentAccessException***。可以通过以下方法解决该问题:

  • 使用可并发访问的集合
  • 在迭代集合时对其加锁
  • 寻找一种避免复制源集合的方法

我们刚才所使用的方法就不满足线程安全要求。如果使用第一种方式解决该问题,我们可以使用CopyOnWhiteArrayList,之前说过针对该列表的所有改动都会先创建底层数据的新副本。如果需要对集合进行加锁,可以使用ReentrantReadWriteLock(可重入读写锁)锁原语将读写操作有序化。

此外,Collections类中也提供了工具方法copy()对集合进行复制,该方法需要传入一个源列表和一个目标列表,且目标列表的长度至少要与源列表相等。该方法在复制元素的时候会保留元素在源列表中的索引位置。

List<Integer> source = Arrays.asList(1,2,3);
List<Integer> dest = Arrays.asList(4,5,6);
Collections.copy(dest, source); // [1,2,3]
复制代码

在这个示例程序中,dest列表原来的项都会被覆盖,因为两个列表的长度相等。如果目标列表的长度大于源列表:

List<Integer> source = Arrays.asList(1, 2, 3);
List<Integer> dest = Arrays.asList(5, 6, 7, 8, 9, 10);
Collections.copy(dest, source); // [1,2,3,8,9,10]
复制代码

只有前三个元素被覆盖,其它元素保留。

Java 8中的Stream API也提供了函数式的实现方法:

List<String> copy = list.stream().collect(Collectors.toList());
复制代码

该方法的优势在于可以同时对元素进行过滤和跳过,如跳过第一个元素:

List<String> copy = list.stream()
  .skip(1)
  .collect(Collectors.toList());
复制代码

也可以对元素的某些字段进行过滤比较:

List<String> copy = list.stream()
  .filter(s -> s.length() > 10)
  .collect(Collectors.toList());
复制代码

也可以通过以下方法对null值进行处理:

List<Flower> flowers = Optional.ofNullable(list)
  .map(List::stream).orElseGet(Stream::empty)
  .skip(1)
  .collect(Collectors.toList());
复制代码

向ArrayList中加入多个元素

首先,我们可以使用addAll()方法向一个ArrayList中增加多个元素,该方法接收一个集合作为入参:

List<Integer> anotherList = Arrays.asList(5, 12, 9, 3, 15, 88);
list.addAll(anotherList);
复制代码

这里需要注意的是,添加到列表list中的新元素与anotherList中的元素将引用同样的对象。因此,对这些元素的修改会影响两个列表。

除此之外还可以使用集合工具类Collections,该类中只包含对集合进行操作或者返回值为集合的静态方法。addAll()就是其中之一,该方法需要传入目标列表,需要传入列表的项可以单独指定也可以以数组形式传入。下面是示例:

List<Integer> list = new ArrayList<>();
// 单独指定元素项
Collections.addAll(list, 1, 2, 3, 4, 5);
// 使用数组作为入参
Integer[] otherList = new Integer[] {1, 2, 3, 4, 5};
Collections.addAll(list, otherList);
复制代码

与上一个方法类似,两个列表中的内容指向相同的对象

Java 8中的Stream模块提供了新的方法:

List<Integer> source = ...;
List<Integer> target = ...;
 
source.stream().forEachOrdered(target::add);
复制代码

这种方式的主要优势在于可以对元素进行跳过或者过滤。下面是跳过第一个元素的示例:

source.stream().skip(1).forEachOrdered(target::add);
复制代码

也可以对元素进行过滤:

source.stream().filter(i -> i > 10).forEachOrdered(target::add);
复制代码

如果希望处理方法具有空安全属性,可以使用Optional

Optional.ofNullable(source).ifPresent(target::addAll)
复制代码

这里通过调用addAll()方法将source中的元素加入到了target中。

删除列表中等于特定值的所有元素

在Java中,从List中删除某个元素可以直接使用List.remove()方法,但是,想要有效地删除所有的特定值要困难一些,下面整理了一些可供使用的方式。

使用while循环

既然我们已经知道如何删除单个元素,那么就可以在循环中重复删除该操作:

void removeAll(List<Integer> list, int element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}
复制代码

但是,很不巧这个程序没法完成任务:

// given
List<Integer> list = list(1, 2, 3);
int valueToRemove = 1;
 
// when
assertThatThrownBy(() -> removeAll(list, valueToRemove))
  .isInstanceOf(IndexOutOfBoundsException.class);
复制代码

问题在于代码第三行,我们调用了**List.remove(int)**,但是该方法会将传入的参数作为待删除元素的索引值,而不是我们需要删除的元素的值。

在上面的测试用例中,我们一直调用list.remove(1),但是我们想要删除的元素的索引为0,而调用该方法会将被删除元素后面的所有元素都向前移动。因此,最后会删掉除第一个元素以外的所有元素,当列表中只有一个元素时,索引1就是非法值,所以程序会抛出异常。

要注意,只有当我们传入原始类型(byte,short,char或int)参数时,才会遇到该问题。因为编译器在查找匹配的重载方法时首先会对参数进行原始类型扩展。

我们只需将传入值得类型改为Integer即可:

void removeAll(List<Integer> list, Integer element) {
    while (list.contains(element)) {
        list.remove(element);
    }
}
复制代码

这样代码就可以正确工作了:

// given
List<Integer> list = list(1, 2, 3);
int valueToRemove = 1;
 
// when
removeAll(list, valueToRemove);
 
// then
assertThat(list).isEqualTo(list(2, 3));
复制代码

由于List.contains()List.remove()两个方法都需要先找到元素的第一个索引值,因此该方法中存在不必要的元素遍历。如果在第一次遍历的时候将索引值保存起来,代码效率会更高:

void removeAll(List<Integer> list, Integer element) {
    int index;
    while ((index = list.indexOf(element)) >= 0) {
        list.remove(index);
    }
}
复制代码

虽然这个方法代码简洁,但是性能仍然不足:因为我们并没有对程序过程进行跟踪,List.remove()方法只能先找到给定值的第一个索引然后再对其进行删除。

循环使用remove方法

**List.remove(E element)**方法还有一个特点:该方法有一个boolean类型的 返回值,如果列表结构因为该操作发生变化,则返回true,表明列表中包含该元素。要注意,List.remove(int index)方法无返回值,因为如果传入的索引参数有效,则列表总会删除该元素,否则就会抛出IndexOutOfBoundsException

因此我们可以这样来删除元素:

void removeAll(List<Integer> list, int element) {
    while (list.remove(element));
}
复制代码

该方法也可以正常工作:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;
 
// when
removeAll(list, valueToRemove);
 
// then
assertThat(list).isEqualTo(list(2, 3));
复制代码

尽管代码更简洁,但是仍然存在前面所说的性能问题。

使用for循环

如果使用for循环,我们就可以跟踪列表遍历的位置,如果遍历元素是待删除元素则可以直接删除:

void removeAll(List<Integer> list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        }
    }
}
复制代码

看起来是可以完成任务的:

// given
List<Integer> list = list(1, 2, 3);
int valueToRemove = 1;
 
// when
removeAll(list, valueToRemove);
 
// then
assertThat(list).isEqualTo(list(2, 3));
复制代码

但是,如果我们换一个不同的输入列表,结果就是错误的:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;
 
// when
removeAll(list, valueToRemove);
 
// then
assertThat(list).isEqualTo(list(1, 2, 3));
复制代码

我们来一步步分析一下代码的工作过程:

  • i = 0
    • 第3行中elementlist.get(i)值都为1,因此Java进入if代码块
    • 删除索引0的元素
    • 列表中的值变为[1,2,3]
  • i = 1
    • *list.get(i)*返回2,因为在列表中删除一个元素之后,它后面的元素会依次前移

因此如果输入列表中有两个连续的元素都是待删除的元素,就会引发该问题。要解决这个问题,我们就需要在删除元素时保持循环变量。

如当删除元素时,将循环变量减一:

void removeAll(List<Integer> list, int element) {
    for (int i = 0; i < list.size(); i++) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
            i--;
        }
    }
}
复制代码

或者是,仅在不删除元素时对循环变量加一:

void removeAll(List<Integer> list, int element) {
    for (int i = 0; i < list.size();) {
        if (Objects.equals(element, list.get(i))) {
            list.remove(i);
        } else {
            i++;
        }
    }
}
复制代码

这里第二种方法的for条件中去掉了i++

这两种方法都是有效的,而且这个实现乍一看应该是完全正确的,但是仍然存在一些性能问题:

  • ArrayList中删除元素,会移动该元素后面的所有元素
  • LinkedList中通过索引访问元素,意味着需要从表头遍历至索引位置。
使用for-each循环

Java 5开始可以使用for-each循环对列表进行遍历,我们不妨使用它来删除元素:

void removeAll(List<Integer> list, int element) {
    for (Integer number : list) {
        if (Objects.equals(number, element)) {
            list.remove(number);
        }
    }
}
复制代码

我们在这里使用了Integer作为循环变量类型,因此避免了空指针异常。同时,我们调用的是List.remove(E element)方法,传入参数为我们要删除的值而不是元素的索引。但是很不幸,这个方法是错误的:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;
 
// when
assertThatThrownBy(() -> removeWithForEachLoop(list, valueToRemove))
  .isInstanceOf(ConcurrentModificationException.class);
复制代码

因为for-each循环本质上是使用Iterator对元素进行遍历,但是当我们修改列表时,Iterator会进入一个不稳定状态,从而抛出ConcurrentModificationException异常

我们学到了一点:当通过for-each循环访问列表元素时,不要对列表进行修改。

使用Iterator

我们可以直接使用Iterator直接对列表进行遍历及修改:

void removeAll(List<Integer> list, int element) {
    for (Iterator<Integer> i = list.iterator(); i.hasNext();) {
        Integer number = i.next();
        if (Objects.equals(number, element)) {
            i.remove();
        }
    }
}
复制代码

在上面的代码中,Iterator会对列表状态进行跟踪,因为列表的修改是由迭代器完成的。因此代码可以实现要求:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;
 
// when
removeAll(list, valueToRemove);
 
// then
assertThat(list).isEqualTo(list(2, 3));
复制代码

因为每一个List类都会提供自己的迭代器实现,我们可以认为,迭代器会以最有效的方式实现元素迭代。但是,在ArrayList时仍然会存在大量的元素移动。

收集元素

截至目前,我们都是通过删除不需要的元素来修改列表结构。其实我们也可以新创建一个新列表,将需要保留的元素纳入其中:

List<Integer> removeAll(List<Integer> list, int element) {
    List<Integer> remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }
    return remainingElements;
}
复制代码

这里方法的返回值变为了List对象,所以调用方式也需要做一些调整:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;
 
// when
List<Integer> result = removeAll(list, valueToRemove);
 
// then
assertThat(result).isEqualTo(list(2, 3));
复制代码

这个方法中可以使用for-each循环的原因在于,我们并没有对当前正在迭代的列表进行修改操作。同时,由于没有删除操作,也就不需要对元素进行移动,因此该方法在使用ArrayList的时候效率较高。

这个实现方法与之前的相比,主要有两点不同:

  • 没有对源列表进行修改,而是返回了一个新的列表对象
  • 方法的实现决定了最终返回的List的具体实现,可能会与源列表不同

当然,我们也可以做一些调整,跟前面的方法保持一致。也就是说,将源列表元素清除,再将保留的元素填入其中:

void removeAll(List<Integer> list, int element) {
    List<Integer> remainingElements = new ArrayList<>();
    for (Integer number : list) {
        if (!Objects.equals(number, element)) {
            remainingElements.add(number);
        }
    }
 
    list.clear();
    list.addAll(remainingElements);
}
复制代码

这里我们不对源列表进行修改,也无需通过索引访问元素及移动元素,只有两个地方可能涉及数组的再分配:调用List.clear()List.addAll()方法时。

使用Stream API

Java 8中引入的lambda表达式以及stream API,可以通过非常简洁的代码解决该问题:

List<Integer> removeAll(List<Integer> list, int element) {
    return list.stream()
      .filter(e -> !Objects.equals(e, element))
      .collect(Collectors.toList());
}
复制代码

有了lambda表达式及函数式接口之后,Java 8中也引入了一下扩展API。List中的removeIf()方法,就可以实现我们所讨论的功能。

该方法需要传入一个谓词函数,该函数对于需要删除的元素返回true。这个地方与之前有所区别,前面的判断都是对于需要保留的元素返回true

void removeAll(List<Integer> list, int element) {
    list.removeIf(n -> Objects.equals(n, element));
}
复制代码

测试看出,该方法是有效的:

// given
List<Integer> list = list(1, 1, 2, 3);
int valueToRemove = 1;
 
// when
removeAll(list, valueToRemove);
 
// then
assertThat(list).isEqualTo(list(2, 3));
复制代码

该方案中的代码是最为简洁地,同时由于其中使用的方法removeIf()是由List本身实现的,我们可以放心地认为其性能是最优的。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值