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

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

2.2 fail-fast和fail-safe源码剖析

fail-fast源码分析

当使用增强for迭代list集合时,会先创建一个Itr对象(属于Iterator类),为属性expectedModCount初始化.


expectedModCount的初始值为list中的modCount (modCount 是list的成员变量,记录list被修改的次数)


之后每次迭代list就是用Itr对象.先判断hasNext()是否有值,如果则调用NEXT()方法进行获取.


NEXT()方法执行时,会先判断是否存在并发修改.如果存在则抛出异常,不存在则返回需要的元素.


分析演示场景: 迭代过程中为list增加了一个元素,导致modCount变成了5,但是expectedModCount还为4.所以Next()中验证是否存在修改时就会报异常.

private class Itr implements Iterator<E> {
    int cursor;       // 返回下一个元素的下标
    int lastRet = -1; // 返回最后一个元素的下标,如果没有则返回-1
    int expectedModCount = modCount;//迭代器对象刚进行迭代时修改的初始次数(也就是循环开始时的修改次数)
    Itr() {}//构造方法
    public boolean hasNext() {//判断是否还有下一个元素
        return cursor != size;//如果5个元素 size:5 当遍历完最后一个元素后再次迭代时,cursor=5,返回false.
    }
    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)//如果进行了修改modCount就会增加,导致这俩属性值不相等,抛出并发修改异常
            throw new ConcurrentModificationException();
    }
}

fail-safe源码分析


当使用增强for迭代list集合时,会先创建一个COWIterator对象,将需要迭代的集合赋值给 snapshot,cursor初始化为0.


之后每次迭代list就是使用COWIterator对象,先hasNext()判断是否有值.然后调用Next()获取值.


**分析演示场景:**当进行for遍历时,会把需要遍历的集合在COWIterator中存一份.之后每次遍历都是使用COWIterator中的数组对象. 当在遍历过程中进行add操作时,会创建一个新的数组,将老的元素和新的元素放入,再赋值给list集合中的数组对象.这个过程也被称为 写时复制(读写分离)


所以最后输出结果是:遍历出的没有新元素E,list直接输出有新元素E


//COWIterator
static final class COWIterator<E> implements ListIterator<E> {
    /** 数组的快照 */
    private final Object[] snapshot;
    /** 返回元素的下一个坐标.  */
    private int cursor;
    //构造器
    COWIterator(Object[] es, int initialCursor) {
        cursor = initialCursor;
        snapshot = es;
    }
    //判断是否有下一个袁旭
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    //获取下一个元素的值.
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];//cursor指针后移
    }
}
//CopyOnWriteArrayList类
public class CopyOnWriteArrayList<E> implements List<E>{
    final transient Object lock = new Object();
    //数组对象用于存放元素
    private transient volatile Object[] array;
    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }
    //添加元素
    public boolean add(E e) {
        synchronized (lock) {
            Object[] es = getArray();//获取之前的数组对象
            int len = es.length;//获取长度
            es = Arrays.copyOf(es, len + 1);//创建一个 len + 1 的新数组
            es[len] = e;//将新的值放在最后一个位置上.
            setArray(es);//将新数组对象赋值给list中的array
            return true;
        }
    }
}

2.3 补充:Vector底层使用的是哪种迭代机制呢?

Vector其实就比ArrayList多了个同步机制,也就是每个方法加上了synchronized.

经过Debug发现,Vector使用的也是Itr作为迭代器,其迭代类型属于:Fail-Fast.

代码演示

private static void testVector(){
    Vector <Student> vector = new Vector <>();
    vector.add(new Student("A"));
    vector.add(new Student("B"));
    vector.add(new Student("C"));
    vector.add(new Student("D"));
    for (Student student : vector) {
        System.out.println(student);
    }
    System.out.println(vector);
}

4494dbb09bea0eb73aa57f43f5381fa5.png

3. LinkedList

3.1 对比LinkedList 和 ArrayList 的区别


基于双向链表,无需连续内存

随机访问慢(要沿着链表遍历)

头尾插入删除性能高

无法很好的利用CPU缓存,无法很好的配合局部性原理,占用内存多,

ArrayList


基于数组,需要连续内存

随机访问快(指根据下标访问)

尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低 (但是性能也可以秒杀LinkedList)

可以很好的利用CPU缓存,配合局部性原理,占用内存少,


3.2 代码对比两者性能

3.2.1 对比继承和实现关系

根据继承和实现关系,可以看出 ArrayList比LinkedList多实现一个RandomAccess接口.


public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{}
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{}

RandomAccess是个标记接口, 主要用于标记该类使用下标进行访问的,还是利用指针进行访问的.

 /* 
 *<pre>
 *     for (int i=0, n=list.size(); i &lt; n; i++)
 *         list.get(i);
 * </pre>
 * 上面这种循环比下面这种循环效率更高:
 * <pre>
 *     for (Iterator i=list.iterator(); i.hasNext(); )
 *         i.next();
 * </pre>
 *
 * @since 1.4
 */
public interface RandomAccess {//标记接口,不做任何实现. 
}

3.2.2 对比随机访问性能

public static void main(String[] args) {
    int n = 1000;
    int insertIndex = n;
    //产生1000个随机数,测试对比ArrayList和LinkedList的随机访问性能
    for (int i = 0; i < 5; i++) {//测试五次(JVM要预热一下,次数越多越准确O!)
        int[] array = randomArray(n);
        List<Integer> list1 = Arrays.stream(array).boxed().collect(Collectors.toList());
        LinkedList<Integer> list2 = new LinkedList<>(list1);
        randomAccess(list1, list2, n / 2);//访问中间的数
    }
}
//随机访问方法
static void randomAccess(List<Integer> list1, LinkedList<Integer> list2, int mid) {
    StopWatch sw = new StopWatch();
    sw.start("ArrayList");
    list1.get(mid);
    sw.stop();
    sw.start("LinkedList");
    list2.get(mid);
    sw.stop();
    System.out.println(sw.prettyPrint());
}

测试结果:

3fc8d38407d839809548b0975b821415.png


3.2.3 对比插入功能

  • 头部插入对比
private static void addFirst(List<Integer> list1, LinkedList<Integer> list2) {
    StopWatch sw = new StopWatch();
    sw.start("ArrayList");
    list1.add(0, 100);
    sw.stop();
    sw.start("LinkedList");
    list2.addFirst(100);
    sw.stop();
    System.out.println(sw.prettyPrint());
}

结果:LinkedList远超于ArrayList.

ddbcba632feda4e9d41f46e99ce62146.png

  • 尾部插入对比
private static void addLast(List<Integer> list1, LinkedList<Integer> list2) {
    StopWatch sw = new StopWatch();
    sw.start("ArrayList");
    list1.add(100);
    sw.stop();
    sw.start("LinkedList");
    list2.add(100);
    sw.stop();
    System.out.println(sw.prettyPrint());
}

结果:ArrayList更快一些

6d05d1bf520e3901680ab326d2333e04.png

  • 中间插入
private static void addMiddle(List<Integer> list1, LinkedList<Integer> list2, int mid) {
    StopWatch sw = new StopWatch();
    sw.start("ArrayList");
    list1.add(mid, 100);
    sw.stop();
    sw.start("LinkedList");
    list2.add(mid, 100);
    sw.stop();
    System.out.println(sw.prettyPrint());
}

结果:ArrayList远超于LinkedList (因为LinkedList的迭代过程非常缓慢)


45aa661f7c8fc862a06ed21a7f140328.png


总结:


传统的 ArrayList:增删慢,查询快 LinkedList:增删快,查询慢 说法是完全错误的.


ArrayList除了头部插入比LinkedList效率低,其他方面都是超于LinkedList. 所以实际项目开发中,建议使用ArrayList.


3.2.4 对比占用内存


没有CPU缓存前:数据的运算,先将数据从硬盘读到CPU,然后CPU执行计算,计算完写到硬盘.但是CPU的计算速度是很快的(1s执行亿次级),例如CPU执行指令<1nm,但是读数据到CPU和写数据到硬盘却需要几百nm,CPU利用率极低.


有了CPU缓存:数据的运算,先将数据从硬盘加载到缓存中,然后CPU从缓存中读数据(可以提高到10nm),接着CPU进行运算,运算完将结果写到缓存,由缓存将数据写入硬盘.CPU的效率可以大大提高.


489b076d06de33039810b78ccf6da194.png


局部性原理: 读取元素时,其相邻元素也有很大概率被访问到.


对于ArrayList,会将其相邻元素也加载进入缓存,由于其元素在内存是是连续的.下次使用就不用再重新加载了,直接从缓存中取.


而对于LinkedList,由于其数据存储并不是连续的.虽然也将即将使用的数据及其相邻内存的数据加入缓存,但是后面需要的数据可能并不在当前的缓存中.而且一般缓存也是很小的,后面加入的数据可能覆盖前面的.所以需要经常将数据加入缓存. 从而无法很好的配合局部性原理.


如果有收获!!! 希望老铁们来个三连,点赞、收藏、转发。

创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客


相关文章
|
9月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
11月前
|
Java 数据库连接 数据库
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
本文全面总结了Java核心知识点,涵盖基础语法、面向对象、集合框架、并发编程、网络编程及主流框架如Spring生态、MyBatis等,结合JVM原理与性能优化技巧,并通过一个学生信息管理系统的实战案例,帮助你快速掌握Java开发技能,适合Java学习与面试准备。
488 2
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
|
9月前
|
算法 Java
50道java基础面试题
50道java基础面试题
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
438 4
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
2619 2
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。