Collection集合


目录
  • Collection集合

    • 2.1、成员属性

    • 2.2、构造方法

    • 2.3 扩容机制

    • 2.4、重点方法

    • 2.5、fail-fast 机制

    • 2.3.1、无参构造进行扩容

    • 2.3.2、有参构造进行扩容

    • 2.3.3 、总结图

    • 2.4.1、add方法

    • 2.4.2、remove方法

    • 2.4.3、set方法

    • 2.4.4、查询方法

    • 2.4.5、遍历方法

    • 2.4.6、clear方法

    • 2.5.1、解决方式

    • 1、Collection集合概述

    • 2、ArrayList类


    1、Collection集合概述

    java中的集合类大概分为了单例和双列的。至于为什么不能有三列的或者是多列的,因为双列中已经满足了多例的使用方式。

    首先看下单列的集合体系图

    Collection集合类是一个父接口,里面定义了子类中一定要实现的方法。对于两个子接口:list和set接口来说,各有各的特点和优势。

    对于List集合来说,元素是可以重复的;对于set接口来说,规定元素是不可以有重复的。之后会通过源代码来进行分析。

    比较常用的方法:

    public boolean add(E e):  把给定的对象添加到当前集合中 
    public void clear() :清空集合中所有的元素。
    public boolean remove(E e): 把给定的对象在当前集合中删除。
    public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
    public boolean isEmpty(): 判断当前集合是否为空。
    public int size(): 返回集合中元素的个数。
    public Object[] toArray(): 把集合中的元素,存储到数组中

    直接去查看两个重要子类的实现方式,再次之前,首先查看下子类中的实现:

    2、ArrayList类

    2.1、成员属性

    	// 默认的初始化大小
        private static final int DEFAULT_CAPACITY = 10;
    
        // 空数组
        private static final Object[] EMPTY_ELEMENTDATA = {};
    
    	// 默认的空数组
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    	
    	// 在进行序列化的时候,如果不需要来进行序列化,那么可以在属性前面加上这个关键字
    	// 放元素的容器
        transient Object[] elementData; 
    
    	// 容器中元素的个数
        private int size;

    这里的两个空数组需要了解一下,这里是用来在构造函数中进行初始化的时候来进行使用的。

    2.2、构造方法

    直接查看常用的构造

    无参构造:

        public ArrayList() {
            // 指向默认空数组
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

    有参构造1:

        public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                // 创建一个Object类型的数组
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                // 如果是0,那么使用不是默认的空数组
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }

    有参构造2:

    将Collection集合类的子类转换成ArrayList类。注意这里的泛型,需要和现有的数据类型的保持一致。

        public ArrayList(Collection<? extends E> c) {
            // 1、首先转换成一个数组
            Object[] a = c.toArray();
            // 2、1判断数组长度
            if ((size = a.length) != 0) {
                // 3、判断原来的数据集合类型是否是ArrayList类型
                if (c.getClass() == ArrayList.class) {
                    // 3.1、如果是的话,那么就直接进行赋值即可
                    elementData = a;
                } else {
                    // 如果不是ArrayList数据类型的,那么需要来保持一致
                    elementData = Arrays.copyOf(a, size, Object[].class);
                }
             // 如果长度不为0,那么转换过来的肯定是一个空数组,直接赋值为空值即可
            } else {
                // replace with empty array.
                elementData = EMPTY_ELEMENTDATA;
            }
        }

    2.3 扩容机制

    扩容方式针对的是无参构造和有参构造中指定了容量的构造方式来进行操作的。

    2.3.1、无参构造进行扩容

    无参构造在创建新集合的时候,是没有指定容量的,那么此时此刻容量就是为0,也就是size是为0的,elementData指向的是一个空数组而已。

    只有在进行add的时候,才会真的来进行elementData的初始化操作。下面来看下

        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!! 增加操作次数,这里为错误机制提供了一种方式
            elementData[size++] = e;
            return true;
        }

    首先需要调用的是内部的方式ensureCapacityInternal,确保内部容量,size此时是为0的,那么size+1意味着要向其中来尝试添加一个元素。

    看下具体方法:

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    // 计算当前集合保存数据集合的容量。
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 是否保存集合的数组指向了一个空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 默认容量和传入进来的容量来进行比较,取出来最大值
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 不是空数组,那么就继续执行下面逻辑
        return minCapacity;
    }

    确保明确的容量方法:

        private void ensureExplicitCapacity(int minCapacity) {
            // 每一次操作,这里都会自增+1
            modCount++;
    
            // 如果尝试添加后的容量是大于当前当前容器的容量的,那么需要来进行扩容
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

    这里首先不需要来看扩容的方法,默认就是小于当前的容量。那么继续,看下面怎么添加的。

    小于当前数组容量的添加方法:

        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  
            // 首先将添加的元素放入到elementData[size]=e上,然后size自增,也就是游标向后移动一位
            elementData[size++] = e;
            return true;
        }

    返回添加为true。

    那么如果说添加的容量大于了集合类中的容器的容量,那么这里来执行对应的步骤:

        private void ensureExplicitCapacity(int minCapacity) {
            // 每一次操作,这里都会自增+1
            modCount++;
    
            // 如果尝试添加后的容量是大于当前当前容器的容量的,那么需要来进行扩容
            // 注意这里相等的时候,不需要来进行扩容
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

    那么看一下grow方法:

        private void grow(int minCapacity) {
            // 计算当前数组的长度
            int oldCapacity = elementData.length;
            // 这里体现出来了扩容机制是1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            // 如果扩容后依然小于最小值,那么依然是使用最小的
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            // 如果达到了最大值,那么依然是最小的那个值
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // 最终采用提供的扩容机制来进行实现。将最终的数组来进行返回
            // 在这里已经将原来的值都来进行了copy
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

    然后再在尾部来进行追加。

    注意:第一次由0到10,接下来的扩容才是真的1.5倍扩容。扩容创建一个数组,然后将原来数组中的值按照原来的顺序来进行排列,然后预留剩下的空数组容量交给后来需要添加的元素预留位置。

    2.3.2、有参构造进行扩容

    有参构造也需要走原来的逻辑。只不过有一点是有点差距的:

    // 计算当前集合保存数据集合的容量。
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 是否保存集合的数组指向了一个空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 默认容量和传入进来的容量来进行比较,取出来最大值
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 不是空数组,那么就继续执行下面逻辑
        return minCapacity;
    }

    这里执行的是直接返回的最小容量,因为elementData不再指向原来的空数组了。那么接下来走的就是原来的逻辑了。

    2.3.3 、总结图

    扩容方法是add方法引起的。最终将上述叙述总结一张图:

    2.4、重点方法

    一般来说,都是增删改查方法

    2.4.1、add方法

    add刚刚说了一个最原始的方式,还是在尾部来进行追加的。那么现在的添加方法是在数组中间来进行添加:

    add(int index, E element)

        public void add(int index, E element) {
            // 范围检查,超出范围,索引越界异常
            rangeCheckForAdd(index);
    		
            // 尝试添加判断剩余容量是否是足够的
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            // 开始进行拷贝,然后将添加位置和添加位置之后的元素每个向后移动一个位置
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            // 在原来的值上赋添加进来的值
            elementData[index] = element;
            size++;
        }

    还有另外两种添加方法,都可以看一下:

        public boolean addAll(Collection<? extends E> c) {
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
            return numNew != 0;
        }

    直接将一个集合中的元素添加到原数组的尾部:

    测试一下:

        @Test
        public void testAddAll1(){
            List<Integer> integers1 = new ArrayList<>();
            integers1.add(1);
            integers1.add(2);
            List<Integer> integers2 = new ArrayList<>();
            integers2.add(3);
            integers2.add(4);
            System.out.println(integers1);
            integers1.addAll(integers2);
            System.out.println(integers1);
        }

    查看控制台输出:

    [1, 2]
    [1, 2, 3, 4]

    再看另外一种,在指定位置上插入对应的集合

        public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
    
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
    
            int numMoved = size - index;
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                                 numMoved);
    
            System.arraycopy(a, 0, elementData, index, numNew);
            size += numNew;
            return numNew != 0;
        }

    测试类:

        @Test
        public void testAddAll2(){
            List<Integer> integers1 = new ArrayList<>();
            integers1.add(1);
            integers1.add(2);
            List<Integer> integers2 = new ArrayList<>();
            integers2.add(3);
            integers2.add(4);
            System.out.println(integers1);
            integers1.addAll(1,integers2);
            System.out.println(integers1);
        }

    查看控制台:

    [1, 2]
    [1, 3, 4, 2]

    2.4.2、remove方法

    删除掉指定下标的方法:

        public E remove(int index) {
            // 检查坐标是否大于等于当前size,因为坐标和当前索引相比是为1的
            rangeCheck(index);
    	   // 操作次数+1
            modCount++;
            // 将旧值先进行保存,然后需要进行返回
            E oldValue = elementData(index);
    	   // 计算需要将删除的坐标向前移动多少次	
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            // 将原来的的引用全部置空,GC回收
            elementData[--size] = null; // clear to let GC do its work
    	   // 返回旧值
            return oldValue;
        }

    再看一个:

        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++)
                    // 从这里可以看到使用的是equals方法,那么在删除的时候,需要使用到对象的重写的equals方法
                    if (o.equals(elementData[index])) {
                        // 和上面逻辑一致
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }

    可以看到如果删除成功,返回true;删除失败,说明没有这个值,无法来进行删除。

    2.4.3、set方法

    最近用到了这个方法,用来修改指定下表的元素。

        public E set(int index, E element) {
            // 检查坐标
            rangeCheck(index);
    	    // 拿到旧值
            E oldValue = elementData(index);
            // 然后将新值放到原来位置上
            elementData[index] = element;
            return oldValue;
        }

    2.4.4、查询方法

    这个一般来说也是常用的

        public E get(int index) {
            rangeCheck(index);
    
            return elementData(index);
        }

    2.4.5、遍历方法

    public void forEach(Consumer<? super E> action) {
        	// 首先判断操作
            Objects.requireNonNull(action);
        	// 不希望在遍历的时候进行其它操作
            final int expectedModCount = modCount;
            @SuppressWarnings("unchecked")
            final E[] elementData = (E[]) this.elementData;
            final int size = this.size;
        	// 不断的进行判空操作,然后消费每个数据
            for (int i=0; modCount == expectedModCount && i < size; i++) {
                action.accept(elementData[i]);
            }
            if (modCount != expectedModCount) {
                // 最终判断,如果不行,那么抛出异常
                throw new ConcurrentModificationException();
            }
        }

    2.4.6、clear方法

    这个方法有点意思的

        public void clear() {
            modCount++;
    
            // clear to let GC do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }

    可以看到这里是将原来的元素全部置空,然后size变为0.但是数组的长度是没有发生变化的。这里的长度指的是数组的长度,而不是有效元素的个数的长度。

    2.5、fail-fast 机制

    当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
    例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

    而上面我们可以不断的看到一个数值modCount,就是用这个来进行记录的。

    那么我们写个代码来实验一下:

    @Test
    public void testFastFail(){
        List<String> stringList = new ArrayList<>();
    
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 10000; i1++) {
                    stringList.add(Thread.currentThread().getName());
                }
            }).start();
        }
    
        try {
            Thread.sleep(1000);
            System.out.println(stringList.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    输出结果:

    16864

    从这里看出来,已经缺少值了,因为少添加了。那么这里就会出现着问题。

    那么如何才能体现出来对应的异常信息呢?可以看到查询里面都有这个异常,那么在添加的时候看一下:

        @Test
        public void testFastFail(){
            List<String> stringList = new ArrayList<>();
    
            for (int i = 0; i < 2; i++) {
                new Thread(()->{
                    for (int i1 = 0; i1 < 10; i1++) {
                        stringList.add(Thread.currentThread().getName());
                    }
                }).start();
            }
            stringList.forEach(System.out::println);
            try {
                Thread.sleep(1000);
                System.out.println(stringList.size());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    查看控制台输出:

    Thread-2
    
    java.util.ConcurrentModificationException
    	at java.util.ArrayList.forEach(ArrayList.java:1262)

    可以看得到在遍历的时候,检测出来

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

    期望操作的线程和当前的线程不一致,原本希望的是当前的线程来进行操作。但是有别的线程来进行入侵了,所以就会有这种结果的产生。

    而在上面分析的过程中,可以看到add、remove都会涉及到对modCount的值,这才是涉及到根本原因的地方。

    2.5.1、解决方式

    1、Collections提供的同步list机制

    List<String> arrList = Collections.synchronizedList(new ArrayList<>());

    2、CopyOnWriteArrayList

    List<String> list = new CopyOnWriteArrayList<>();

    3、Vector类

    List<String> arrList = new Vector<>();
    千里之行,始于足下。不积跬步,无以至千里

    更多文章请关注《万象专栏》