<Java八股文面试>ArrayList源码 | Iterator源码 | LinkedList和ArrayList对比(上)

简介: <Java八股文面试>ArrayList源码 | Iterator源码 | LinkedList和ArrayList对比

1. ArrayList

1.1 ArrayList 扩容规则介绍


ArrayList() 会使用长度为零的数组

ArrayList(int initialCapacity) 会使用指定容量的数组

public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量

add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍

addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量的1.5 倍, 实际元素个数).即:下次扩容容量 和 实际元素个数之间选一个最大值.

备注:对于4和5,默认指的就是无参构造方法,要是有参就是在参数的基础上进行扩容啦.


图解

1.无参构造方法下add()方法扩容机制


b7746a89970faa5cbf9aa0364ed91f23.png

代码演示:

/**
     * 
     * 方法功能:测试ArrayList的扩容规则
     * 1.ArrayList扩容前20次的结果:
     * [0, 10, 15, 22, 33, 49, 73, 109, 163, 244, 366,549, 823, 1234, 1851, 2776, 4164, 6246, 9369, 14053, 21079]
     * 2.步骤分析:22 = 15 + (15 >> 2); 49 = 33 + (33 >> 2)
     * 3.可以看出,当元素个数为100个时,需要扩容的大小为109.
     */
private static List<Integer> arrayListGrowRule(int n) {
    List<Integer> list = new ArrayList<Integer>();
    int init = 0;
    list.add(init);
    if (n >= 1) {
        init = 10;
        list.add(init);
    }
    for (int i = 1; i < n; i++) {
        init += (init) >> 1;
        list.add(init);
    }
    return list;
}
/**
     * 方法功能:当集合为空时,使用addAll()向集合中添加元素
     * addAll方法扩容原理:下次扩容容量 和 实际元素个数之间选一个最大值.
     */
private static void testAddAllGrowEmpty() {//
    ArrayList<Integer> list = new ArrayList<Integer>();
    // list.addAll(List.of(1, 2, 3));//此时输出的length为10,
    list.addAll(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));//此时输出的length为11=> max(11,10)_
    System.out.println(length(list));
}
/**
     * 方法功能:当集合不为为空时,使用addAll()向集合中添加元素
     */
private static void testAddAllGrowNotEmpty() {
    ArrayList<Integer> list = new ArrayList<Integer>();
    for (int i = 0; i < 10; i++) {
        list.add(i);
    }
    // list.addAll(List.of(1, 2, 3));//此时输出的length为15=>max(15,13)
    list.addAll(List.of(1, 2, 3, 4, 5, 6));//此时输出的length为16=>max(15,16)
    System.out.println(length(list));
}


1.2 ArrayList源码剖析


ArrayList是List接口的实现类,它是支持根据需要而动态增长的数组。java中标准数组是定长的,在数组被创建之后,它们不能被加长或缩短。这就意味着在创建数组时需要知道数组的所需长度,但有时我们需要动态程序中获取数组长度。ArrayList就是为此而生的。


1.2.1 ArrayList构造方法和属性分析


源码如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    /**
     * 默认初始化容量
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * 为空实例赋值的空数组对象.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
    /**
     * 空数组对象,和上面用途稍微不一样.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    /**
     * 存放元素的数组对象
     */
    transient Object[] elementData; // non-private to simplify nested class access
    /**
     * 元素个数
     */
    private int size;
    //无参构造
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//初始化为一个空数组
    }
    //参数为容量的有参构造
    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);
        }
    }
    //参数为集合的有参构造
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {//集合不为空,则创建一个新的数组,并将元素复制到新数组里,elementData指向新数组.
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {// 如果是一个空集合初始化赋值,则直接将其赋值为空数组.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
}


结论


在无参构造中,我们看到了在用无参构造来创建对象的时候其实就是创建了一个空数组,长度为0


在参数为容量的构造中,传入的参数是正整数就按照传入的参数来确定创建数组的大小,否则异常


在参数为集合的构造中,传入的集合如果没有元素,则将其赋值为空数组.如果集合有元素,就创建一个新的数组,并将其元素赋值到新数组,更新集合中的数组对象引用.


1.2.2 add()方法


总的来说就是分两步:


扩容:把原来的数组复制到另一个内存空间更大的数组中

复制(添加)元素到新数组:把新元素添加到扩容以后的数组中

84ad4e930dc7c0a44cda6363be30f736.png


源码如下:

//计算需要扩容的容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //如果在添加的时候远数组是空的,取默认容量与minCapacity之间的最大值.
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 扩容后的为 Max(默认容量 , 加入元素后的容量) ,这样可以保证只扩容一次.
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //如果ArrayList不为空,就返回加入元素后的容量 (之前的元素容量+1) 
    return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果所需容量 比 elementData实际的容量 要小,则需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);//扩容
}
//注意:minCapacity代表加入 元素后需要的容量
//elementData:实际存放元素的数组, 和 minCapacity大小关系不确定.

接下来重点来了,ArrayList扩容的关键方法grow():

//扩容方法
private void grow(int minCapacity) {
    //获取到ArrayList中elementData数组的内存空间长度
    int oldCapacity = elementData.length;
    //计算扩容后的大小:扩容至原来的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组. 如果不够,则直接使用minCapacity 的值,这样可以避免重复扩容. 
    if (newCapacity - minCapacity < 0)
        // 不够就将数组长度设置为需要的长度
        newCapacity = minCapacity;
    //若预设值大于默认的最大值检查是否溢出
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
    // 并将elementData的数据复制到新的内存空间
    elementData = Arrays.copyOf(elementData, newCapacity);
}

结论: 从方法中我们可以清晰的看出其实ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。


1.2.3 addAll()方法


通过观察addAll()方法源码,可以看到其原理和add()方法类似,都是先扩容,再将数组内容复制到新数组.


public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

2. Iterator


2.1 Fail-Fast 和 Fail-Safe 基本介绍


ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败


CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

public class FailFastVsFailSafe {
    // fail-fast 一旦发现遍历的同时其它人来修改,则立刻抛异常
    // fail-safe 发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成
    //ArrayList属于fail-fast类型
    private static void failFast() {
        ArrayList<Student> list = new ArrayList<>();
        list.add(new Student("A"));
        list.add(new Student("B"));
        list.add(new Student("C"));
        list.add(new Student("D"));
        for (Student student : list) {
            System.out.println(student);
        }
        System.out.println(list);
    }
    //CopyOnWriteArrayList属于fail-safe类型
    private static void failSafe() {
        CopyOnWriteArrayList<Student> list = new CopyOnWriteArrayList<>();
        list.add(new Student("A"));
        list.add(new Student("B"));
        list.add(new Student("C"));
        list.add(new Student("D"));
        for (Student student : list) {
            System.out.println(student);
        }
        System.out.println(list);
    }
    static class Student {
        String name;
        public Student(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    public static void main(String[] args) {
        failFast();
        // failSafe();
    }
}

运行结果

1.测试fail-fast

  • 添加断点

202204071430856.png

  • 在迭代过程中未list增加一个新项

202204071430871.png

  • 程序报错

19421213216be2e0823ac49af59615dc.png

2.测试fail-safe

添加断点和添加新项和上面一致. 贴一下最终的运行结果:


41a3a835e706940ecbf8b5530859fafc.png

根据结果可以得出结论:copyonwrite是把原集合复制出来,在新集合上进行修改,修改好后指向新集合。现在遍历的是旧集合不影响


相关文章
|
6天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
21 5
|
5天前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
14 1
|
4天前
|
算法 Java
JAVA 二叉树面试题
JAVA 二叉树面试题
10 0
|
存储 安全 Java
LinkedList源码解读—Java8版本(上)
LinkedList源码解读—Java8版本(上)
161 0
LinkedList源码解读—Java8版本(上)
|
Java
LinkedList源码解读—Java8版本(下)
LinkedList源码解读—Java8版本(下)
132 0
|
存储 Java
LinkedList源码解读—Java8版本(中)
LinkedList源码解读—Java8版本(中)
124 0
|
4天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
71 38
|
1天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
5天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
20 1
[Java]线程生命周期与线程通信
|
3天前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。