【Java】20211016_Java 集合 _ArrayList

【Java】20211016_Java集合_ArrayList


从简单数据结构谈起

数组

懂得都懂(doge)

数组.png

ArrayList的底层结构

ArrayList的底层结构是数组(见代码块1-1),首先看ArrayList的初始化构造方法。

[^代码块1-1]

transient Object[] elementData;

ArrayList提供了三种构造方法(见代码块1-2)

[^代码块1-2]

private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
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) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}    
  1. ArrayList的无参构造,将其底层数组赋值为一个空数组。
  2. ArrayList的int型参数构造,传入参数initialCapacity作为底层数组的初始化长度,判断初始化长度小于0则抛出异常,等于0则将底层数组赋值为一个空数组,大于0则以初始化长度动态初始化一个数组。
  3. ArrayList的Collection型参数构造——略。
长度可变揭秘

无论使用哪种构造方式,一个确定的数组经过初始化后,其长度是不会发生改变的,所以我们需要看看ArrayList提供的add方法来探究ArrayList集合长度可变的秘密(见代码块1-3)

[^代码块1-3]

private int size;
public boolean add(E e) {
ensureCapacityInternal(size + 1);  // Increments modCount!!
elementData[size++] = e;
return true;
}

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static final int DEFAULT_CAPACITY = 10;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

protected transient int modCount = 0;
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
   // overflow-conscious code
   int oldCapacity = elementData.length;
   int newCapacity = oldCapacity + (oldCapacity >> 1);
   if (newCapacity - minCapacity < 0)
       newCapacity = minCapacity;
   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);
}

以一个通过无参刚初始化的空集合为例:

  1. ensureCapacityInternal方法——再次确定集合容量。由于刚初始化,此时size为0,ensureCapacityInternal方法实际接收的参数minCapacity为1,在ensureCapacityInternal方法内部调用了calculateCapacity方法。
  2. calculateCapacity方法——计算集合容量。由于在使用无参构造初始化时
    elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    ,所以这个方法直接会返回minCapacity(1)和DEFAULT_CAPACITY(10)中的最大值即10。
  3. ensureExplicitCapacity方法——明确添加元素后的集合容量。在ensureExplicitCapacity方法中,传入的minCapacity是上一步的返回结果10,modCount++后modCount为1,接着在判断条件中,显然(10-0)>0为true,所以下一步要走grow方法。
  4. grow方法——扩容方法。
    // 获取旧容量值
    int oldCapacity = elementData.length;
    // 计算新容量值,oldCapacity >> 1 <==> oldCapacity / 2,所以新容量为旧容量的1.5倍大
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    紧接着是对新容量值做最终判断,如果newCapacity (这里为0)小于传入的minCapacity(这里是10),那么新容量值就取传入的minCapacity,否则就和MAX_ARRAY_SIZE比较,这里的MAX_ARRAY_SIZE非常大,一般不会为true(如果实际工作中你能让扩容走到这一步,联系我我给你发五毛钱安家费),接着就会通过Arrays工具类的copy方法将原来elementData数组中的数据拷贝到一个新的数组中并将新数组地址赋值给elementData完成集合扩容。
  5. 走完上述方法后,执行elementData[size++] = e;才正式将想要添加的元素e添加到集合中。

走完整个扩容方法,可以知道:

  1. 集合扩容的时机——如果一个集合原有长度是10,直到放置第11个元素才会扩容,即在添加超过集合容量的第一个元素时在add方法内部扩容。
  2. 扩容后的容量——原容量的1.5倍

面试题

(其实14号美团面试没有问ArrayList扩容,我也没答,但还是写了这个md文档,主要是ArrayList的扩容机制和HashMap存在不一样的地方,所以要做个记录,以防下次被问到)

正题:List.contains(Object obe)方法是怎么实现的?

内心os:这还要想?你要确定一个集合里有没有这个元素,直接遍历然后去做比较呗?

想是这么想,答也是这么答的,ArrayList在实现上也确实是这么个思路。看看码(见代码块2-1):

[^代码块2-1]

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

直接看indexOf方法,indexOf方法分别谈论了o是否为null的两种情况,因为o为null在调用o.equals()时会报空指针异常。

  1. 如果o是null,就遍历集合通过==来判断集合中是否有null这个特殊元素。
  2. 如果o不是null,就遍历集合通过equals来判断集合中是否存在和o相同的元素。

null在Java中是特殊的,地址值固定的,所以可以通过==来判断,但是对于其他对象,应该调用可以被重写的对象o的equals方法来判断。(其实contains方法我还是回答对了的)

说到equals方法,上一篇写了下面的代码(见代码块2-2):

[^代码块2-2]

Integer a = 1000;
Integer b = 1000;
System.out.println(a == b);

上面的代码最终输出结果为false,实际上在对两个包装类进行比较时应该用equals方法,阿里巴巴代码规约里已经告诉我们了。

阿里代码规约_包装类之间比较用equals.png


努力让自己不那么菜

欲速则不达,每天进步一点点。

(有没有成都的大佬内推我一波)