首页 > 编程语言 >2-ArrayList底层结构和源码分析

2-ArrayList底层结构和源码分析

时间:2024-07-12 09:30:27浏览次数:14  
标签:transient ArrayList elementData 源码 数组 序列化 底层

2-ArrayList底层结构和源码分析

介绍汇总:

  1. ArrayList的注意事项
  2. ArrayList的运行重要步骤
  3. 补充

1-ArrayList的注意事项

  1. ArrayList 允许添加所有的元素,包括 null ,而且还可以多个 null
  2. ArrayList 是由数组来实现数据存储的。
  3. ArrayList 基本等同于 Vector ,除了 ArrayList 是线程不安全执行效率高)看源码。在多线程情况下,不建议使用 ArrayList。
// 此为 ArrayList 的 插入数据的方法
// 该方法并没有关键字 synchronized 修饰,可以表明 ArrayList 是线程不安全的
// synchronized 大概是加锁的意思
public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

2-ArrayList的运行重要步骤

  1. ArrayList 中维护了一个 Object 类型的数组 elementData 。(debug 看源码
transient Object[] elementData;

// 先来解释 transient 关键字
/*
  在Java中,transient关键字是一个变量修饰符,用来表示一个字段不应该被序列化。
  当一个对象被序列化时(比如,通过ObjectOutputStream),对象的所有变量都会被写入到序列化文件中,
  除了那些用transient修饰的变量。这样,当对象被反序列化时(比如,通过ObjectInputStream),
  transient变量将不会被恢复,它们的值将会是类型的默认值(比如,int的默认值是0,
  对象的默认值是null)。
*/

/*
  transient关键字主要用于以下情况:
  敏感信息:如果对象的某个字段包含敏感信息,如密码或个人身份识别信息,那么应该使用transient修饰该字段,以防止这些信息被序列化到文件中。
  无法序列化的对象:如果对象的某个字段引用了另一个无法序列化的对象,那么这个字段也应该用transient修饰,否则序列化过程将抛出NotSerializableException异常。
  派生字段:如果对象的某个字段的值可以从其他字段派生出来,那么该字段可以用transient修饰,以节省序列化和反序列化的时间和空间。
*/

// 解释一下 elementData 的作用
/*
  在 Java 的 ArrayList 类中,transient Object[] elementData; 是一个非常重要的成员变量,它用于存储 ArrayList 中的元素。
  ArrayList 是一个动态数组,能够根据需要自动扩容以存储更多的元素,而 elementData 就是用来实际存储这些元素的数组。
  
  transient 关键字在这里的作用是阻止 elementData 数组被自动序列化。
  由于 ArrayList 实现了 Serializable 接口,它支持序列化操作,
  但是 elementData 数组中的元素可能包含不应该被序列化的数据,
  或者数组本身的大小(容量)可能远大于实际存储的元素数量,导致序列化后的数据包含大量无用信息。
  
  因此,ArrayList 提供了两个私有方法 writeObject(java.io.ObjectOutputStream s) 和 readObject(java.io.ObjectInputStream s) 来控制序列化和反序列化的过程。
  在序列化时,writeObject 方法会先检查 elementData 数组的大小,如果它大于实际存储的元素数量,那么就会创建一个新的数组,只包含实际存储的元素,并将这个新数组序列化。
  在反序列化时,readObject 方法会负责创建一个新的 ArrayList 实例,并恢复其元素。
  
  总之,transient Object[] elementData; 在 ArrayList 中的作用是作为存储元素的数组,
  而 transient 关键字则用于控制这个数组的序列化行为,确保序列化后的数据不包含无用信息。
*/
  1. 当创建 ArrayList 对象是,若使用的是无参构造器,则初始的 elementData 容量为 0, 第一次添加,则扩容 elementData 为 10,若再次扩容,则扩容 elementData 为 1.5 倍(newCapacity = oldCapacity + (oldCapacity >> 1))。

  2. 若使用指定大小的构造器,则初始 elementData 容量为指定大小,若再次扩容,则直接扩容 elementData 为 1.5 倍。

  3. ArrayList 的扩容过程

注:这些流程图使用 debug 来完成。

        // 扩容流程代码
        ArrayList arrayList = new ArrayList();
        // ArrayList arrayList = new ArrayList(8);

        for (int i = 1 ; i <= 10 ; i ++) {
            arrayList.add(i) ;
        }

        for (int i = 11 ; i <= 15 ; i ++) {
            arrayList.add(i) ;
        }

        arrayList.add(100) ;
        arrayList.add(200) ;
        arrayList.add(null) ;

        for (Object object : arrayList) {
            System.out.println(object);
        }
  1. 注意

​ ArrayList 的无参构造器与指定大小的有参构造器,区别主要是刚开始初始化内存数组的不一样.

// 无参构造器
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);
        }
    }

3-补充

// 指定数组列表的大小为 0 的话,数组列表内部
// 会将 elementData = EMPTY_ELEMENTDATA 
// 区别于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
// 从源码中看,虽然都是空数组,但是他们扩容时不同
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA 第一次扩会扩成容量为 10 ,之后就是其 1.5 倍扩
// EMPTY_ELEMENTDATA 首次及第二次扩都以 minCapcity 来扩,之后可能都是其 1.5 倍扩
ArrayList arrayList = new ArrayList(0);

for (int i = 1 ; i <= 10 ; i ++) {
    arrayList.add(i) ;
}

for (int i = 11 ; i <= 15 ; i ++) {
    arrayList.add(i) ;
}

arrayList.add(100) ;
arrayList.add(200) ;
arrayList.add(null) ;

for (Object object : arrayList) {
    System.out.println(object);
}

标签:transient,ArrayList,elementData,源码,数组,序列化,底层
From: https://www.cnblogs.com/Yao-happy/p/18296947

相关文章