java框架集合List子接口之ArrayList源码剖析

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

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的数据传递

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


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


序列化优点


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


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


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


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

将collection对象转换成数组,然后将数组的地址赋给elementData。


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


如果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;
    }

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


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


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


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


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是线程不安全的

相关文章
|
4天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
1天前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
10 3
|
4天前
|
存储 Java
判断一个元素是否在 Java 中的 Set 集合中
【10月更文挑战第30天】使用`contains()`方法可以方便快捷地判断一个元素是否在Java中的`Set`集合中,但对于自定义对象,需要注意重写`equals()`方法以确保正确的判断结果,同时根据具体的性能需求选择合适的`Set`实现类。
|
4天前
|
存储 Java 开发者
在 Java 中,如何遍历一个 Set 集合?
【10月更文挑战第30天】开发者可以根据具体的需求和代码风格选择合适的遍历方式。增强for循环简洁直观,适用于大多数简单的遍历场景;迭代器则更加灵活,可在遍历过程中进行更多复杂的操作;而Lambda表达式和`forEach`方法则提供了一种更简洁的函数式编程风格的遍历方式。
|
4天前
|
Java 开发者
|
4天前
|
存储 Java 开发者
Java中的集合框架深入解析
【10月更文挑战第32天】本文旨在为读者揭开Java集合框架的神秘面纱,通过深入浅出的方式介绍其内部结构与运作机制。我们将从集合框架的设计哲学出发,探讨其如何影响我们的编程实践,并配以代码示例,展示如何在真实场景中应用这些知识。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和实用技巧。
7 0
|
6月前
|
存储 安全 Java
java集合框架及其特点(List、Set、Queue、Map)
java集合框架及其特点(List、Set、Queue、Map)
|
3月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
3月前
|
Java
【Java集合类面试二十三】、List和Set有什么区别?
List和Set的主要区别在于List是一个有序且允许元素重复的集合,而Set是一个无序且元素不重复的集合。
|
17天前
|
安全 Java 程序员
深入Java集合框架:解密List的Fail-Fast与Fail-Safe机制
本文介绍了 Java 中 List 的遍历和删除操作,重点讨论了快速失败(fail-fast)和安全失败(fail-safe)机制。通过普通 for 循环、迭代器和 foreach 循环的对比,详细解释了各种方法的优缺点及适用场景,特别是在多线程环境下的表现。最后推荐了适合高并发场景的 fail-safe 容器,如 CopyOnWriteArrayList 和 ConcurrentHashMap。
46 5
下一篇
无影云桌面