ArrayList使用及原理
一、前言
集合类是面试中经常会被问到,今天带大家分析一下最常用的集合类之一ArrayList类,希望对大家有所帮助。
ArrayList属于Collection集合类大家族的一员,是分支List中的主力军之一。ArrayList使用非常广泛,无论是在数据库表中查询,还是网络信息爬取都需要使用,所以了解ArrayList的原理就十分重要了(本文ArrayList版本基于JDK 1.8)。
二、ArrrayList的继承关系
通过IDEA生成ArrayList的继承关系图,可以清晰的看出ArrayList的继承关系。入下图。
三、定义ArrayList
ArrayList有三个构造方法:
- 无参构造方法
- 参数为整数的构造方法
- 参数为集合的构造方法
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;
可以看到,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