Loading

ArrayList使用及原理

一、前言

集合类是面试中经常会被问到,今天带大家分析一下最常用的集合类之一ArrayList类,希望对大家有所帮助。

ArrayList属于Collection集合类大家族的一员,是分支List中的主力军之一。ArrayList使用非常广泛,无论是在数据库表中查询,还是网络信息爬取都需要使用,所以了解ArrayList的原理就十分重要了(本文ArrayList版本基于JDK 1.8)。

二、ArrrayList的继承关系

通过IDEA生成ArrayList的继承关系图,可以清晰的看出ArrayList的继承关系。入下图。

 

 

 三、定义ArrayList

ArrayList有三个构造方法:

  1. 无参构造方法
  2. 参数为整数的构造方法
  3. 参数为集合的构造方法

3.1 ArrayList的属性

首先,我们先看一下ArrayList类定义的几个属性。

/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

   
View Code

 可以看到,DEFAULT_CAPACIT属性定义ArrayList的默认容量是10。

ArrayList定义了两个空实例 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA 用于空实例的共享空数组实例。DEFAULTCAPACITY_EMPTY_ELEMENTDATA用于默认大小的空实例。 我们将此与EMPTY_ELEMENTDATA区别开来,用于添加第一个元素的时候知道扩容多少。

elementData属性是一个Object数组,用于存储添加的元素。transient关键字标识elementData不能被序列化。

size属性标识,当前Arraylist的长度。

3.2 ArrayList的构造方法

1、参数为整数的构造方法

 1 public ArrayList(int initialCapacity) {
 2         if (initialCapacity > 0) {
 3             this.elementData = new Object[initialCapacity];
 4         } else if (initialCapacity == 0) {
 5             this.elementData = EMPTY_ELEMENTDATA;
 6         } else {
 7             throw new IllegalArgumentException("Illegal Capacity: "+
 8                                                initialCapacity);
 9         }
10     }

很容易可以看出,当参数为正数时,初始化一个长度为传入参数的数组;参数为0时,初始化一个长度为默认长度的数组,否则就抛出一个非法参数异常。

2、无参构造方法

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

无参构造方法只是将elementData指向了一个空数组。

当然,我们能够调用ArrayList提供的扩容方法来扩充ArrayList的容量

public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
}

可以看出,若当前数组是空时,最小扩容为10,否则扩容传入的正整数。然后调用ensureExplicitCapacity方法

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

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

该方法主要调用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);
}
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
 

该方法首先算出当前容量的1.5倍小于传入的容量,如果是则将传入的参数作为扩容大小,否则,扩容到当前容量的1.5倍。如果参数大于数组最大值,则扩容到ArrayList最大值,否则扩容到数组最大值。实际上MAX_ARRAY_SIZE与Integer.MAX_VALUE相差8。再来看一下是怎么扩容的

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
 }

看到了吧,就是实例化了一个对应类的数组而已。

3、参数为集合的构造方法

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

该构造函数首先将如参转化为数组赋值给elementData,将elementData指向新copy的一个数组对象,copyof方法就是上文描述的。

四、ArrayList的使用

前面我们知道了改怎么实例化一个ArrayList对象,接下来我们讲讲该怎么使用使用ArrayList了。ArrayList给我们提供了很多方法,经常使用的有add,addAll,set,get,remove,size,isEmpty等;

接下来我们举个例子来说明一下这些方法的使用。

4.1 add方法

首先我们先添加几种我最爱吃的几种水果

public void testArrayList() {
        ArrayList<String> list = new ArrayList<>();
        list.add("苹果");
        list.add("香蕉");
        list.add("草莓");
        list.add("水蜜桃");
        list.add("菠萝");
        list.add("葡萄");
 }

好奇add方法做了什么吗,那么接着往下看

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

可以从代码中看出首先执行了ensureCapacityInternal方法,然后想elementData里面添加一个值,也就是,我们添加的值都被放在了elementData数组里,这跟之前所说的一致。接下来再看看ensureCapacityInternal方法。

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

我将这几个相关的方法复制过来,首先看一下calculateCapacity方法,通过上面的构造函数我们发现if判断为true,取一个最大值。我们这个时候最大值就应该是默认值了,也就是10。然后调用ensureExplicitCapacity,是不是很眼熟呀,这不就是之前所将的扩容吗。初始化了一个长度为10的数组。其实,之前版本ArrayList的new ArrayList()是会初始化一个长度为10的数组的,之所以移除,可能是考虑到节省空间。目前的设计体现了一种懒加载的思想,当用的时候再去分配空间。

那么,如果我们再向list里添加水果名称会发生什么呢?当我们再添加时 ensureExplicitCapacity 方法的if条件是false,不会再分配空间。但,当我们填第10个的时候,我们当前的对象就装满,怎么办呢,当然要换个大一点的来装呀。这时执行grow方法进行扩容。通过上面的grow方法我们知道,grow会准备一个长度为15的对象来装我们的水果,这样就可以继续装了。为什么会分配当前长度的1.5倍的容量呢?考虑一下,如果每次只分配比当前长度多一个会发生什么呢?对了,以后再添加就会继续分配空间,扩容可不是一个快的操作,会减慢add的执行速度。所以我就多分配点给你用,避免反复扩容。但也不能分配的太大,造成空间浪费,因此才制定了这个游戏规则。

我们还能够通过add(index, element)方法在index前添加一个元素,如下

public void testArrayList() {
        ArrayList<String> list = new ArrayList<>();
        list.add("苹果");
        list.add("香蕉");
        list.add("草莓");
        list.add("水蜜桃");
        list.add("菠萝");
        list.add("葡萄");
        list.add("香蕉");
        list.add(0, "香蕉");
        System.out.println(list.toString());
}

输出:[香蕉, 苹果, 香蕉, 草莓, 水蜜桃, 菠萝, 葡萄, 香蕉]

这里会有一个大家很容易出现的错误,如果我们执行下面代码会发生什么呢?ArrayList还会自动分配空间吗?

public void testArrayList() {
        ArrayList<String> list = new ArrayList<>();
        list.add(1, "葡萄");
}

答案是不会了这样做会抛出一个数组越界的异常。那我们自己分配空间呢,如下代码,设置一个长为10的数组。

public void testArrayList() {
        ArrayList<String> list = new ArrayList<>(10);
        list.add(1, "葡萄");
}

结果会怎么样呢?运行一下,竟然还会抛出异常。让我们看看怎么回事吧

public void add(int index, E element) {
        rangeCheckForAdd(index);

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


private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

结果显而易见,你的异常就是这么抛出来的。虽然ArrayList是对数组的封装,但是这和数组的用法上还是有点区别的。

4.2、set方法

set方法能够修改指定位置的值,测试代码如下:

public void testArrayList() {
        ArrayList<String> list = new ArrayList<>(10);
        list.add("葡萄");
        list.add("苹果");
        list.set(0, "香蕉");
        System.out.println(list.toString());
 }

返回结果为:[香蕉, 苹果],再来看一下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));
}

我们可以看到set方法就是将某个位置的元素换成传入的值,并将原来的值返回。

4.3 remove(int index)和remove(Object o)

再来看看移除元素的代码

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

其中System.arraycopy(elementData, index+1, elementData, index, numMoved);是用来将后面的元素前移

五、结语

本文主要对ArrayList原理进行介绍,着重介绍了ArrayList的增加、扩容机制、获取元素、修改元素、删除时元素移动的方式。

如果本文对你的学习有帮助,请给一个赞吧,这会是我最大的动力。

 

参考资料:
Java集合 ArrayList原理及使用
https://www.cnblogs.com/LiaHon/p/11089988.html

 

 

 

 

 

 

 

posted @ 2020-04-03 19:00  Charming-Boy  阅读(434)  评论(0编辑  收藏  举报