java框架集合List子接口之ArrayList源码剖析「建议收藏」

Java (76) 2023-06-03 08:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说java框架集合List子接口之ArrayList源码剖析「建议收藏」,希望能够帮助你!!!。

感兴趣的话大家可以关注一下公众号 : 猿人刘先生 , 欢迎大家一起学习 , 一起进步 , 一起来交流吧!

ArrayList

ArrayList实现了List接口 , 它是有序且可以重复的 , 允许存放所有所有元素 , 包括null , 除了实现List接口之外这个类还提供了一些方法来操作内部存储列表数组的大小 , 这个类大致相当于Vector , 只是它不是同步的 , 同时ArrayList还实现了RandomAccess, Cloneable, java.io.Serializable

RandomAccess

Random是随机的意思,Access是访问的意思,合起来就是随机访问的意思。RandomAccess是一个标记接口 , 用以标记实现的List集合具备快速随机访问的能力 , 那么什么是随机访问的能力呢?其实很简单,随机访问就是随机访问List中的任何一个元素。

所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了,这里的快速随机访问,那么就不是所有List集合都支持了

  • ArrayList基于数组实现,天然带下标 ,可以实现常量级的随机访问,复杂度为O(1)

  • LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n)

通过代码的实现也能看出来 , ArrayList实现了RandomAccess接口 , 而LinkedList没有实现

Cloneable

支持拷贝:实现Cloneable接口,重写clone方法、方法内容默认调用父类的clone方法。

浅拷贝(shallowCopy)

  • 如果是基础类型和String类型的变量拷贝之后是独立的,不会随着源变量变动而变 , 但是如果修改了任意一个变量的值 , 对另一个都是有影响的

  • 引用类型拷贝的是引用地址,拷贝前后的变量引用同一个堆中的对象

深拷贝(deepCopy)

  • 基本类型是复制的变量名和值,值变了不影响源变

  • 用类型复制的是变量名和值(引用地址),对象变了,会影响源变量(引用地址是一样的)

  • String:是不可变对象,重新赋值时,会在常量表新生成字符串(如果已有,直接取他的引用地址),将新字符串的引用地址赋值给栈中的新变量,因此源变量不会受影响

也可以理解为浅拷贝是修改堆内存中的同一个值 , 而深拷贝是修改堆内存中的不同的值

java.io.Serializable

  • 序列化 : 把Java对象转换为字节序列(二进制)的过程

  • 序列化作用 : 在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中

  • 反序列化 : 把字节序列(二进制)恢复为Java对象的过程

  • 反序列化作用 : 根据字节流中保存的对象状态及描述信息,通过反序列化重建对象

json/xml的数据传递

  1. 在数据传输(也可称为网络传输)前,先通过序列化工具类将Java对象序列化为json/xml文件。

  2. 在数据传输(也可称为网络传输)后,再将json/xml文件反序列化为对应语言的对象

序列化优点

  1. 将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。

  2. 序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。

  3. 通过序列化可以在进程间传递对象。

ArrayList属性

// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例。
private static final Object[] EMPTY_ELEMENTDATA = { 
   };
// 用于默认大小的空实例的共享空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = { 
   };
// 当前数据对象存放地方,当前对象不参与序列化
transient Object[] elementData
// 当前ArrayList的长度
private int size;
// 数组最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ArryList构造函数

无参构造

    /** * Constructs an empty list with an initial capacity of ten. */
    public ArrayList() { 
   
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

如果使用无参构造创建ArrayList对象 , 此时我们创建的ArrayList对象中的elementData中的长度是1,size是0 , 当第一次添加元素的时候 , elementData才会变成默认长度10

有参构造

    /** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */
    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);
        }
    }

如果传入参数,则代表指定ArrayList的初始数组长度,传入参数如果是大于等于0,则使用用户的参数初始化,如果用户传入的参数小于0,则抛出异常

    /** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */
    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;
        }
    }
  1. 将collection对象转换成数组,然后将数组的地址赋给elementData。

  2. 更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData

  3. 如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。

注意:this.elementData = arg0.toArray(); 这里执行的简单赋值是浅拷贝,所以要执行Arrays.copy 做深拷贝

add(E e)方法源码

这种情况下默认使用尾插法 , 时间复杂度O(1) , 如果需要扩容 , 则使用Arrays.copy()进行深拷贝 , 以此来生成新的数组 , 然后把旧数组的值赋值给新数组 , 并且如果此时数组的长度+1小于默认容量 , 那么就会初始化一个容量为10的数组 , 并不是在new ArrayList<>()的时候就会生成默认容量的数组

 /** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */
    public boolean add(E e) { 
   
        // 判断是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) { 
   
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
   
            // 从默认容量和当前ArrayList的size + 1选一个最大的最为扩容阈值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) { 
   
        // 将修改次数(modCount)自增1
        modCount++;

        // overflow-conscious code
        // 判断是否需要扩容 , 如果当前容量减去数组容量大于0 , 表示需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

        /** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */
    private void grow(int minCapacity) { 
   
        // overflow-conscious code
        // 旧的容量
        int oldCapacity = elementData.length;
        // 新容量等于旧容量 + 旧容量左移1位
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            // 如果新容量 - 需要扩容阈值 < 0 那么新容量就使用扩容阈值
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 如果新容量 - 数组最大长度 > 0 , 那么就需要另外判断
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) { 
   
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

旧数组值赋值给新数组之后可能会出现三种情况 :

  1. 如果旧数组之前经过多次年轻代的GC(yongGC)没有被清理掉的话 ,旧数组就会进入老年代 , 那么生成新数组之后 ,旧数组会被老年代GC掉

  2. 如果旧数组经过多次yongGC , 但是没有符合进入老年代的条件及每次yongGC都会在Survivor 0 ,Survivor 1进行移动 , 每移动一次同时记录次数, 达到15次 , 才会被放入老年代 ,这个时候就会被yongGC

  3. 如果新数组分配的和旧数组是同一块地址 ,这个时候不需要进行回收

add(int index, E element)源码

如果使用的这个add方法 , 那么将会在数组指定位置插入一个新元素 , 其它元素向后移动 ,此时时间复杂度为O(N) , 扩容机制和add(E e)方法是相同的

    public void add(int index, E element) { 
   
        // 如果index大于当前ArrayList的size , 或者小于0 , 则抛异常
        rangeCheckForAdd(index);
        // 判断是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 将新的数据内容存放到数组的指定位置(index)上
        elementData[index] = element;
        size++;
    }

get(int index)源码

数组的查询是通过一个公式来计算的 :a[n] = 起始地址 + (n * 字节数) , n表示的就是下标 , 比如我们此时要寻找下边为2的元素 , 那么通过公式计算 , 100 + (2*4) , 那么找到的就是地址为108的元素 , 也就是说5 , 因为它有一个公式 , 经过计算 , 可以很快的找到这个元素的地址 , 所以它的查询的时间复杂度是o(1) , 因此使用get方法随机访问元素的时候 , 时间复杂度为O(1)

    public E get(int index) { 
   
        // 检查下标是否越界
        rangeCheck(index);

        return elementData(index);
    }
    
    E elementData(int index) { 
   
        // 返回下标对应的元素
        return (E) elementData[index];
    }

contains方法

调用indexOf方法,遍历数组中的每一个元素作对比,如果找到对于的元素,则返回true,没有找到则返回false

    public boolean contains(Object o) { 
   
        return indexOf(o) >= 0;
    }
    
    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;
    }

remove(int index)源码

根据下标删除的情况下时间复杂度分为几种情况考虑 : 如果使用尾删法及删除最后一个元素 , 那个时间复杂度为O(1) , 不涉及到其他元素的移动 , 否则时间复杂度为O(N) , 将会涉及到其他元素的移动

 public E remove(int index) { 
   
        // 检查下标是否越界
        rangeCheck(index);
        // 修改次数 + 1
        modCount++;
        // 根据下标获取需要修改的值
        E oldValue = elementData(index);
        // 判断是否需要移动数组元素
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 使用System.arraycopy 将需要插入的位置(index)后面的元素统统往前移动一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 不需要移动元素的话 , 把最后一位置为空
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

remove(Object o)源码

如果使用这种方法进行删除元素 , 那么时间复杂度为O(N) , 涉及到元素的比阿尼寻找以及移动

    public boolean remove(Object o) { 
   
        // 循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作
        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 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
    }

clear()源码

添加操作次数(modCount),将数组内的元素都置空,等待垃圾收集器收集,不减小数组容量。

   public void clear() { 
       
        modCount++;

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

        size = 0;
    }

subList(int fromIndex, int toIndex)源码

我们看到代码中是创建了一个ArrayList 类里面的一个内部类SubList对象,传入的值中第一个参数时this参数,其实可以理解为返回当前list的部分视图,真实指向的存放数据内容的地方还是同一个地方,如果修改了sublist返回的内容的话,那么原来的list也会变动。

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

iterator()源码

interator方法返回的是一个内部类,由于内部类的创建默认含有外部的this指针,所以这个内部类可以调用到外部类的属性

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

一般我们调用interator之后会进行集合的遍历 , 这里使用next做遍历的时候有个需要注意的地方,就是调用next的时候,可能会引发ConcurrentModificationException,当修改次数,与期望的修改次数(调用iterator方法时候的修改次数)不一致的时候,会发生该异常,详细我们看一下代码实现

    @SuppressWarnings("unchecked")
    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();
    }
    

expectedModCount这个值是在用户调用ArrayList的iterator方法时候确定的,但是在这之后用户add,或者remove了ArrayList的元素,那么modCount就会改变,那么这个值就会不相等,将会引发ConcurrentModificationException异常,这个是在多线程使用情况下,比较常见的一个异常。

System.arraycopy 方法

参数 说明
src 原数组
srcPos 原数组
dest 目标数组
destPos 目标数组的起始位置
length 要复制的数组元素的数目

Arrays.copyOf方法

参数 说明
original 要复制的数组
newLength 要返回副本的长度
newType 要返回副本的类型

其实Arrays.copyOf底层也是调用System.arraycopy实现的源码如下:

    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

小结

  • ArrayList底层是基于动态数组实现的 , 可以进行动态扩容

  • ArrayList自己实现了序列化和反序列化方法 , 因为它实现了writeObject和readObject方法

  • ArrayList在编码时如果知道数据比较多 , 那么可以提前传入容量值 , 避免进行频繁扩容

  • ArrayList使用尾插法的时间复杂度为O(1) , 指定位置插入时间复杂度为O(N)

  • ArrayList支持随机访问 , 其时间复杂度为O(1)

  • ArrayList使用尾删法时 , 时间复杂度为O(1) , 并且会把最后一个元素置位null , 其它删除时间复杂度为O(N) , 因为会涉及到元素的移动以及元素的遍历

  • ArrsyList是线程不安全的

发表回复