• ArrayList总结及部分源码分析


    ArrayList源码阅读笔记

    1. ArrayList继承的抽象类和实现的接口

    • ArrayList类实现的接口
      1. List接口:里面定义了List集合的基本接口,ArrayList进行了实现
      2. RandomAccess接口
      3. Cloneable接口
      4. Serializable:标记该类支持序列化
    • ArrayList继承了AbstractList抽象类

    2. ArrayList底层的数据结构

    • ArrayList底层是基于数组实现的

      1. 特点:

        1. 数组在内存中是一个内存地址连续的,固定内存大小的内存空间,并且ArrayList可以存放重复的元素。允许存储 一个null或者多个null。
      2. 优点:

        1. 数组查询元素的效率快,因为数组查询元素时只需要一个元素对应在数组中存储的下标就可以获取到对应位置的元素值。
        2. ArrayList是异步的,也就是说该集合线程不安全,但是执行效率快
      3. 缺点:

        1. 当向ArrayList集合中存储的新的元素时,长度已经不够时,就会触发ArrayList底层的扩容机制。其实底层就是数组扩容。因为数组扩容需要把旧的数组元素复制到新的数组中,所以在数据量特别大的时候,扩容的效率不高。
        2. 在指定位置添加和修改元素效率不高(尾部除外)。
          • 例如:在数组的尾部以外的地方插入一个元素,那么为例保证整个数组中所有元素的连续存储性,就必须向将从该位置到尾部的所有元素进行后移一位,腾出一个空的位置,再把新的元素进行插入。删除也是,删除了指定位置的之后需要把从该元素到尾部的所有元素向前移动一个位置。
        3. ArrayList的所有方法都没有进行使用 synchronized 关键字进行修饰,这就代表该类的所有方法都不是同步的,在多线程情况下存着线程安全问题。

    3. ArrayList适用的场景

    1. 对于频繁查询和修改集合中元素的场景非常适合,因为ArrayList的查询效率快。

    2. 单线程环境下,频繁对数据进行查询和修改时也适用。如果该多线程情况下可以使用Vector集合。该集合底层也是基于数组实现的,但是中会出现线程安全问题的方法使用了 synchronized 进行修饰,保证了线程安全。但是该集合目前已经基本废弃了。因为将方法进行同步了,那么效率就会下降。如果在多线程情况下还想使用ArrayList集合,这个时候可以创建一个线程安全的ArrayList集合

      // 创建一个线程安全的ArrayList集合,在多线程情况下能保证线程安全
      List<Object> list=Collections.synchronizedList(new ArrayList<Object>());
      

    4. ArrayList底层源码分析

    1. ArrayList的三个构造函数

    1. 无参构造

      /**
       * 在创建对象时,如果不指定构造函数,将调用此构造函数初始化一个长度为0,类型为Object的数组
       */
      public ArrayList() {
          this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
      }
      
      • 源码说明:无参构造器中用到的成员变量

        • 在创建对象时,如果不指定构造函数,将调用此构造函数初始化一个长度为0,类型为Object的数组
        // 一个 Object 类型的数组,可以存放任意类型的数据,初始化大小为0
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        
        // 一个 Object 类型的数组,可以存放任意类型的数据,不参与集合的序列化
        transient Object[] elementData; 
        
    2. 有参构造(传入一个整数,指定初始的集合长度大小)

      /**
       * 创建对象时,如果指定初始大小将掉有该方法,创建一个有指定初始换长度的ArrayList集合
       * 如果提前知道需要存储的数据量有多少,使用该构造函数,可以改善因为频繁扩容导致的性能下降问题
       * @param  initialCapacity  		创建对象时,指定的初始化长度
       * @throws IllegalArgumentException  如何传入的数字小于0,则抛出异常,非法参数异常
       */
      public ArrayList(int initialCapacity) {
          // 判断传入的参数是否大于0
          if (initialCapacity > 0) {
              // 创建一个Object类型,长度为 initialCapacity 的数组,赋给 elementData 数组
              this.elementData = new Object[initialCapacity];
          } else if (initialCapacity == 0) {  // 判断传入的参数是否等于0
              // 一个 Object 类型的数组,可以存放任意类型的数据,初始化大小为 0
              this.elementData = EMPTY_ELEMENTDATA;
          } else {  // 以上两个条件都不满足,即传入的参数小于0,抛出非法参数异常
              throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
          }
      }
      
      • 源码说明:

        // 一个 Object 类型的数组,可以存放任意类型的数据,不参与集合的序列化
        // 使用 writeObject() 来实现数据的序列化 和 readObject() 反序列化
        transient Object[] elementData; 
        
        // 一个 Object 类型的数组,可以存放任意类型的数据,初始化大小为0
        private static final Object[] EMPTY_ELEMENTDATA = {};
        
    3. 有参构造(传入一个Collection类型的集合)

      /**
       * 在初始化的时候直接将一个集合传入,可以吧传入集合的元素全部复制到创建的新集合中
       * @param c 					传入的集合
       * @throws NullPointerException   当传入的集合为空时,会抛出异常
       */
      public ArrayList(Collection<? extends E> c) {
      	elementData = c.toArray();
      	if ((size = elementData.length) != 0) {
      		if (elementData.getClass() != Object[].class)
      			elementData = Arrays.copyOf(elementData, size, Object[].class);
      	} else {
      		this.elementData = EMPTY_ELEMENTDATA;
      	}
      }
      

    2. 添加一条数据的完整流程

    1. 在每次添加数据时,如果数据是基本数据类型,会先将基本数据类型进行装箱操作,把基本数据类型转换成对应的包装类型(引用数据类型)
    // 例如:集合中存放Integer数据类型,在进行add操作时,会先进行装箱操作
    
    /**
     * 将基本数据类转换为引用数据类型
     * @param  i 	传入的参数为一个基本型数据类型
     * @return 		返回的参数是一个基本数据类型的包装类(引用数据类型)
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    1. 调用add方法进行添加操作
    /**
     * 添加元素方法,成功返回true,失败会抛出异常
     * @param e 			需要进行添加的元素值
     * @return <tt>true</tt> 添加成功返回true,添加过程中如果失败会抛出异常
     */
    public boolean add(E e) {
        // 判断是否需要扩容的
        ensureCapacityInternal(size + 1);  
        // 将传递进来的元素添加到数组的末尾
        elementData[size++] = e;
        // 返回true代表添加成功
        return true;
    }
    
    • 变量说明:

      /**
       * 该变量用于记录当前ArrayList中实际存储的元素个数
       */
      private int size;
      
    1. add方法中调用 ensureCapacityInternal 方法
    /**
     * 传入一个经过当前数组实际元素个数加 1 过后的参数,这样做主要是为了判断当前数组是否还支持添加新元素
     * @param minCapacity 当前数组实际元素个数加 1 过后的参数
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    1. 先调用 calculateCapacity 方法,在调用 ensureExplicitCapacity
    • calculateCapacity 方法

      /**
       * 用于确定存储元素的数组需要的长度,或者当该数组为空时做长度初始化处理
       * @param elementData 当前用于存放元素的数组对象
       * @param minCapacity 当前数组实际元素个数加 1 过后的参数
       */
      private static int calculateCapacity(Object[] elementData, int minCapacity) {
          // 判断当前存储元素的数组是否为空
          if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 返回 DEFAULT_CAPACITY(10) 和 minCapacity 两个数中大的数,如果初始化是采用无参构造器,
              // 则应当返回10,也就是经过判断之后存储数据的数组需要的容量大小
              return Math.max(DEFAULT_CAPACITY, minCapacity);
          }
          // 如果当前存储元素的数组不为空则直接返回当前数组实际元素个数加 1 过后得到的值
          return minCapacity;
      }
      
    • ensureExplicitCapacity 方法

      /**
       * 按照传递进来的具体值,判断是否需要进行扩容操作
       * @param minCapacity 经过判断之后存储数据的数组需要的容量大小
       */
      private void ensureExplicitCapacity(int minCapacity) {
          // 用于记录数组被修改的次数
          modCount++;
      
          // 如果实际需要的数组长度 - 数组当前的实际长度 > 0,说明该数组是要进行扩容
          if (minCapacity - elementData.length > 0)
              // 核心的扩容方法
              grow(minCapacity);
      }
      
      1. 判断是否需要扩容的核心方法 grow
      /**
       * 具体的扩容方法
       * @param minCapacity 经过判断之后存储数据的数组需要的容量大小
       */
      private void grow(int minCapacity) {
          
          // 获取为扩容之前的数组长度
          int oldCapacity = elementData.length;
          // 扩容之后的新数组长度为旧数组长度的 1.5 倍
          int newCapacity = oldCapacity + (oldCapacity >> 1);
          
          // 新数组长度 - 旧数组的长度 < 0,这种情况发生在创建ArrayList时使用的构造器是默认的无参构造器
          // 所以在第一次添加元素时 elementData.length == 0,
          if (newCapacity - minCapacity < 0)
              // 此时的 minCapacity 是在执行 calculateCapacity 方法时得到的数值10
              newCapacity = minCapacity;
          
          // 当扩容之后的数组长度大于最大的长度时
          if (newCapacity - MAX_ARRAY_SIZE > 0)
              newCapacity = hugeCapacity(minCapacity);
          
          // 使用数组的 copyOf 对数组进行扩容
          elementData = Arrays.copyOf(elementData, newCapacity);
      }
      
      • 变量说明:

        // 整数类型的最大值减8
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        
      1. 当数组要求的扩容的长度过设置的最大值时
      /**
       * 具体的扩容方法
       * @param minCapacity 经过判断之后存储数据的数组需要的容量大小
       */
      private static int hugeCapacity(int minCapacity) {
      	// 是否已经内存溢出
          if (minCapacity < 0) 
              throw new OutOfMemoryError();
          
          // 判断实际需要的数组长度是否大于设置的最大值,是则返回最大值整数类型的最大值,否则返回设置的最大值
          return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
      }
      
  • 相关阅读:
    Medium | LeetCode 179. 最大数 | 排序
    Medium | LeetCode 448. 找到所有数组中消失的数字 | 原地Hash
    Medium | LeetCode 78. 子集 | 回溯
    Medium | LeetCode 39. 组合总和 | 回溯
    js上传头像进行图片压缩
    js 点击按钮复制文本
    eclipse下载
    gulp打包修改配置文件
    vuejs之过滤器(filters)的使用
    关于layui弹出层关闭之后,弹出的dom显示问题
  • 原文地址:https://www.cnblogs.com/wufuqin/p/14966992.html
Copyright © 2020-2023  润新知