集合之ArrayList

2021-07-23  本文已影响0人  风月寒
构造函数

有有参构造和无参构造两种。

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() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

从上面可知,当initialCapacity大于0的时候,直接初始化一个大小为initialCapacity的Object数组。initialCapacity为0的时候,则将空的Object数组赋值给elementData。

在这里需要区别的是,数组的长度和list的长度,在面试的时候,问过一个这样的问题,就是在有参初始化list的时候,数组的长度有没有变化?list的长度有没有变化?

ArrayList<Integer> list = new ArrayList(10);
System.out.println(list.size());
list.set(2,1)

第一行返回的是0,第二行则抛出IndexOutOfBoundsException异常。所以在初始化ArrayList的时候,数组的长度会改变,但是list的长度返回的是size。只有当add()或者remove()的时候size才会变化。

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

public void add(int index, E element) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
    elementData[index] = element;
    size++;
}

在add方法中,会先调用ensureCapacityInternal()。

private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

会与DEFAULT_CAPACITY先进行比较,取最大值,然后紧接着调用ensureExplicitCapacity更新modCount的值,并判断是否需要扩容。

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

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

在ensureExplicitCapacity()主要是判断是否进行扩容处理,当传入的是数据大于数据的长度的时候,则需要扩容。

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //更新为旧容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //检查新容量的大小是否小于最小需要容量,如果小于那旧将最小容量最为数组的新容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        ////如果新容量大于MAX_ARRAY_SIZE,使用hugeCapacity比较二者
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 将原数组中的元素拷贝
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

下面来看看hugeCapacity();

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //对minCapacity和MAX_ARRAY_SIZE进行比较
    //若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
    //若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
    //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

这里需要注意MAX_ARRAY_SIZE,有些虚拟机会在数组中保存 header words 头部字。当虚拟机大于 MAX_ARRAY_SIZE (Integer.MAX -8 )就容易OOM。

所有有这个判断的目的是尽量减小OOM。

进行一系列处理之后,然后将添加的值放入到对应的位置,并size++;

add(int index, E element)与add(E element)最大的区别是add(int index, E element)需要进行数组的拷贝。调用的是arraycopy()

System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);

ArrayList所谓的查询快增删慢就是体现在这里。

remove()
public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) elementData[index];

        int numMoved = size - index - 1;
        //如果这个值大于0 说明后续有元素需要左移(size=index+1)
        //如果是0说明被移除的对象就是最后一位元素(不需要移动别的元素)
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

先进行size检测,并对modCount进行修改,然后也是进行数组的拷贝处理。

另外还有一个remove(Object o)方法,区别就是先根据值找到对应的index,然后和remove(int index)是一摸一样的。

但是在我们平常工作中,遍历删除元素对于新手可能会出现一些问题。下面我们进行细讲。

ArrayList遍历时删除元素总结有下面几种方式。

1、 普通for循环正序删除(结果:会漏掉元素判断)

2、 普通for循环倒序删除(结果:正确删除)

3、 for-each循环删除(结果:抛出异常)

4、 Iterator遍历,使用ArrayList.remove()删除元素(结果:抛出异常)

5、 Iterator遍历,使用Iterator的remove删除元素(结果:正确删除)

普通for循环正序删除
原ArrayList是[4, 5, 6, 7, 7, 5]

for (int i = 0; i < arrayList.size(); i++) {
    if (arrayList.get(i) == 7) {
        arrayList.remove(i);
        //解决方案: 加一行代码i = i - 1; 删除元素后,下标减1
    }
    System.out.println("当前arrayList是"+arrayList.toString());
}

返回的结果为[4,5,6,7,5]

可以看到与我们预期的结果不一致,因为在最删除的时候,我们知道,当前数字后面的数的位置都会往前移,第二个7的位置会移到第一个7的位置。为当下一轮循环的时候,i++了,对应位置上是5了,所以会出想上面的情况。

解决办法:在删除之后,i--操作。

for-each循环删除
for (Integer value : arrayList) {
        if (value.equals(7)) {
            arrayList.remove(value);
        }
    System.out.println("当前arrayList是"+arrayList.toString());
    }
    
    
结果 Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.test.ArrayListTest1.removeWayThree(ArrayListTest1.java:50)
    at com.test.ArrayListTest1.main(ArrayListTest1.java:24)

会抛出ConcurrentModificationException异常,主要在于for-each的底层实现是使用ArrayList.iterator的hasNext()方法和next()方法实现的,

public E next() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            int i = cursor;
            if (i >= limit)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

调用next()的时候,会进行判断,modCount如果不等于expectedModCount,则会报上面的那个异常。expectedModCount是一开始酒精性赋值,相当于是一个固定值,不会改变。而我们进行remove操作的时候,modCount会增加,所以导致两个值不一致,所以报异常了。

Iterator遍历,使用ArrayList.remove()删除元素这种方式包异常也是跟上面一摸一样的原因。

Iterator遍历,使用Iterator的remove删除元素
Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
    Integer value = iterator.next();
    if (value.equals(3)) {//3是需要删除的元素
        iterator.remove();
    }
}


当调用iterator.remove()时,调用的是Itr的remove

public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

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

从上面可以看到,对expectedModCount进行的重新赋值,所以就不行报异常。

Arrays.asList
List<Integer> statusList = Arrays.asList(1, 2);
statusList.add(3);
System.out.println(statusList.contains(3));

预期的结果,应该是输出true,但是实际却是抛出了java.lang.UnsupportedOperationException异常:

因为asList 返回的ArrayList却是Arrays类的内部类,它也继承了AbstractList类,重写了很多方法,比如我们上面使用的contains方法,但是却没有重写add方法,所以我们在调用add方法时才会抛出java.lang.UnsupportedOperationException异常。

subList

1、修改原集合元素的值,会影响子集合

2、修改原集合的结构(指的是添加和删除元素操作),会引起ConcurrentModificationException异常

3、修改子集合元素的值,会影响原集合

4、修改子集合的结构,会影响原集合

上一篇 下一篇

猜你喜欢

热点阅读