JDK集合类Collection解析--ArrayList

成员变量

  1. private static final int DEFAULT_CAPACITY
    默认初始容量 10
  2. private static final Object[] EMPTY_ELEMENTDATA
    所有实例共享的空数组
  3. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    调用无参构造器时共享的赋值对象,虽然都是空数组,但与EMPTY_ELEMENTDATA不同,详见无参构造器
  4. private int size
    数组长度。与容量不同,表示数组中元素个数
  5. transient Object[] elementData
    真正的存储结构

构造方法

  1. int initialCapacity

    // 传入数组的容量,与size不同,表示数组最大存储元素个数
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
        	// 这里指向公用空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
        	//传入容量小于0抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    
  2. 无参

    public ArrayList() {
    	// 调用无参构造并不按默认容量DEFAULT_CAPACITY初始化
    	// 而是先指向公用对象DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    	// 在add中为elementData申请内存
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
  3. Collection<? extends E>

    // 传入其他集合类
    public ArrayList(Collection<? extends E> c) {
    	/*
    	 * 这里elementData初始化调用传入参数的toArray方法
    	 * 此方法存在于Collection接口中,各类实现不同
    	 * (但应该都是深拷贝,等我看完了Collection回来填坑)
    	 */
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
        	// 验证c的toArray返回类型是否为Object
        	// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 如果传入为其他类型的空数组
            // 仍指向EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    

常用方法

  1. 扩容

    private Object[] grow(int minCapacity) {
    	// 将原有数组按新的容量复制
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }
    // 计算扩容容量
    private int newCapacity(int minCapacity) {
        int oldCapacity = elementData.length;
        // 新的容量为旧容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
        	/*
        	 * 如果是无参构造,在此时确定数组的容量
        	 * 取传入值和默认值10的最大值作为容量
        	 * (好像一般都是10)
        	 */
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // 容量溢出,抛出异常(不是传负值)
                throw new OutOfMemoryError();
            return minCapacity;
        }
        // 如果超过了最大容量Integer.MAX_VALUE - 8,调整为最大容量
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
    

    此外还有个问题,为啥是扩容到原来的1.5倍呢?因为一次性扩容太大(例如2.5倍)可能会浪费更多的内存(1.5倍最多浪费33%,而2.5被最多会浪费60%,3.5倍则会浪费71%……)。但是一次性扩容太小,需要多次对数组重新分配内存,对性能消耗比较严重。所以1.5倍刚刚好,既能满足性能需求,也不会造成很大的内存消耗。

  2. 删除元素
    ArrayList中对remove有两个重载,分别是
    a. public E remove(int index)
    删除指定位置的元素,并返回删除元素的值。实现如下

    public E remove(int index) {
        Objects.checkIndex(index, size);
        final Object[] es = elementData;
    
        @SuppressWarnings("unchecked") E oldValue = (E) es[index];
        fastRemove(es, index);
    
        return oldValue;
    }
    

    其中的删除操作依靠内部的私有函数fastRemove实现,具体如下

    private void fastRemove(Object[] es, int i) {
        modCount++;
        final int newSize;
        if ((newSize = size - 1) > i)
            System.arraycopy(es, i + 1, es, i, newSize - i);
        es[size = newSize] = null;
    }
    

    根据删除位置的不同,又分为两种情况:
    当待删除元素为数组末尾时,直接将末尾位置置为null
    当待删除元素在其他位置时,将该元素后续向前复制,再将末尾置null

    b. public boolean remove(Object o)
    删除数组中第一次出现的o元素

    public boolean remove(Object o) {
        final Object[] es = elementData;
        final int size = this.size;
        int i = 0;
        /*
         * label语法,SE7之后加入的新特性
         * 类似于goto指针(不全等,java里没有goto,goto有害!)
         * break label 跳出语句块,常用于跳出多重循环
         * 参考链接:https://stackoverflow.com/questions/28381212/how-to-use-labels-in-java-code
         */
        found: {
            if (o == null) {
                for (; i < size; i++)
                    if (es[i] == null)
                        break found;
            } else {
                for (; i < size; i++)
                    if (o.equals(es[i]))
                        break found;
            }
            return false;
        }
        fastRemove(es, i);
        return true;
    }
    
  3. 添加
    最终调用私有方法

    private void add(E e, Object[] elementData, int s) {
    	// 判断是否扩容,扩容原理见上
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    

    注意添加的是浅拷贝(引用),而非深拷贝

        public static void main(String[] args) {
        List<List<Integer>> arr = new ArrayList<>(5);
        ArrayList<Integer> tmp = new ArrayList(5);
        for (int i = 0; i < 5; i++) {
            tmp.add(i);
            arr.add(tmp);
        }
        show(arr);
        /*
        0 1 2 3 4 
        0 1 2 3 4 
        0 1 2 3 4 
        0 1 2 3 4 
        0 1 2 3 4 
         */
        System.out.println();
        for (int i = 0; i < 5; i++) {
            tmp.set(i,i+5);
        }
        show(arr);
        /*
        5 6 7 8 9 
        5 6 7 8 9 
        5 6 7 8 9 
        5 6 7 8 9 
        5 6 7 8 9 
         */
    }
    // 打印函数
    private static void show(List<? extends List> arr) {
        int len = arr.size();
        for (int i = 0; i < len; i++) {
            for(var ele : arr.get(0)){
                System.out.printf("%d ",ele);
            }
            System.out.println();
        }
    }
    

    此外ArrayList不检测添加元素是否为null,允许其加入

  4. 删除数组

    public void clear() {
    	/* 对数组的结构性改动(添加/删除)都会影响modCount
    	 * 上面的源码之所以没有显示,是因为我没把完整顺序截全
    	 * modCount用于iterator的fast-fail机制
    	 */
        modCount++;
        final Object[] es = elementData;
        /*
         * 删除整个数组并不是简单的把elementData重新指向EMPTY_ELEMENTDATA
         * 而是将原有数组元素置为null,以便GC回收
         */
        for (int to = size, i = size = 0; i < to; i++)
            es[i] = null;
    }
    
  5. hashCode and equals

    public int hashCode() {
        int expectedModCount = modCount;
        // 计算[0,size)范围的数组hash值
        int hash = hashCodeRange(0, size);
        // fast-fail检测线程安全
        checkForComodification(expectedModCount);
        return hash;
    }
    
    int hashCodeRange(int from, int to) {
        final Object[] es = elementData;
        if (to > es.length) {
            throw new ConcurrentModificationException();
        }
        /*
         * 以下是hashcode计算过程
         * 将数组中的每一个数加权相乘的出hash值
         */
        int hashCode = 1;
        for (int i = from; i < to; i++) {
            Object e = es[i];
            /* 
             * 若为null,该位置不对hash产生贡献
             * 注意:按位加权,所以
             * null 1 null null
             * 和
             * 1 null null null
             * 的hash值不同
             */
            hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode());
        }
        return hashCode;
    }
    
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
    
        if (!(o instanceof List)) {
            return false;
        }
    
        final int expectedModCount = modCount;
        // ArrayList can be subclassed and given arbitrary behavior, but we can
        // still deal with the common case where o is ArrayList precisely
        // 上面说的挺好
        boolean equal = (o.getClass() == ArrayList.class)
            ? equalsArrayList((ArrayList<?>) o)
            : equalsRange((List<?>) o, 0, size);
    
        checkForComodification(expectedModCount);
        return equal;
    }
    

线程安全

显然ArrayList并不是线程安全的,以add为例

 public static void main(String[] args) {
    ArrayList<Integer> store = new ArrayList<>();
    int len = 10;
    Thread[] t = new Thread[len];
    for (int i = 0; i < len; i++) {
        t[i] = new Thread(new Runnable() {
            @Override
            public void run() {
                Random rand = new Random();
                for (int j = 0; j < 500; j++) {
                    store.add(rand.nextInt()%100);
                }
            }
        });
        t[i].start();
    }
    // 要等所有线程都执行完再去查数组长度
    // 否则得到的不是线程最终操作完的结果
    for (int i = 0; i < len; i++) {
        try {
            t[i].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    System.out.println(store.size());
}

最后结果居然才3843,堪称在线吃数。在多线程下,ArrayList不能保证线程安全,一在于多线程同时扩容造成丢失,二在于多线程同时添加造成覆盖,所以每一个线程操控后的store可能是完全不同的。我们重写run看一下store的hashcode

 public void run() {
    Random rand = new Random();
    for (int j = 0; j < 500; j++) {
        store.add(rand.nextInt()%100);
    }
    System.out.println(Thread.currentThread().getName()+" "+store.hashCode());
}

执行结果如下

Exception in thread "Thread-6" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList.checkForComodification(ArrayList.java:604)
	at java.base/java.util.ArrayList.hashCode(ArrayList.java:614)
	at com.java.learning.language$1.run(language.java:20)
	at java.base/java.lang.Thread.run(Thread.java:835)
Thread-2 709127055
Thread-0 -746687074
Thread-3 2143175265
Thread-9 1059229658
Thread-7 -1392478734
Thread-8 -612096406
Thread-5 -941813659
Thread-1 -334317182
Thread-4 450111088

可以看到每个线程操作后store五花八门,甚至还有个fast-fail抛了个ConcurrentModificationException。
补充:除了上次两个问题以外,还可能抛出ArrayIndexOutOfBoundsException,由于扩容不同步,当一个线程扩容,另一个线程写入时可能存在的情况
删除道理相似,这里不再演示了

那么如何保证ArrayList的线程安全呢?
请大声喊出那个线程安全容器的名字:Vector !
使用java.util.Collections.SynchronizedList它能把所有 List 接口的实现类转换成线程安全的List,比 Vector 有更好的扩展性和兼容性实际上粗暴的一匹

/*
 * SynchronizedList是Collections里一内部类
 * 这货怎么保证线程安全呢?
 * 很简单,增删改查全sync
 * 那这样行么?行,怎么不行每次就一个线程能操作还能不行
 * 但行归行,这不是最优的
 * 如果好几个线程同时读取某个数,这需要加锁控制同步么?
 * No,不用,仅读取不会造成任何线程安全问题,加锁反而降低吞吐量
 */
public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
    synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}

如果针对读多写少的场景下,可以采用CopyOnWriteArrayList

允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它。
如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。

这里重点介绍写方法,因为读没啥好说的。。。

/*
 * CopyOnWriteArrayList可以体现读写分离的思想:读和写分别在不同容器
 * 写时需要同步控制,避免并发扩容
 * 而读不需要同步控制,因为读取的容器永远不会添加
 * (因为都是添加完了才给你看的,没添加完你也看不了)
 */
 public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        /*
         * CopyOnWriteArrayList没有特殊的扩容机制
         * 来一个扩一次,然后内部存储结构引用指向新的内存
         */
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

但是这里需要我们思考以下,不加同步的读是否真的安全,SynchronizedList虽然慢了点,但是绝对的线程安全,CopyOnWriteArrayList真的能实现线程安全么?废话肯定能,不能留着干嘛
虽然多线程切换具有随意性,但是仍可以分为如下三种情况

  1. 写发生在读之前
    这感觉就像句废话,因为没啥用。。。
  2. 写读并发
    由于读并没有同步控制,所以现在这两个操作是同时执行的
    考虑最坏的情况,在读到一半换人写了,回来读的时候发现容器换了。如果不是读取新加入的元素,那么之前元素并没有改变,依然正确。
    而且是先完成新数组构建,再修改引用,因此也不用担心数组长度大于实际值 从而导致ArrayIndexOutOfBoundsException的问题
  3. 写在读之后
    同1

删除

/*
 * COW发生结构性变化时,都会加整体lock
 * 仅允许一个线程对其改动
 * 同样也是改动到新数组,然后指向新数组
 */
public E remove(int index) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        E oldValue = elementAt(es, index);
        int numMoved = len - index - 1;
        Object[] newElements;
        if (numMoved == 0)
            newElements = Arrays.copyOf(es, len - 1);
        else {
            newElements = new Object[len - 1];
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index + 1, newElements, index,
                             numMoved);
        }
        setArray(newElements);
        return oldValue;
    }
}

参考资料

1. 面试官问线程安全的List,看完再也不怕了! - Java技术栈的文章 - 知乎
2. 翻看 Java10 里面的 ArrayList 源码,remove 方法里面有个 found: {} 这是什么意思呢?
3. 第六章 Java数据结构和算法 之 容器类(一)- 李一恩的文章 - CSDN
4. Top 40 Java collection interview questions and answers

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值