史上最全的Java容器集合之ArrayList(源码解读)(下)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在

具体方法


add方法


/**
     * 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;
    }
复制代码


第一步 扩容操作 第二步 往尾部添加一个元素 跟着往下看

 ensureCapacityInternal(xxx); 确定内部容量的方法   


private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) { //看,判断初始化的elementData是不是空的数组,也就是没有长度
    //因为如果是空的话,minCapacity=size+1;其实就是等于1,空的数组没有长度就存放不了,所以就将minCapacity变成10,也就是默认大小,但是带这里,还没有真正的初始化这个elementData的大小。
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
    //确认实际的容量,上面只是将minCapacity=10,这个方法就是真正的判断elementData是否够用
        ensureExplicitCapacity(minCapacity);
    }
复制代码


ensureExplicitCapacity(xxx);

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
//minCapacity如果大于了实际elementData的长度,那么就说明elementData数组的长度不够用,不够用那么就要增加elementData的length。这里有的读者就会模糊minCapacity到底是什么呢,这里给你们分析一下
/*第一种情况:由于elementData初始化时是空的数组,那么第一次add的时候,minCapacity=size+1;也就minCapacity=1,在上一个方法(确定内部容量ensureCapacityInternal)就会判断出是空的数组,就会给
  将minCapacity=10,到这一步为止,还没有改变elementData的大小。
 第二种情况:elementData不是空的数组了,那么在add的时候,minCapacity=size+1;也就是minCapacity代表着elementData中增加之后的实际数据个数,拿着它判断elementData的length是否够用,如果length
不够用,那么肯定要扩大容量,不然增加的这个元素就会溢出。
*/
        if (minCapacity - elementData.length > 0)
    //arrayList能自动扩展大小的关键方法就在这里了
            grow(minCapacity);
    }
复制代码


grow(xxx); arrayList核心的方法,能扩展数组大小的真正秘密。

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  //将扩充前的elementData大小给oldCapacity
        int newCapacity = oldCapacity + (oldCapacity >> 1);//newCapacity就是1.5倍的oldCapacity
        if (newCapacity - minCapacity < 0)//这句话就是适应于elementData就空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,就是为10.前面的工作都是准备工作。
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
    //新的容量大小已经确定好了,就copy数组,改变容量大小咯。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
复制代码


 hugeCapacity();

//这个就是上面用到的方法,很简单,就是用来赋最大值。
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
//如果minCapacity都大于MAX_ARRAY_SIZE,那么就Integer.MAX_VALUE返回,反之将MAX_ARRAY_SIZE返回。因为maxCapacity是三倍的minCapacity,可能扩充的太大了,就用minCapacity来判断了。
//Integer.MAX_VALUE:2147483647   MAX_ARRAY_SIZE:2147483639  也就是说最大也就能给到第一个数值。还是超过了这个限制,就要溢出了。相当于arraylist给了两层防护。
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
复制代码


总结一下扩容过程

  • 1.判断是否需要扩容,如果需要,计算需要扩容的最小容量
  • 2.如果确定扩容,就执行grow(int minCapacity),minCapacity为最少需要的容量
  • 3.第一次扩容是的容量大小是原来的1.5倍
  • 4如果第一次 扩容后容量还是小于minCapacity,那就扩容为minCapacity
  • 5.最后,如果minCapacity大于最大容量,则就扩容为最大容量



add(int, E)方法

public void add(int index, E element) {
        // 插入数组位置检查
        rangeCheckForAdd(index);
        // 确保容量,如果需要扩容的话则自动扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index); // 将index后面的元素往后移一个位置
        elementData[index] = element; // 在想要插入的位置插入元素
        size++; // 元素大小加1
    }
  // 针对插入数组的位置,进行越界检查,不通过抛出异常
  // 必须在0-最后一个元素中间的位置插入,,否则就报错
  // 因为数组是连续的空间,不存在断开的情况
  private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
复制代码


这个也不难 前面我们数组已经实现了


get方法


public E get(int index) {
        // 先进行越界检查
        rangeCheck(index);
        // 通过检查则返回结果数据,否则就抛出异常。
        return elementData(index);
    }
    // 越界检查的代码很简单,就是判断想要的索引有没有超过当前数组的最大容量
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
        @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }
复制代码


先检查数组越界,然后你就是直接去数组那边拿数据然后返回


set(int index, E element)


// 作用:替换指定索引的元素
  public E set(int index, E element) {
        // 索引越界检查
        rangeCheck(index);
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
复制代码


这个就是替换指定位置的元素

remove(int):通过删除指定位置上的元素


public E remove(int index) {
       // 索引越界检查
        rangeCheck(index);
        modCount++;
        // 得到删除位置元素值
        E oldValue = elementData(index);
        // 计算删除元素后,元素右边需要向左移动的元素个数
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 进行移动操作,图形过程大致类似于上面的add(int, E)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 元素大小减1,并且将最后一个置为null.
        // 置为null的原因,就是让gc起作用,所以需要显示置为null
        elementData[--size] = null; // clear to let GC do its work
        // 返回删除的元素值
        return oldValue;
    }
复制代码


remove(Object o)

public boolean remove(Object o) {
        // 如果删除元素为null,则循环找到第一个null,并进行删除
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    // 根据下标删除
                    fastRemove(index);
                    return true;
                }
        } else {
        // 否则就找到数组中和o相等的元素,返回下标进行删除
            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);
        // 元素大小减1,并且将最后一个置为null. 原因同上。
        elementData[--size] = null; // clear to let GC do its work
    }
复制代码


总结


ArrayList的优缺点


  • ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快
  • ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已
  • 删除元素时,涉及到元素复制,如果要复制的元素很多,那么就会比较耗费性能
  • 插入元素时,涉及到元素复制,如果要复制的元素很多,那么就会比较耗费性能


为什么ArrayList的elementData是用transient修饰的

  • 说明:ArrayList实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化
  • 理解:序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法。
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();
        }
    }
复制代码


  • 优点:这样做既提高了序列化的效率,减少了无意义的序列化;而且还减少了序列化后文件大小。

版本说明


  • 这里的源码是JDK8版本,不同版本可能会有所差异,但是基本原理都是一样的。

结尾


ArrayList就这么多了,大家自己可以对着博客,对着源码看,我感激它这个源码不是很难,基于数组的把可能是 因为博主也是一个开发萌新 我也是一边学一边写 我有个目标就是一周 二到三篇 希望能坚持个一年吧 希望各位大佬多提意见,让我多学习,一起进步。

相关文章
|
2月前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
52 3
|
3天前
|
存储 安全 算法
Java容器及其常用方法汇总
Java Collections框架提供了丰富的接口和实现类,用于管理和操作集合数据。
Java容器及其常用方法汇总
|
2月前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
60 5
|
3月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
3月前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
72 4
|
3月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
3月前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
62 2
|
3月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
3月前
|
存储 Java 开发者
Java中的集合框架深入解析
【10月更文挑战第32天】本文旨在为读者揭开Java集合框架的神秘面纱,通过深入浅出的方式介绍其内部结构与运作机制。我们将从集合框架的设计哲学出发,探讨其如何影响我们的编程实践,并配以代码示例,展示如何在真实场景中应用这些知识。无论你是Java新手还是资深开发者,这篇文章都将为你提供新的视角和实用技巧。
47 0
|
6月前
|
安全 算法 Java
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?
这篇文章讨论了Java集合类的线程安全性,列举了线程不安全的集合类(如HashSet、ArrayList、HashMap)和线程安全的集合类(如Vector、Hashtable),同时介绍了Java 5之后提供的java.util.concurrent包中的高效并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList。
【Java集合类面试二】、 Java中的容器,线程安全和线程不安全的分别有哪些?