Java常用集合使用

java集合主要是指java.util包下面的Collection接口和Map接口,包含了常用的数据结构,主要有4个部分:List列表、Set集合、Map映射、工具类(Arrays和Collections)

1 集合类图

java集合类图

2 List

2.1 Vector

  • a 线程安全*
  • b 性能低*

2.2 ArrayList

*a ArrayList内部是通过数组实现的,它允许对元素进行快速随机访问。缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,也就是将数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动,代价比较高。所以ArrayList适合随机查找和遍历,不适合插入和删除。

*b 非线程安全。

LinkedList

  • a LinkedList实现了List和Deque接口,因此是一个双向链表, 可以当作堆栈、队列和双向队列使用。当数据量很大或者操作很频繁的情况下,添加和删除元素时具有比ArrayList更好的性能,因为ArrayList要移动数据 。但在元素的查询和修改方面要弱于ArrayList,因为LinkedList要移动指针。LinkedList类每个结点用内部类Node表示,通过first和last引用分别指向链表的第一个和最后一个元素,当链表为空时,first和last都为NULL值。

  • b LinkedList是非线程安全的。

  • c 方法

    1. boolean add(E e) :在链表尾部添加一个元素,如果成功,返回true,否则返回false。
    2. void addFirst(E e) :在链表头部插入一个元素。
    3. addLast(E e) :在链表尾部添加一个元素。
    4. void add(int index, E element) :在指定位置插入一个元素。
    5. boolean remove(Object o) :从当前链表中移除指定的元素。
    6. E remove(int index) :从当前链表中移除指定位置的元素。
    7. E removeFirst() :从当前链表中移除第一个元素。
    8. E removeLast() :从当前链表中移除最后一个元素。
    9. E remove() :从当前链表中移除第一个元素,作用等同removeLast()。

3 Set

3.1 TreeSet, LinkedHashSet, HashSet 的区别

  1. 都是非线程安全的;
  2. TreeSet的主要功能用于排序,内部结构是一个树结构(红黑树),元素是有序的,添加、删除操作时间复杂度为O(log(n)),存放的对象所属的类必须实现Comparable接口;
  3. LinkedHashSet的主要功能用于保证FIFO即有序的集合(先进先出);
  4. HashSet使用哈希表实现的,并发度是16,负载因子0.75,元素是无序的。添加、删除操作时间复杂度都是O(1);
  5. HashSet插入数据最快,其次LinkHashSet,最慢的是TreeSet,因为TreeSet内部要实现排序;
  6. HashSet不保证有序,LinkHashSet保证FIFO即按插入顺序排序,TreeSet安内部实现排序,也可以自定义排序规则;
  7. HashSet和LinkHashSet允许存在null数据,TreeSet中插入null数据时会报NPE(null无法排序);

4 Queue

Queue表示队列,实现了一个先进先出(FIFO:First In First Out)的有序表。常用方法:

  1. boolean add(E)/ boolean offer(E):添加元素到队尾;
  2. E remove()/ E poll():获取队首元素并从队列中删除;
  3. E element()/ E peek():获取队首元素,但并不从队列中删除。
    添加、删除和获取队列元素有两个方法(有容量限制的队列),这是因为在添加或获取元素失败时,这两个方法的行为是不同的:
操作 抛异常 返回true/null
添加元素到队尾 add(E e) boolean offer(E e)
取队首元素并删除 E remove() E poll()
取队首元素但不删除 E element() E peek()

注意:避免将null添加到队列(有容量限制的队列)中,否则poll()方法返回null时,无法确定是取到了null元素还是队列为空。

4.1 ArrayBlockingQueue

  1. ArrayBlockingQueue是一个线程安全的阻塞式的有界队列,有界意味着,它不能够存储无限多数量的对象。所以在创建 ArrayBlockingQueue 时,必须要给它指定一个队列的大小。底层以数组的形式保存数据。
    默认情况下,不保证顺序。但是,如果将构造方法中的fair参数设置为true的话,此时构造的队列以FIFO顺序访问。

  2. ArrayBlockingQueue中比较重要的方法:

  • add(E e):把元素e添加加到 BlockingQueue,如果BlockingQueue的容量可以容纳该元素,则返回 true,否则报IllegalStateException异常;
  • offer(E e):将元素e加到BlockingQueue,如果BlockingQueue可以容纳的话,则返回 true,否则返回 false ;
  • put(E e):把元素e加到 BlockingQueue 里,如果 BlockQueue 没有空间,则调用此方法的线程被阻塞,直到 BlockingQueue里面有空间再继续后续操作;
    使用的时候需要注意,防止队列满了之后继续添加对象造成阻塞。
  • poll(timeout):获取队列里排在首位的对象,如果队列没有元素,则等timeout参数规定的时间,取不到时返回 null ;
  • take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,则调用此方法的线程被阻塞,直到BlockingQueue有新的对象被加入为止 ;
  • remove():删除元素,需要将删除元素后的元素统一迁移一个单位,并且在操作过程中会获得锁,对性能有影响,因此不应轻易执行remove操作
  1. ArrayBlockingQueue 使用场景。
  • 可保证先进先出(FIFO)队列,队列头是最先进队的元素,队列尾是最后进队的元素;
  • 有界队列,即初始化时指定的容量,就是队列最大的容量,不会出现扩容,容量满,则阻塞 进队操作,容量空,则阻塞出队操作;
  • 队列不支持null元素

4.2 LinkedBlockingQueue

  1. 一个阻塞式的有界队列,保存元素的是一个链表,队列默认的最大长度为Integer.MAX_VALUE。其内部有一个Node的内部类,其中有一个成员变量 Node next。就这样形成了一个链表的结构,要获取下一个元素,只要调用next就可以了。

  2. LinkedBlockingQueue VS ArrayBlockingQueue

  • LinkedBlockingQueue是基于链表实现的,初始化是可以不用指定队列大小(默认是Integer.MAX_VALUE);ArrayBlockingQueue是基于数组实现的初始化时必须指定队列大小;
  • LinkedBlockingQueue在put操作都会生成新的Node对象,take操作Node对象在某一时间会被GC,可能会影响GC性能;ArrayBlockingQueue是固定的数组长度循环使用,不会出现对象的产生与回收;
  • LinkedBlockingQueue是基于链表的形式,在执行remove操作时,不用移动其他数据; ArrayBlockingQueue是基于数组,在remove时需要移动数据,影响性能;
  • LinkedBlockingQueue使用两个锁将put操作与take操作分开;ArrayBlockingQueue使用一个锁来控制,在高并发高吞吐的情况下,LinkedBlockingQueue的性能较好。
  • 都不允许null元素

Deque

  1. 读音: deck

  2. 父接口: Collection<E>, Iterable<E>, Queue<E>; 子接口: BlockingDeque<E>;实现类: ArrayDeque, ConcurrentLinkedDeque, LinkedBlockingDeque, LinkedList

  3. Deque允许两头都进,两头都出,这种队列叫双端队列(Double Ended Queue),提供了如下功能:

  • 既可以添加元素到队尾,也可以添加元素到队首;
  • 既可以从队首获取元素,又可以从队尾获取元素。
  1. Deque VS Queue
操作 Queue Deque
添加元素到队尾 add(E e) / offer(E e) addLast(E e) / offerLast(E e)
取队首元素并删除 E remove() / E poll() E removeFirst() / E pollFirst()
取队首元素但不删除 E element() / E peek() E getFirst() / E peekFirst()
添加元素到队首 addFirst(E e) / offerFirst(E e)
取队尾元素并删除 E removeLast() / E pollLast()
取队尾元素但不删除 E getLast() / E peekLast()

5 Deque 的实现类主要分为两种场景:

  1. 非并发场景
  • LinkedList 大小可变的链表双端队列,允许元素为 null
  • ArrayDeque 大小可变的数组双端队列,不允许 null
    并发场景
  1. LinkedBlockingDeque 如果队列为空时,获取操作将会阻塞,直到有元素添加。

6 Map类图

map类图

6.1 TreeMap, LinkedHashMap, HashMap 的区别

  1. LinkedHashMap是继承于HashMap,拥有HashMap所有特性,比HashMap多了双向链表,存取数据还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。
  2. LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入);
  3. LinkedHashMap是线程不安全的;
  4. HashMap无序,非线程安全;
  5. TreeMap的用法主要是排序, key必须实现Comparable接口,默认的排序为升序,如果要改变其排序需要自己实现一个Comparator。

LinkedHashMap默认按照插入顺序的,也就是插入的是什么顺序,读出来的就是什么顺序。同时也提供了按照访问顺序的构造方法,方法参数是accessOrder,需设置为true。

Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
        linkedHashMap.put("key1", "value1");
        linkedHashMap.put("key2", "value2");
        linkedHashMap.put("key3", "value3");
        Set<Map.Entry<String, String>> set = linkedHashMap.entrySet();
        Iterator<Map.Entry<String, String>> iterator = set.iterator();

        System.out.println("首次遍历");
        while(iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println("key: " + key + ",value: " + value);
        }

        System.out.println(linkedHashMap.get("key1"));

        Set<Map.Entry<String, String>> set2 = linkedHashMap.entrySet();
        Iterator<Map.Entry<String, String>> iterator2 = set2.iterator();

        System.out.println("再次遍历");
        while(iterator2.hasNext()) {
            Map.Entry<String, String> entry = iterator2.next();
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println("key: " + key + ",value: " + value);
        }
    }

// 首次遍历
// key: key1,value: value1
// key: key2,value: value2
// key: key3,value: value3

// value1

// 再次遍历
// key: key2,value: value2
// key: key3,value: value3
// key: key1,value: value1

6.2 EnumMap

HashMap是一种通过对key计算hashCode(),直接定位到value所在的内部数组的索引。
EnumMap是给枚举使用的,key为枚举,它在内部以数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),效率最高。

public V put(K key, V value) {
        typeCheck(key);

        int index = key.ordinal();
        Object oldValue = vals[index];
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
}
public V get(Object key) {
    return (isValidKey(key) ? unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
}

7 Arrays使用

7.1 避免使用基本数据类型数组转换为列表

public static void main(String[] args) {
    int[] ints = {1,2,3,4};
    List list = Arrays.asList(ints);
    System.out.println("size of list:" + list.size());
}
size of list:1

Arrays.asList(T...a)源码:

/**
     * Returns a fixed-size list backed by the specified array.  (Changes to
     * the returned list "write through" to the array.)  This method acts
     * as bridge between array-based and collection-based APIs, in
     * combination with {@link Collection#toArray}.  The returned list is
     * serializable and implements {@link RandomAccess}.
     *
     * <p>This method also provides a convenient way to create a fixed-size
     * list initialized to contain several elements:
     * <pre>
     *     List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
     * </pre>
     *
     * @param <T> the class of the objects in the array
     * @param a the array by which the list will be backed
     * @return a list view of the specified array
     */
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

asList方法的参数接受的是一个泛型的变长参数,但java中8种基本数据类型(byte,short,int,long,float,double,char,boolean)是无法泛型化的,也就是说8个基本类型是无法作为asList的参数的, 要想作为泛型参数就必须使用其所对应的包装类型。

既然要使用包装类型,那为什么上述demo没有报错呢??
因为该实例是将int类型的数组当做其参数,而在Java中数组是一个对象,它是可以泛型化的,所以该例子没有报错。上面的例子是将整个int类型的数组当做泛型参数,那么经过asList转换就只有一个int 的列表了。

public static void main(String[] args) {
    int[] ints = {1,2,3,4};
    List list = Arrays.asList(ints);
    System.out.println("type of List:" + list.get(0).getClass());
    System.out.println("list.get(0) == ints:" + list.get(0).equals(ints));
    Integer[] ints2 = {1,2,3,4};
    List list2 = Arrays.asList(ints);
    System.out.println("size of list2:" + list2.size());
    System.out.println("type of list2.get(0):" + list2.get(0).getClass());
    System.out.println("list2.get(0) == ints2[0]:" + list2.get(0).equals(ints2[0]));
}
type of list :class [I
list.get(0) == ints:true
size of list2:4
type of list2.get(0) :class java.lang.Integer
list2.get(0) == ints2[0]:true

7.2 asList方法返回的列表不可变

public static void main(String[] args) {
    Integer[] ints = {1,2,3,4};
    List list = Arrays.asList(ints);
    list.add(5);
}
java.lang.UnsupportedOperationException
    at java.util.AbstractList.add(AbstractList.java:148)
    at java.util.AbstractList.add(AbstractList.java:108)
private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private static final long serialVersionUID = -2764017481108945198L;
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
...
}
image.png

通过源码可以看出,asList返回的结果并不是java.util.ArrayList, 而是Arrays的内部类,该类的add、remove等改变结果的方法从AbstractList父类继承过来,但是这些方法直接抛出UnsupportedOperationException异常,因此并没有list的基本变长特性。该内部list是一个长度不可变的列表,传入参数的数组大小是多少,其返回的列表就大小就是多少。

7.3 常用方法

1 toString()

int[] array = new int[]{1, 2, 3};
System.out.println(Arrays.toString(array)); // [1, 2, 3]

int[][] deepArray = new int[][]{{1, 3},{2, 4}};
System.out.println(Arrays.toString(deepArray)); // [[I@47c62251, [I@3e6fa38a]


// 多维数组时,需要使用deepToString转换
System.out.println(Arrays.deepToString(deepArray)); // [[1, 3], [2, 4]]

2 fill填充

int[] array = new int[5];
Arrays.fill(array, 2);
System.out.println(Arrays.toString(array)); //[2, 2, 2, 2, 2]

array = new int[5];
Arrays.fill(array, 1, 4, 2); //部分填充, 后三个参数分别表示开始索引(包含),结束索引(不包含),填充值
System.out.println(Arrays.toString(array)); //[0, 2, 2, 2, 0]

3 sort

int[] array = new int[]{3, 10, 4, 0, 2};
Arrays.sort(array); // 串行排序
System.out.println(Arrays.toString(array)); //[0, 2, 3, 4, 10]

array = new int[]{3, 10, 4, 0, 2};
Arrays.parallelSort(array); // 并行排序
System.out.println(Arrays.toString(array)); // [0, 2, 3, 4, 10]

array = new int[]{3, 10, 4, 0, 2};
Arrays.sort(array, 0, 4); //部分排序,后两个个参数分别表示开始索引(包含),结束索引(不包含)
System.out.println(Arrays.toString(array)); // [0, 3, 4, 10, 2]

4 binarySearch: 二分查询

在调用该方法之前,必须先调用 Arrays.sort() 方法进行排序,如果数组没有排序,那么结果是不确定的,此外如果数组中包含多个指定元素,则无法保证将找到哪个元素

当搜索元素是数组元素时,返回该元素的索引值;当搜索元素不是数组元素时,返回 - (索引值 + 1)

Integer[] data = {1, 3, 5, 7};
Arrays.sort(data); // 搜索元素是数组元素,返回该元素索引值
System.out.println(Arrays.binarySearch(data, 1)); // 0

Integer[] data = {1, 3, 5, 7};
Arrays.sort(data); // 搜索元素不是数组元素,且小于数组中的最小值,此时程序会把数组看作 {0, 1, 3, 5, 7},0的索引值为0,则搜索0时返回 -(0 + 1) = -1
System.out.println(Arrays.binarySearch(data, 0)); 

Integer[] data = {1, 3, 5, 7};
Arrays.sort(data); // 搜索元素不是数组元素,且大于数组中的最大值,此时程序会把数组看作 {1, 3, 5, 7, 9},9的索引值为4,则搜索8时返回 -(4 + 1) = -5
System.out.println(Arrays.binarySearch(data, 9)); 

Integer[] data = {1, 3, 5, 7};
Arrays.sort(data); // 搜索元素不是数组元素,但在数组范围内,此时程序会把数组看作 {1, 2, 3, 5, 7},2的索引值为1,则搜索2时返回 -(1 + 1) = -2
System.out.println(Arrays.binarySearch(data, 2)); 

5 数组复制

int[] array = new int[]{3, 10, 4, 0, 2};
int[] arrayCopy = Arrays.copyOf(array, 3); // 第二个参数为新数组的长度
out.println(Arrays.toString(arrayCopy)); // [3, 10, 4]

arrayCopy = Arrays.copyOf(array, 7); 
out.println(Arrays.toString(arrayCopy)); //[3, 10, 4, 0, 2, 0, 0], 多出的长度补0

arrayCopy = Arrays.copyOfRange(array, 1, 4); // 后两个个参数分别表示开始索引(包含),结束索引(不包含)
out.println(Arrays.toString(arrayCopy)); // [10, 4, 0]

6 对数组元素采用指定的方法计算

Integer[] data = {1, 2, 3, 4};
Arrays.setAll(data, i -> data[i] * 2);  // i为索引值
System.out.println(Arrays.toString(data)); // [2, 4, 6, 8]

Integer[] data = {1, 2, 3, 4};
Arrays.parallelSetAll(data, i -> data[i] * 2); // i为索引值
System.out.println(Arrays.toString(data)); // [2, 4, 6, 8]

Integer[] data = {2, 3, 4, 5};
// 第一个元素2不变,将其与第二个元素3一起作为参数x, y传入,得到乘积6,作为数组新的第二个元素
// 再将6和第三个元素4一起作为参数x, y传入,得到乘积24,作为数组新的第三个元素,以此类推
Arrays.parallelPrefix(data, 0, 3, (x, y) -> x * y);
System.out.println(Arrays.toString(data)); // [2, 6, 24, 5]

8 subList注意事项

8.1 ArrayList.subList只是原始ArrayList的一个视图

官网文档:
Returns a view of the portion of this list between the specified

  • {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. (If
  • {@code fromIndex} and {@code toIndex} are equal, the returned list is
  • empty.) The returned list is backed by this list, so non-structural
  • changes in the returned list are reflected in this list, and vice-versa.
  • The returned list supports all of the optional list operations.
    我们先看一个例子
public static void main(String[] args) {
    List<Integer> list1 = new ArrayList<>();
    list1.add(1);
    list1.add(2);

    //通过构造函数新建一个包含list1的列表list2
    List<Integer> list2 = new ArrayList<>(list1);

    //通过subList生成一个与list1一样的列表list3
    List<Integer> list3 = list1.subList(0, list1.size());

    //修改list3
    list3.add(3);

    System.out.println("list1 == list2:" + list1.equals(list2));   // true ??
    System.out.println("list1 == list3:" + list1.equals(list3));   // false ??
}

答案是这样的:

list1 == list2:false
list1 == list3:true

我们看一下subList的实现:

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}


private class SubList extends AbstractList<E> implements RandomAccess {
        private final AbstractList<E> parent;
        private final int parentOffset;
        private final int offset;
        int size;

        // 该SubLsit是ArrayList的内部类,它与ArrayList一样,都是继承AbstractList和实现RandomAccess接口。同时也提供了get、set、add、remove等list常用的方法
        SubList(AbstractList<E> parent,
                int offset, int fromIndex, int toIndex) {

            //parent就是在前边传递过来的list,也就是说this.parent就是原始list的引用
            this.parent = parent;
            this.parentOffset = fromIndex;
            this.offset = offset + fromIndex;
            this.size = toIndex - fromIndex;
            this.modCount = ArrayList.this.modCount;
        }

        public E set(int index, E e) {
            rangeCheck(index);
            checkForComodification();
            E oldValue = ArrayList.this.elementData(offset + index);
            ArrayList.this.elementData[offset + index] = e;
            return oldValue;
        }

        public E get(int index) {
            rangeCheck(index);
            checkForComodification();
            return ArrayList.this.elementData(offset + index);
        }

        public int size() {
            checkForComodification();
            return this.size;
        }

        public void add(int index, E e) {
            rangeCheckForAdd(index);
            checkForComodification();

            // 因为this.parent就是原始list的引用,所以add方法影响的是原始的list
            parent.add(parentOffset + index, e);
            this.modCount = parent.modCount;
            this.size++;
        }

        public E remove(int index) {
            rangeCheck(index);
            checkForComodification();
            E result = parent.remove(parentOffset + index);
            this.modCount = parent.modCount;
            this.size--;
            return result;
        }
        ....
}

通过源码,自然解释了list1 == list2:false;list1 == list3:true这个结果。
总结: subList生成的子列表只是原列表的一个视图而已,如果我们操作子列表它产生的作用都会表现在原列表上面。

8.2 避免操作生成subList的原列表

public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);

        List<Integer> list2 = list1.subList(0, list1.size());

        list1.add(3);

        System.out.println(list1.size());
        System.out.println(list2.size());
}

执行结果:

3
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
    at java.util.ArrayList$SubList.size(ArrayList.java:1040)
public int size() {
    checkForComodification();
    return this.size;
}

private void checkForComodification() {
    // 当原列表的modCount与this.modCount不相等时就会抛出ConcurrentModificationException
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
}

通过源码,我们知道SubList.modCount 在new的过程中继承了原列表modCount,只有在修改该SubList时才会修改该值,而在该实例中我们是操作原列表,原列表的modCount不会反应在子列表的modCount上,所以抛出该异常。

9 Collections使用

9.1 转化为数组

ArrayList<String> list = new ArrayList<>();
...
Object[] a1 = list.toArray();//将List对象转换成Object数组
String[] a2 = list.toArray(new String[list.size()]);
String[] a2 = list.toArray(new String[0]); //当参数数组长度为0时,会重新分配一个数组

9.2 排序

  • reverse(List):反转List中元素的顺序;
  • shuffle(List):对List集合元素进行随机排序;
  • sort(List):根据元素的自然顺序对指定List集合元素按升序排序;
  • sort(List, Comparator):根据指定的Conparator产生的顺序对List集合进行排序;
  • swap(List, int i, int j):将指定List集合中的i和j处的元素进行交换;

9.3 替换所有的元素:fill

Collections.fill(list, "a");

9.4 查找元素在集合中出现的次数:frequency

int n = Collections.frequency(list, 6)

9.5 遍历删除

// 初始化集合
ArrayList<String> list=new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
 
//下面是错误的遍历删除方式,抛出异常:java.util.ConcurrentModificationException
for(String s:list){
   list.remove(s);
}

Iterator<String> it = list.iterator();
while(it.hasNext()){
    String x = it.next();    
    list.remove(x);
}

// 正确的遍历方式
for(Iterator<String> it=list.iterator();it.hasNext();){
    it.next();
    it.remove();
}
System.out.println("list = " + list.size());

// 倒叙方式可以正常删除
for(int i=list.size()-1; i>=0; i--){
    list.remove(i);
}
 

// 下面方式不会报异常,但只能遍历删除部分元素,因为当一个元素被删除时,列表的大小缩小并且下标变化,所以想要在一个循环中用下标删除多个元素的时候,不会生效
for(int i=0;i<list.size();i++){
    list.remove(i);
}

为什么ArrayList在使用迭代器迭代元素时不能使用List.remove()删元素,而是使用Iterator.remove()删元素。

    public Iterator<E> iterator() {
        return new Itr();
    }

    /**
     * An optimized version of AbstractList.Itr
     */
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        ...
    }

   public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
   }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

当ArrayList调用iterator()方法,初始化iterator时,将list的modCount值赋给expectedModCount,如果expectedModCount和modCount的值一直相等,iterator.next()是不会抛并发修改异常的, 继续看List.remove(Object)源码:

public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

可以看出list.remove()被调用时,方法中modCount自增了,而expectedModCount是没有修改的,因此,在下一次iterator调用next()时,将抛出ConcurrentModificationException。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容