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

简介: 前言文本已收录至我的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就这么多了,大家自己可以对着博客,对着源码看,我感激它这个源码不是很难,基于数组的把可能是 因为博主也是一个开发萌新 我也是一边学一边写 我有个目标就是一周 二到三篇 希望能坚持个一年吧 希望各位大佬多提意见,让我多学习,一起进步。

相关文章
|
3月前
|
Java 大数据 API
Java Stream API:现代集合处理与函数式编程
Java Stream API:现代集合处理与函数式编程
264 100
|
3月前
|
Java API 数据处理
Java Stream API:现代集合处理新方式
Java Stream API:现代集合处理新方式
298 101
|
3月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
2月前
|
存储 Java 索引
用Java语言实现一个自定义的ArrayList类
自定义MyArrayList类模拟Java ArrayList核心功能,支持泛型、动态扩容(1.5倍)、增删改查及越界检查,底层用Object数组实现,适合学习动态数组原理。
113 4
|
2月前
|
存储 算法 安全
Java集合框架:理解类型多样性与限制
总之,在 Java 题材中正确地应对多样化与约束条件要求开发人员深入理解面向对象原则、范式编程思想以及JVM工作机理等核心知识点。通过精心设计与周密规划能够有效地利用 Java 高级特征打造出既健壮又灵活易维护系统软件产品。
103 7
|
3月前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
3月前
|
缓存 Java 开发者
Java 开发者必看!ArrayList 和 LinkedList 的性能厮杀:选错一次,代码慢成蜗牛
本文深入解析了 Java 中 ArrayList 和 LinkedList 的性能差异,揭示了它们在不同操作下的表现。通过对比随机访问、插入、删除等操作的效率,指出 ArrayList 在多数场景下更高效,而 LinkedList 仅在特定情况下表现优异。文章强调选择合适容器对程序性能的重要性,并提供了实用的选择法则。
218 3
|
4月前
|
存储 NoSQL Java
Java Stream API:集合操作与并行处理
Stream API 是 Java 8 提供的集合处理工具,通过声明式编程简化数据操作。它支持链式调用、延迟执行和并行处理,能够高效实现过滤、转换、聚合等操作,提升代码可读性和性能。
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
【Java集合类面试二十三】、List和Set有什么区别?
List和Set的主要区别在于List是一个有序且允许元素重复的集合,而Set是一个无序且元素不重复的集合。