• Java集合源码分析(二)——ArrayList


    简介

    ArrayList 是一个数组列表,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。

    其继承关系如下:
    在这里插入图片描述

    源码分析

    这里的代码是Java1.8的。

    public class ArrayList<E> 
    		extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
    

    实现接口

    • List
    • RandomAccess
    • Cloneable
    • java.io.Serializable

    父类

    • AbstractList

    字段

    • elementData:就是Object类型的数组。存储着ArrayList中真正的数据。其初始容量是10,之后会根据数据操作动态改变,具体发生在ensureCapacity()函数中。
    • size:表示列表中实际的元素数量,像一个指针,一直指着elementData中末尾元素的下一位。
    • serialVersionUID :意思是该类的版本序列号。(常量)
    • DEFAULT_CAPACITY:默认的初始数组大小。
    • EMPTY_ELEMENTDATA:在用初始容量值来初始化ArrayList的时候,如果设置大小为0,那么elementData就会指向这个空数组。
    • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:没有指定初始化大小的时候,elementData就会指向这个空数组。
    • MAX_ARRAY_SIZE:列表的最大容量,就是int型的最大值。
        private static final long serialVersionUID = 8683452581122892189L;
        private static final int DEFAULT_CAPACITY = 10;
        private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
        transient Object[] elementData;
        private int size;
        private static final int MAX_ARRAY_SIZE = 2147483639;
    

    这里的transient是什么意思?

    1. transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。
    2. 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
    3. 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。也可以认为在将持久化的对象反序列化后,被transient修饰的变量将按照普通类成员变量一样被初始化。

    方法

    分析几个比较重要的,能够体现其设计精髓的API。

    1.扩容

    	// 这个函数内部没有被使用到,应该是给外部调用的进行扩容的
        public void ensureCapacity(int minCapacity) {
            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                // any size if not default element table
                ? 0
                // larger than default for default empty table. It's already
                // supposed to be at default size.
                : DEFAULT_CAPACITY;
    
            if (minCapacity > minExpand) {
                ensureExplicitCapacity(minCapacity);
            }
        }
    	// 内部扩容基本都调用的这个函数
        private void ensureCapacityInternal(int minCapacity) {
        	// 看看是不是初次扩容,如果是就进行比较,如果需要扩容的数量小于10,那就直接扩到10
            if (this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                var1 = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            this.ensureExplicitCapacity(minCapacity);
        }
    	// 明确可能需要扩容
        private void ensureExplicitCapacity(int minCapacity) {
        	// 增加修改计数器
            ++this.modCount;
            // 看看现有的数组大小够不够
            if (minCapacity - this.elementData.length > 0) {
                this.grow(minCapacity);
            }
    
        }
        
    	// 真正实现扩容的函数
        private void grow(int minCapacity) {
            // overflow-conscious code
            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);
            // 新建一个数组,将老的数据再拷贝进去
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
        
        private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) 
            	// 溢出
                throw new OutOfMemoryError();
            // 控制最大值
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }
    

    扩容触发条件
    如果需求容量略大于现有容量,则进行扩容。

    扩容大小
    每次将新的容量提升为原来的1.5倍。

    扩容原理
    就重新构造一个新的更大的数组,然后通过数组拷贝工具函数将原来的数组的数据放进去。

    2.添加元素

        // 添加元素
        public boolean add(E e) {
        	// 看看是不是需要扩容
            ensureCapacityInternal(size + 1);  // modCount放在这个函数里面了
            // 在末尾添加元素
            elementData[size++] = e;
            return true;
        }
    

    实现原理:先查看数组是否需要扩容,然后再到数组末尾添加新的元素。

    3.元素查找

        // 正向查找,返回元素的索引值
        public int indexOf(Object o) {
            if (o == null) {
                for (int i = 0; i < size; i++)
                    if (elementData[i]==null)
                        return i;
            } else {
                for (int i = 0; i < size; i++)
                    if (o.equals(elementData[i]))
                        return i;
            }
            return -1;
        }
    

    实现原理:就是挨个匹配,找到了就返回索引,没找到就返回-1。需要注意的是,List中填入的元素类型一定要实现equals方法,不然没法进行比较。

    4.转化为数组

        // 返回ArrayList的Object数组
        public Object[] toArray() {
            return Arrays.copyOf(elementData, size);
        }
    
        // 返回ArrayList的模板数组。所谓模板数组,即可以将T设为任意的数据类型
        public <T> T[] toArray(T[] a) {
            // 若数组a的大小 < ArrayList的元素个数;
            // 则新建一个T[]数组,数组大小是“ArrayList的元素个数”,并将“ArrayList”全部拷贝到新数组中
            if (a.length < size)
                return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    
            // 若数组a的大小 >= ArrayList的元素个数;
            // 则将ArrayList的全部元素都拷贝到数组a中。
            System.arraycopy(elementData, 0, a, 0, size);
            if (a.length > size)
                a[size] = null;
            return a;
        }
    

    注意点:
    这里有两个方法可以转化为数组,通常我们使用第二个,因为第一个可能会丢出“java.lang.ClassCastException”异常,看源码,第一个直接把elementdata拷贝了就丢出来,返回的Object[]类型,而如果在外面对其进行类型转化,就会发生这个异常。因为Java出现向下转型可能会出现丢失,需要强制转换。

    模板数组拷贝实现原理:

    • 就是需要传入一个模板数组,数组元素的类型就是程序员想要的,就不要在外面进行转换。
    • 如果传入的数组大小大于列表中数据的长度,通过把elementData中的元素拷贝到模板数组中,然后再返回就能得到想要的数组。
    • 如果传入数组大小小于列表中数组的长度,就重新建一个数组返回。

    5.获取元素

    	// 将Object类型转化为列表的泛型
        E elementData(int index) {
            return (E) elementData[index];
        }
        // 获取index位置的元素值
        public E get(int index) {
            rangeCheck(index);
    
            return elementData(index);
        }
    

    实现原理
    首先先判断请求数据的索引是否合法,然后直接拿出来数组的元素然后转换,再丢出来。

    6.删除元素

        // 删除ArrayList指定位置的元素
        public E remove(int index) {
            RangeCheck(index);
    
            modCount++;
            E oldValue = (E) elementData[index];
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
            elementData[--size] = null; // Let gc do its work
    
            return oldValue;
        }
    

    实现原理
    将想要删除元素的位置之后的元素都往前移动一个单位,然后将最后的数组指向空。
    所以,ArrayList 删除中间元素的开销是很大的,如果该元素后面有很多元素,那么开销就是直线上升了。

    为什么要将最后的元素指向空呢?
    这样原来那个数组位置所指的实例就会失去引用,在之后的垃圾回收中就会被自动回收。

    7.添加集合

        // 从index位置开始,将集合c添加到ArrayList
        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;
        }
    

    实现原理:
    先把加进来的集合转化为数组,然后计算出需要插入的位置数量,插入位置后面的元素都向后移动该数量的步长,再将数据复制到相应的位置上。其实和插入一个数据的方法是差不多的。

    8.克隆

        // 克隆函数
        public Object clone() {
            try {
                ArrayList<?> v = (ArrayList<?>) super.clone();
                // 拷贝原来列表中的数组元素到新列表的数组中
                v.elementData = Arrays.copyOf(elementData, size);
                v.modCount = 0;
                return v;
            } catch (CloneNotSupportedException e) {
                // this shouldn't happen, since we are Cloneable
                throw new InternalError(e);
            }
        }
    

    实现原理
    首先调用父类的克隆接口函数,产生一个新的实例。再将现在的数组和尺寸都拷贝到新的实例中,再返回。

    9.构造方法

        public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
            	// 如果初始容量设置为0,就把elementData指向空
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
    
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    
        public ArrayList(Collection<? extends E> c) {
            elementData = c.toArray();
            if ((size = elementData.length) != 0) {
                // c.toArray might (incorrectly) not return Object[] (see 6260652)
                if (elementData.getClass() != Object[].class)
                    elementData = Arrays.copyOf(elementData, size, Object[].class);
            } else {
                // replace with empty array.
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }
    

    提供了三种构造方法,分别是不设定初始值,设定初始大小和设定初始的集合。

    从这里看出,如果ArrayList的初始容量没有设置,或者设置值为0,那么就会将elementData指向一个空数组,而1.6是直接新建了一个大小为10的数组,1.8很节省空间。

    和之前不同的是,前两个构造函数中没有初始化size,因为size是对象的成员变量,分配到堆上,可以不初始化,默认就为0.

    总结

    源码总结

    1. ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10
    2. 当ArrayList容量不足以容纳全部元素时,ArrayList会进行扩容:newCapacity = oldCapacity + (oldCapacity >> 1);”。
    3. ArrayList的克隆函数,即是将全部元素拷贝到一个数组中。
    4. ArrayList的中间插入和删除操作都需要将对应操作位置后的数据进行向后拷贝,所以,开销是不固定的,后面的元素越多,开销越大。
    5. ArrayList中数组保存数据,所以,读取中间的一个元素的随机访问速度很快。
    6. ArrayList中通过size字段保存其存储的元素数量,而实际占用的空间是elementData数组的大小,所以,除非执行了trimToSize()函数,将列表数据数量和数组元素数量保持同步,不然就会浪费后面的一段空间。
    7. ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。

    源码相关问题

    源码中这个modCount是做什么的?
    因为ArrayList和很多集合是线程不安全的,内部会通过modCount来记录线程对其的修改行为,add()、remove(),还是clear(),在有些情况下,修改行为的不可见是会出大问题,比如引用指向了其他的内存区域,这样就变成内存溢出攻击了。如果我们之前先将modCount数值保存下来,如果中间发现被其他线程修改了,那就直接抛出异常。
    尤其是迭代器,在它的源码中有:if (modCount != expectedModCount) throw new ConcurrentModificationException();
    所以,我们在有线程不安全的情况下,可以使用java.util.concurrent包下的集合,这些是线程安全的。

    System.arraycopy() 和 Arrays.copyOf()的区别?
    Arrays.copyOf()的底层还是对前者的调用。

    ArrayList的遍历方式

    • 迭代器遍历
    • 随机访问遍历
    • foreach遍历

    其中随机访问遍历的效率最高,时间短。foreach底层应该是使用了迭代器遍历,二者的开销差不多。但是迭代器访问也有好处,就是上面提到的,更安全。

  • 相关阅读:
    创业公司的经济适用架构师
    软件工程–从嗤之以鼻到视若法宝
    阿里云CDN+OSS完成图片加速
    听说你在为天天写业务代码而烦恼?
    从实践者的角度看软件架构的历史
    KVM虚拟化技术
    网络基础和 TCP、IP 协议
    分布式应用程序协调服务 ZooKeeper
    python 装饰器
    python 柯里化**
  • 原文地址:https://www.cnblogs.com/lippon/p/14117609.html
Copyright © 2020-2023  润新知