精通ArrayList,关于ArrayList你想知道的一切

发布时间:2021-01-12 发布网站:编程之家
编程之家收集整理的这篇文章主要介绍了精通ArrayList,关于ArrayList你想知道的一切编程之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

精通ArrayList,关于ArrayList你想知道的一切

ArrayList 数据结构 扩容 序列化 线程安全


前言

在做Java开发中,ArrayList是最常用的数据结构之一,我们用它来存储一个数据列表。初始化一个ArrayList对象之后,我们可以使用它提供的诸多的方法:插入,指定位置插入,批量插入,获取,删除,非空判断,存量获取等。

虽然我们都熟练使用,但是否有过这样的疑问:ArrayList是怎么保存我add()进去的数据的呢?当我new 一个ArrayList对象的时候,他有多大容量?我初始化了一个容量为10的ArrayList,却能插入11个元素,它是怎么扩容的呢?……下面将会从ArrayList源码来看这些问题。

ArrayList 内部结构,和常用方法实现

ArrayList是基于数组存储。打开ArrayList源码发现其中有个变量表明:

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData;

变量的注释是说,这个数组是用来存储ArrayList元素的,数组的长度即是ArrayList的容量。一个空ArrayList中的elementData是一个空数组,当第一次添加数据的时候,容量会扩充到DEFAULT_CAPACITY(也就是10)。

实例化方法

ArrayList有两个实例化方法,也称构造函数。无参实例化方法代码如下:

	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};    
	/**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

这个实例化方法很简单,其实就是给存储元素的数组elementData赋值——一个空的Object数组。

有参的实例化方法如下:

private static final Object[] EMPTY_ELEMENTDATA = {};    
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

方法接收参数initialCapacity做为初始化elementData的长度,如果这个数小于0抛异常,如果等于0 结果和无参构造函数一样。

添加元素 add()方法

添加方法有两个,一个是普通插入,一个是指定位置插入。普通方法代码如下:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

方法第一行是和容量相关的,下面详细分析。这里主要看第二行,添加元素其实就是将目标元素e放入数组elementData的size下标处。同时让size加1。下面在看指定位置插入:

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++;
    }

方法接收两个参数:index--插入位置,element--目标元素。第一行检查插入位置是否合理(0 < index <size),第二行统一是容量方面的。我们需要注意第三行,调用System.arraycopy方法来做“元素移位”,位移后再赋值elementData[index] = element。

假设list里边存储了A,B,C,D,E5个字母,现在调用add(3,"F"),将F插入。则调用System.arraycopy位移后示意图如下:

A B C D E
A B C D E

然后执行elementData[3]=“F”;整体过程

原始 A B C D E
位移 A B C D E
插入 A B C F D E

get()方法

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

get方法第一行检查index是否合法,例如你肯定不能get(-1)。然后取出elementData数组中的index下标处的元素。

移除元素

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData,index+1,numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

remove方法移除elementData中index处的元素,并将这个元素返回。numMoved为需要发生位移的元素的个数。然后调用位移。然后移除最后一个元素。

怎么扩容的

上面我们看到add()方法中有个ensureCapacityInternal()方法,这个方法实际上完成了扩容操作。扩容操作分为两部分,1、确定最小容量的值(为了插入当前元素,容量所要达到的值)

这个最小容量值就是minCapacity变量。如果当前elementData数组为空,minCapacity=10。否则minCapacity=size+1;

如果minCapacity小于10,则取10。如果minCapacity大于elementData的长度,则调用grow()方法扩容。

2、调用grow()方法,grow()方法如下:

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size,so this is a win:
        elementData = Arrays.copyOf(elementData,newCapacity);
    }

这个方法里确定elementData数组的新容量,并调用Arrays.copyOf完成扩容。确定新容量基于这三个数值:

  • 最小容量(size+1) minCapacity
  • 当前容量的1.5倍 elementData.lengt 1.5倍
  • 允许的最大容量

当调用new ArrayList();初始化的时候,elementData为空。第一次调用add()方法的时候,扩容至10。添加第11个元素的时候就需要扩容了,扩容后的值是15。10+(10>>1)=15。当添加第16个元素的时候,扩容至15+(15>>1)=22。

所以,可以粗略的理解成每次需要扩容时会扩大至原来的1.5倍,最大不超过Integer.MAX_VALUE。

序列化的问题

前面我们提到,ArrayList是基于数组的,它存储数据就是放在其数组类型成员变量上,也就是这个:

	transient Object[] elementData; 

这个变量是用transient修饰的。transient关键字的作用是这么定义的:

如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。

莫非数组元素不参与序列化?那我辛辛苦苦add添加的数据岂不是没了。——然而,实际的情况不是这样。

ArrayList实现了writeObject()和readObject()方法,相当于定制了序列化和反序列化。尽管elementData变量是用transient修饰的。但是实际上elementData中的元素在序列化的时候被写入了。方法如下:

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count,and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

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

为什么要这么做呢?我们前面提到,ArrayList是动态扩容的,所以,当一个arrayList具有10个容量的时候,实际上可能只存放了一个元素。即size=1,而elementData.length=10。这个时候序列化elementData干啥呢,后面都是空值。

线程安全问题

ArrayList是线程不安全的。所以,多线程环境下容易出错。下面例子中启动20个线程,每个线程向共享的ArrayList中插入20个元素,最终输出ArrayList的长度。如果ArrayList是线程安全的,那么最终的结果应该是200.

public class Test {

    public static void main(String[] args)throws Exception {
        ArrayList<Integer> list = new ArrayList<>();
        final CyclicBarrier cb=new CyclicBarrier(20);
        final CountDownLatch latch=new CountDownLatch(20);
        for(int i=0;i<20;i++){
            Thread t1 = new Thread(()->{
                try{
                    long cur = System.nanoTime();
                    Thread.sleep(100);
                    System.out.println(cur+"准备好了");
                    cb.await();
                    for(int j=0;j<20;j++){
                        list.add(j);
                    }
                    System.out.println(cur+"执行完了");
                    latch.countDown();
                }catch (InterruptedException |BrokenBarrierException e){
                    e.printStackTrace();
                }
            });
            t1.start();
        }
        latch.await();
        System.out.println("数组大小:"+list.size());
    }
}

我随便执行一次,得到如下结果:

33927850979931准备好了
33927850899613准备好了
33927850816171准备好了
33927850686322准备好了
33927850595294准备好了
33927850751470准备好了
33927851168680准备好了
33927851239628准备好了
33927851821046准备好了
33927851959373准备好了
33927851351182准备好了
33927851887532准备好了
33927851070067准备好了
33927852038799准备好了
33927851433286准备好了
33927852370336准备好了
33927852448424准备好了
33927852126703准备好了
33927852215500准备好了
33927852281986准备好了
33927852281986执行完了
33927850979931执行完了
33927850899613执行完了
33927850816171执行完了
33927850686322执行完了
33927850595294执行完了
33927850751470执行完了
33927851168680执行完了
33927851239628执行完了
33927851821046执行完了
33927852370336执行完了
33927851433286执行完了
33927852038799执行完了
33927851959373执行完了
33927851070067执行完了
33927851887532执行完了
33927852215500执行完了
33927851351182执行完了
33927852126703执行完了
33927852448424执行完了
数组大小:397

和明显结果不对,再执行几次,还会发现,报数组越界。所以,ArrayList是线程不安全的。

总结

以上是编程之家为你收集整理的精通ArrayList,关于ArrayList你想知道的一切全部内容,希望文章能够帮你解决精通ArrayList,关于ArrayList你想知道的一切所遇到的程序开发问题。

如果觉得编程之家网站内容还不错,欢迎将编程之家网站推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您喜欢交流学习经验,点击链接加入编程之家官方QQ群:1065694478