我们在学习这一块内容时需要注意的一个问题是 集合中存放的依然是对象的引用而不是对象本身。
List接口扩展了Collection并声明存储一系列元素的类集的特性。使用一个基于零的下标,元素可以通过它们在列表中的位置被插入和访问。一个列表可以包含重复元素。List在集合中是一个比较重要的知识点也是在开发中最常用的。
我们都知道ArrayList是由数组实现的,但是和数组有很大区别的是随着向ArrayList中不断添加元素,其容量也自动增长,而数组声明好之后其容量就不会改变。想要探明其中的究竟探析其中的原理十分重要,今天重新看了一下这块的源代码(JDK1.8.0_92)感觉很有收获,所以在此记录和分享。
1.Arraylist类中的属性
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L; /**
*默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10; /**
*被用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {}; /**
* Object[]类型的数组,保存了添加到ArrayList中的元素。ArrayList的容量是该Object[]类型数组的长度
* 当第一个元素被添加时,任何空ArrayList中的elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA将会被
* 扩充到DEFAULT_CAPACITY(默认容量)。
*/
private transient Object[] elementData; /**
* ArrayList的大小(其实就是size()方法返回的那个值)
*
* @serial
*/
private int size; ...... }
类属性
在这里需要注意的有几点:
DEFAULT_CAPACITY 这个变量指的是ArrayList默认的容量,其实刚刚初始化一个Arraylist其容量是0,当添加一个之后容量就变成了10,在jdk1.6版本的时候还不是这么处理的。接下来会一一介绍。
elementData 这个变量是一个数组,在JDK1.8.0_92的源代码的注解中很清晰的说明了这个数组是用来缓存ArrayList里的数据(这里的数据指的是对象的引用),ArrayList的大小取决于这个缓存数组的长度,还指明了一点就是在初始化的时候这个缓存数组的是一个空数组当第一次添加的时候会把这个缓存数组的长度扩展为上面的DEFAULT_CAPACITY也就是10。
size 这个变量就指的是缓存数组的大小也就是ArrayList的长度。
2.构造方法
//带参数的构造方法 参数时ArrayList的初始长度
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
//不带参数的构造方法(初始化的长度为0) 也是我们最常用的构造方法
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
//带参数的构造方法 构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回的顺序排列的
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
在这里需要注意的是,在不同版本的jdk中此处的实现机制略有不同。如下:
在jdk1.8.0_45中不带参数的构造方法:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
}
在JDK1.6中不带参数的构造方法:
public ArrayList() {
this(10); //public ArrayList(int initialCapacity)中this.elementData = new Object[initialCapacity];
}
在JDK1.8.0_92和jdk1.8.0_45中代码虽然略微有些不同但是他们的实现机制是一样的,都是刚开始的声明一个空数组是在添加的时候才把数组的长度扩展为10,而在JDK1.6时在刚刚声明的时候就声明的长度为10的数组。这也是慢慢在做优化吧。
具体是在什么时候数组长度进行扩展的我们在下边看
3.添加元素
//将指定的元素(E e)添加到此列表的尾部
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
} //将指定的元素(E e)插入到列表的指定位置(index)
public void add(int index, E element) {
rangeCheckForAdd(index); //判断参数index是否IndexOutOfBoundsException ensureCapacityInternal(size + 1); // Increments modCount!! 如果数组长度不足,将进行扩容
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //将源数组中从index位置开始后的size-index个元素统一后移一位
elementData[index] = element;
size++;
} /**
* 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
//将数组a[0,...,numNew-1]复制到数组elementData[size,...,size+numNew-1]
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
} /**
* 从指定的位置开始,将指定collection中的所有元素插入到此列表中,新元素的顺序为指定collection的迭代器所返回的元素顺序
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index); //判断参数index是否IndexOutOfBoundsException Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount int numMoved = size - index;
if (numMoved > 0)
//先将数组elementData[index,...,index+numMoved-1]复制到elementData[index+numMoved,...,index+2*numMoved-1]
//即,将源数组中从index位置开始的后numMoved个元素统一后移numNew位
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//再将数组a[0,...,numNew-1]复制到数组elementData[index,...,index+numNew-1]
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
上面几个添加方法中具体的实现方法没有在上边列出来我放在下边
具体的实现方法:
/**
* public方法,让用户能手动设置ArrayList的容量
* @param minCapacity 期望的最小容量
*/
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);
}
} private void ensureCapacityInternal(int minCapacity) {
//当elementData为空时,ArrayList的初始容量最小为DEFAULT_CAPACITY(10)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
} private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
} //数组可被分配的最大容量;当需要的数组尺寸超过VM的限制时,可能导致OutOfMemoryError
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /**
* 增加数组的容量,确保它至少能容纳指定的最小容量的元素量
* @param minCapacity 期望的最小容量
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//注意此处扩充capacity的方式是将其向右移一位再加上原来的数,实际上是扩充了1.5倍
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;
}
在这里值得注意的是grow()方法中的>> 1,其实该运算就是相当于除以2.例如:
1010 十进制:10 原始数 number
10100 十进制:20 左移一位 number = number << 1;
1010 十进制:10 右移一位 number = number >> 1;
所以我们跟进 add(E e)方法就会知道当对ArrayList进行add操作时,最开始的时候进入add()方法,然后执行ensureCapacityInternal()方法,继续跟进在这里elementData == EMPTY_ELEMENTDATA为true所以minCapacity = 10。然后minCapacity - elementData.length > 0为true,执行grow()方法,每次执行grow()方法在一般情况下都会生成一个新数组且长度是原数组长度的1.5倍,但是有两种情况除外第一点就是当新生成的数组长度小于10时 那么将新生成的数组长度改为10,也就是grow()方法第一个if语句里的代码做的事情(就是保证了在添加元素小于8个的时候ArrayList的长度都是10),第二点就是当新生成的数组长度大于Integer.MAX_VALUE - 8时会对数组长度进行调整避免越界(这就是grow()方法第二个if语句里的代码做的事情)。这就是ArrayList容量自动扩充的基本原理。
我之前看过jdk1.6在此处的实现方式,略有不同但是也是生成新数组且长度是原数组的1.5倍。至于为什么是1.5倍我觉得可能是那些大牛以自己的经验或是经过科学的推导才得到这么一个最优解,我也不知道了 ,猜的 欢迎补充。
就介绍这么多吧,ArrayList里还有很多方法像删除元素,修改元素还有查找元素等等,就不在这里都写出来了。
我们大致了解了ArrayList的基本原理然后再去想ArrayList的特点就比较容易懂了。相对于LinkedList,ArrayList比较适用于搜索操作,LinkedList比较适用于插入或是删除操作。因为ArrayList适用数组实现的,在内存中所存储的数据是连续的所以很容易从一个元素定位到另一个元素,而LinkedList就不同了,LinkedList适用双链表实现的他就必须挨个一个一个的看是不是要找的元素,但是LinkedList在执行中间插入或是中间删除操作时效率是很高的。
还有在eclipse中查看源代码比较常用的快捷键是Alt + 左右方向键,可以实现在之前鼠标光标处跳转 非常实用。
欢迎大家查补缺漏!