Java学习——集合(1)ArrayList详解

283 阅读4分钟

Java学习——集合(1)ArrayList详解 —— 悟道


创建 ArrayList

创建 ArrayList 时,一般是直接创建,或者是直接将实现了 Collection 接口的集合类传入进行初始化创建

// 1 直接new
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
System.out.println(Arrays.toString(list.toArray()));

// 2 创建时直接添加元素 实现了collection接口的类
ArrayList<String> list1 = new ArrayList<>(list);
System.out.println(Arrays.toString(list1.toArray()));
HashSet<String> hashSet = new HashSet<>();
hashSet.add("C");
ArrayList<String> list2 = new ArrayList<>(hashSet);
System.out.println(Arrays.toString(list2.toArray()));

添加元素 -> ArrayList 的扩容机制

首先看 ArrayList 的 add 方法,当添加元素时,首先是通过 ensureCapacityInternal 方法进行增量判断,判断后进行赋值以及返回添加元素成功。

public boolean add(E e) {
    // Increments modCount!!这里是增量机制
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

private static final int DEFAULT_CAPACITY = 10;

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

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

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

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

而对于 ensureCapacityInternal 方法来说,首先通过 calculateCapacity 方法来计算容量。如果 elementData (存储数据的数组) 为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,则说明 ArrayList 是无参刚创建的,所以使用默认的容量(10)与实际数量 minCapacity(size + 1) 中最大值。通过 ensureExplicitCapacity 方法验证容量与 elementData 实际长度的大小情况,通过 grow 方法进行扩容。同理 addAll 方法也是同样的机制。只不过多了复制数组的一步。成员变量 modCount 则是后面再说,下面先说 grow 方法是怎么增容的。

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);
}

grow 方法增容详解。首先根据 ArrayList 中元素的数量 + (数量 >> 1)也就大约是容量的1.5倍,所以 newCapacity - minCapacity < 0 这个条件只有空数据第一次添加的时候能成立,让 elementData 初始化为长度10的数组,之后都不成立。在上面的 ensureExplicitCapacity 方法中 minCapacity 是 size + 1,所以只有当 size == elementData.length 时,才会 调用 grow 方法进行扩容,将容量 增扩为 oldCapacity + (oldCapacity >> 1),约是1.5倍。

删除元素 -> ArrayList 删除元素时所需考虑问题

删除方法一般使用 remove 方法,其他较为特殊的方法暂不讨论。

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

通过 index 删除时先进行下标范围检查,然后获取到删除元素,移动原数组。为什么是 size - 1 - index,是因为 index 从0开始。numMoved 不判断=0的情况则是因为=0时,删除元素是末尾元素,直接置 null 就好。elementData[--size] = null 就是末尾元素置 null。其他情况则移动元素通过 object 删除时则涉及到 fastRemove 方法,原理和删除 index 的情况一致。这里判断了 o == null 是因为 o 为 null 时调用 equals 方法时报 NPE,所以要判断。而 equals 方法判断相等时,为了实现现实中的‘相等概念’需要重写 equals 和 hashCode 方法。否则 equals 只是判断是否为同一对象。

ArrayList<String> list = new ArrayList<>();
list.add("张三");
list.add("张三");
list.add("赵四");
list.add("王五");

for (int i = 0; i < list.size(); i++) {
    if ("张三".equals(list.get(i))) {
        list.remove(list.get(i));
        System.out.println("张三已被删除");
    }
}

for (String str : list) {
    if ("张三".equals(str)) {
        list.remove(str);
        System.out.println("张三已被删除1");
    }
}

/*
    张三已被删除
    张三已被删除1
    Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at com.wuhaitao.base.collection.List.ArrayListInfo.main(ArrayListInfo.java:62)
*/

一般情况下,集合删除元素时,我们都是通过遍历去删除元素。所以这里存在一些问题。由于有fail-fast 机制,这个机制意思是:一旦检测到可能会发生错误,就立马抛出异常,程序将不再往下执行。这个机制并不是集合中特有的,只是在集合中很容易实现,在这里介绍很方便。比如上面的例子,看着没有任何问题,但是一旦运行起来就会报错,抛出了 ConcurrentModificationException 异常。并且还调用了911行的 checkForComodification 方法。明明我们只调用了 remove 方法,remove 方法中也没有调用 checkForComodification 方法,那么这些情况是为什么呢?另外,为什么 fori 没有问题的删除了第一个‘张三’,但是为什么后面不再删除了?在 fori 的情况下,当第一个元素被删除时,集合本身元素位置发生变化,删除完第一个‘张三’后:i = 1,但是第二个‘张三’被移动到了 index = 0 的位置,所以下一次 get(i) 时,获取到的是被移动到 index = 1 的‘赵四’第二个‘张三’,因此被巧妙地跳过了。所以,当使用 fori 进行删除时,虽然可以进行删除操作,但存在数据问题。我们接着来看增强 for。

String[] arr = new String[]{"张三", "张三", "赵四", "王五"};
String[] var2 = arr;
int i = arr.length;

for(int var4 = 0; var4 < i; ++var4) {
    String s = var2[var4];
    System.out.println(s);
}

List<Integer> list = new ArrayList();

for(i = 0; i < 5; ++i) {
    list.add(i);
}

Iterator var7 = list.iterator();

while(var7.hasNext()) {
    Integer integer = (Integer)var7.next();
    if ("张三".equals(str)) {
        list.remove(str);
    }
}

增强 for 是一个语法糖。所以在编译代码时,它会被解析成基础代码。如果是数组使用增强 for 则解析过后就是 fori 的形式。如果是实现了 Iterable 的集合,则会解析成 iterator 迭代器的形式遍历集合。所以,在之前 ArrayList 的增强 for 中,会被解析成 iterator 迭代器形式遍历。所以会调用 iterator,hasNext 方法以及 next 方法。但是呢!这里有一个我们非常需要注意的地方,删除方法我们是调用的 ArrayList 的 remove 方法。我们接下来看下 Itr 源码。(注意:泛型也是语法糖,也会被解析成基础代码,但这里不做讲解)

public Iterator<E> iterator() {
    return new Itr();
}

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    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];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

在 ArrayList 实现的 iterator 方法中,返回了内部类 Itr 实例对象。Itr 则实现了 Iterator 接口,所以会重写 hasNext,next 方法。在 hasNext 中判断了指针是否还指向元素。而在 next 方法中掉了用我们前面提到的报错方法 checkForComodification,这个方法抛出了 ConcurrentModificationException 异常。而这个异常是根据 modCount != expectedModCount 这个条件达成的。好,现在我们就要来聊聊这个一开始就提到的 modCount 了。

protected transient int modCount = 0;
// add 方法调用
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// remove 方法调用
private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

首先 modCount 并不在 ArrayList 中定义的,而是在 AbstractList 中定义的,其记录的是集合被修改的次数。所以,在 add、remove 等方法中会见到 modCount++ 这个操作。在‘张三’的例子增强for中,创建 Itr 内部类时 int expectedModCount = modCount,expectedModCount 这个内部类成员变量已被赋值,因为做了四次 add、一次 remove 操作, 所以 expectedModCount = modCount = 5。当增强 for 第一次遍历时,checkForComodification 方法判断成功,删除第二个‘张三’(第一个‘张三’是 fori 中删除)上面说过,增强 for 中调用的是 ArrayList 的删除方法。所以这时 modCount++ 变成了6,所以在下一次删除时,checkForComodification 方法判断失败,所以报了 ConcurrentModificationException 异常。那么我们来看正常的 iterator 迭代器遍历。

List<String> list = new ArrayList<>();
list.add("张三");
list.add("张三");
list.add("赵四");
list.add("王五");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String next = iterator.next();
    if ("张三".equals(next)) {
        iterator.remove();
        System.out.println("张三已被删除1");
    }
}
// itr 的删除源码
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

在迭代器遍历删除时,我们调用的时 Itr 内部类的删除方法,我们明显看到了 expectedModCount = modCount,这也就解释了为什么迭代器遍历不会报错而增强 for 会报错最后,阿里守则中,也明确提到了,不要在 foreach 循环里删除元素。若需删除元素,请使用 Iterator 方式。在并发操作时,需对 Iterator 对象加锁。

查找元素 -> 如果有序,可以二分查找提高效率

如果要正序查找一个元素,可以使用 indexOf 方法;如果要倒序查找一个元素,可以使用 lastIndexOf 方法contains方法可以判断 ArrayList 中是否包含某个元素,内部调用indexOf方法。

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

如果 ArrayList 中的元素是经过排序的,就可以使用二分查找法,效率更快。

ArrayList<String> copy = new ArrayList<>(alist);
copy.add("1");
copy.add("2");
copy.add("2");
copy.add("4");
Collections.sort(copy);
System.out.println(copy);

int index = Collections.binarySearch(copy, "b");
System.out.println(index);

更新元素

来看一下 set() 方法的源码。

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

更新元素时,先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值


(。・∀・)ノ゙嗨!如果这篇文章小小帮助到你,可以请小弟我喝瓶可乐呀。点赞是对我最大的支持与认可,感谢!