Java 集合框架(3)---- List 相关类解析(下)

前言

在上篇文章中,我们主要看了 AbstractCollection 抽象类 List 接口下的 AbstractList 抽象类,介绍了他们实现了 Collection 中一些抽象方法。在这篇文章中,我们来看一下 List 接口下的一些具体类,也就是我们平常经常使用的一些类:

ArrayList

这个类算的上是我们平常开发中最常用的类之一了。翻译过来意思是 数组列表 ,不过比起这个名称,我更喜欢叫它 动态数组(受 C++ STL 模板的 vector 模板类的影响)。不过不管怎么叫它,它的功能不会遍,我们经常会用它作为动态管理数组元素的集合类。
我们先来看一下它的类继承图:

ArrayList 类的继承结构图

我们可以看到,ArrayList 类继承于 AbstractList 抽象类,这个抽象类我们在上篇文章中已经仔细介绍过了,它继承于 AbstractCollection 抽象类,实现了 List 接口,并且实现了一些 AbstractCollection 接口没有实现的抽象方法(size()iterator() 等方法)。官方文档对它的描述是:该类提供了 List 接口的骨架实现,以最大限度地减少实现由 “随机访问” 数据存储(如数组)所支持的接口所需的工作量。对于顺序访问数据(如链接列表),应该优先使用 AbstractSequentialList 类 。ArrayList 类本身就是表示一种线性结构的类,那么继承于 AbstractList 类也是理所当然。此外,ArrayList 类还实现了 SerializableRandomAccessCloneable 接口。其中 Serializable 接口是用于将对象序列化以储存在文件中或者通过流的形式在网络中传输的接口,RandomAccess 接口是一个没有声明任何方法的空接口,cloneable 接口是一个对象复写 Object 类中 clone() 方法必须实现的接口,它也是一个没有声明任何方法的空接口,但是它却是一个很重要的接口。我们知道 Object 类对象的 clone() 方法用于生成一个和这个对象的完全相同的拷贝对象,但是调用一个对象的 clone() 方法的前提是这个对象的类必须实现 Cloneable 接口,否则的话调用者就会得到一个 CloneNotSupportedException 异常,有兴趣小伙伴们去做个小实验就明白了。

关于 ArrayList 提供的一些方法相信你已经不陌生了,其提供的大多数方法都是 AbstractList 类中声明的,下面我们从源码的角度上来看其中的一些方法细节:

先从构造方法开始:

/**
 * 指定数组容量的构造方法
 */
public ArrayList(int initialCapacity) {
   
        if (initialCapacity > 0) {
   
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
   
            // 将保存元素的数组指向一个默认为空的数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
   
            // 指定的数组容量小于 0 ,抛出一个异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     * 构造一个初始容量为 10 的空数组列表
     */
    public ArrayList() {
   
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个包含了参数指定的集合中包含的所有元素的数组列表
     */
    public ArrayList(Collection<? extends E> c) {
   
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
   
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
   
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

我们在其中看到了一些对象的成员变量,我们来看看它们在类中的声明:

   /**
     * 默认的数组初始容量为 10 
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 代表没有任何元素的空数组实例对象
     */
    private static final Object[] EMPTY_ELEMENTDATA = {
   };

    /**
     * 默认构造方法中赋值给 elementData 引用的数组对象,是一个空元素数组
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
   };

    /**
     * 储存当前 ArrayList 对象包含的元素数组,当第一个元素通过 add 方法添到数组中时,
     * 如果当前的 elementData 引用指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA ,
     * 即如果 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,
     * 这个 elementData 数组将会被扩展至具有默认容量(10)的数组
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 记录数组列表中元素的个数
     */
    private int size;

看到这里可能有些小伙伴们会问了:不是说 ArrayList 类默认的构造方法会构造出一个容量为 10 的数组吗,为什么在 ArrayList 类默认的构造函数中只看到了一句 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 的代码,并且在后面的代码中也显示出这个 DEFAULTCAPACITY_EMPTY_ELEMENTDATA; 是一个容量为 0的空数组啊。难道官方说明出错了?

其实并不是这样的,在 elementData 字段上的注释中解释的很清楚:当第一个元素通过 add 方法添加到当前 ArrayList 对象中时,如果 elementData 字段和 ``DEFAULTCAPACITY_EMPTY_ELEMENTDATA相等时,elementData会被扩展至具有默认容量的数组。好了,这么说的还是有点虚,我们不妨来看看ArrayList类的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(int ) 方法,我们继续跟进:

private void ensureCapacityInternal(int minCapacity) {
   
    // 当 elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的时候,
    // 将数组容量设置为 DEFAULT_CAPACITY 和参数 minCapacity 中较大的一个
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
   
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

在出现了 elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA 的相等性比较之后,方法又调用了 ensureExplicitCapacity(int ) 方法,我们还是继续跟进:

private void ensureExplicitCapacity(int minCapacity) {
   
    // 该字段定义在 AbstractList 中,定义代码为:
    // protected transient int modCount = 0;
    // 代表了列表元素的更改次数,此时明显这个值要加 1
    modCount++;
    // overflow-conscious code
    // 如果要求的最小容量大于当前元素数组的长度,那么进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

在这里调用了 grow(int ) 方法来进行扩容,还是继续看一下 grow(int )方法吧:

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
   
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 可以看到:新的数组容量为前一次数组容量的 1.5 倍,
    // 即每次储存元素的数组容量扩大的倍数为 1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果计算出扩容后的容量小于参数指定的容量,那么将容量调整为参数指定的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果计算出扩容后的容量大于允许分配的最大容量值,那么进行溢出判断处理,
    // MAX_ARRAY_SIZE 为 AbstractList 中定义的一个字段,代码:
    // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    // 进行扩容处理
    elementData = Arrays.copyOf(elementData, newCapacity);
}

这里通过 hugeCapacity(int ) 方法来进行判断溢出,我们来看看这个方法:

private static int hugeCapacity(int minCapacity) {
   
    // int 类型为 32 位有符号整数,并且计算机内部通过补码来保存数字,
    // 最高位为符号位,如果为 0,代表为正数,如果为 1,代表为负数。
    // 如果 minCapacity 发生溢出,那么其最高位必定为 1 ,
    // 整个数字就是一个负数,此时抛出 OutOfMemoryError 异常。
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
        
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

关于计算机中数字的表示方法这里再解释一下:我们知道,计算机通过二进制补码来表示整数,对于有符号的整数,用其最高位的那一位来表示当前数字的正负,如果最高位为 0,那么这个数为正数,如果最高位为 1,那么这个数为负数,这里举个例子:

int a = 0b11000000000000000000000000000000; // 11 后面跟 30 个 0
System.out.println(a);
System.out.print(-(1 << 30));

此时 a 的值是多少呢?按照我们之前的理论:此时 a 的最高位为 1 ,那么就是一个负数,第二个 1 后面跟了 30 个 0,那么 a 的值应该是 -2^30 ,后面的那个输出我将1 向左移 30 位在取相反数,那么此时两个结果应该相同。事实真的如此吗,我们来看看结果:

这里写图片描述

我们看到确实是这样的。其实关于补码还有一点特殊的规则,比如 0 和对应数据类型的最大负值是怎么表示的,有兴趣的小伙伴可以自己查阅一些资料。

我们回到上面的 grow(int ) 方法中来,在调用了 hugeCapacity(int ) 方法之后,会调用 Array.copyOf 方法来进行扩容处理,我们继续跟进:

@SuppressWarnings("unchecked")
public static <T> T[] copyOf
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值