主要变量一览
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
上面这些参数的含义基本上,大家都晓得了,这里还是大概的说一下:
- DEFAULT_CAPACITY:默认的初始化的数组的大小,为10
- EMPTY_ELEMENTDATA:空数组,存储的数组没有被复制,那么就会调用这个。
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA:和EMPTY_ELEMENTDATA区别开。主要是使用的时候扩容的策略不同。这里我们先跳过,后面单独说扩容的策略到时候就会说到了。
- elementData:这个最重要了,存储的数据基本数组就是这个了。
- size:当前存在数组中有多少的元素,我们一般都是看这个。
为什么存储数据的数组 elementData
要用 transient
来修饰?
我像是为了节约空间,因为我们的数组在大部分的情况下是不会被填满的,一般一满就扩容,所以如果在序列化的时候,直接就用elementData
来扩容,本质上是浪费空间的。我们从下面的代码也可以看出来:
/**
* Reconstitutes the {@code ArrayList} instance from a stream (that is,
* deserializes it).
* @param s the stream
* @throws ClassNotFoundException if the class of a serialized object
* could not be found
* @throws java.io.IOException if an I/O error occurs
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// like clone(), allocate array based upon size not capacity
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
Object[] elements = new Object[size];// 壹
// Read in all elements in the proper order.
for (int i = 0; i < size; i++) {
elements[i] = s.readObject();
}
elementData = elements;
} else if (size == 0) {
elementData = EMPTY_ELEMENTDATA;
} else {
throw new java.io.InvalidObjectException("Invalid size: " + size);
}
}
在壹中,这个是按照size的大小来从新塑造了一个数组,然后进行序列化,其实是节约了空间的。
查找
这个最简单,基本逻辑上就是遍历,然后匹配,但是在匹配上,有讲究。
public int indexOf(Object o) {
return indexOfRange(o, 0, size);
}
int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
if (o == null) {
for (int i = start; i < end; i++) {
if (es[i] == null) {
return i;
}
}
} else {
for (int i = start; i < end; i++) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
我们要确定是这查找的值是否为null
,如果为null,就 ==
来比较,这个一般比较的是引用;而如果不是null
,那么走的就是equals
的逻辑,一般来说,我们常用的String,Integer(这种基本类型的包装类)都是有重写equals
的逻辑的,不是比较引用,而是比较值。
增加
这段是主要的add的代码逻辑,首先我们要知道在 数组 的某个位置上增加一个元素的逻辑就是,首先把那个元素之后的所有元素给搬运到后一个位置,然后 把index位置上的元素赋值给需要被增加的元素。
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
// 当前的存储数据的数组是否已满
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
// 搬运元素
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
其实这段代码就是告诉我们,在index的位置上插入一个元素,主要是就是先确认传入的index是否是合理的,如果合理,再判断,当前的存储的数据的数组是不是已经满了,满了就扩容,然后复制元素到新的数组,然在新的数组里再插入。
扩容操作
我们一般情况下是不会在使用开始的就确切的知道这次数据量的大小的,所以当存储数据的数据结构满的时候,就要进行扩容。
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return minCapacity;
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个参数就在这里发生了作用了,当然,这个我们还要结合构造函数来看看,构造函数有三个,但是因为第三个用集合来构造我们不用考虑,所以我们来看看以下两个构造函数代码:
/**
* Constructs an empty list with the specified initial capacity.
*
*/
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);
}
}
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
如果是无参构造,那么elementData就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
如果有参构造并且initialCapacity=0,那么elementData就是EMPTY_ELEMENTDATA。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
扩容的时候,如果elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
就从DEFAULT_CAPACITY和参数minCapacity之中选个最大的值开始扩,也就是这次扩容,最小也是DEFAULT_CAPACITY。
打个比方,如果我们直接无参初始化:
ArrayList<Integer> list = new ArrayList<>();
那么我们发生第一次add操作的时候要进行grow,grow(扩容)出来的数组大小就是10了;我们再进行add操作,就不用grow(扩容)操作了。
但是如果我们进行有参初始化;
ArrayList<Integer> list = new ArrayList<>(0);
那么我们第一次add操作的时候,也要进行grow,grow(扩容)出来的数组此时为1;我们再进行add操作,还要grow(扩容)操作。
why? 其实这里我猜测是这样的:为了优化空间。我们用指定大小的初始化对象的方式就是为了提升效率,节约空间。如果我们压根没有10的元素要放到list中,那么默认的初始化就无形之中浪费了空间,这很不极客。所以,我猜测这种区别无参和带参数的扩容机制是为了节省空间的。
以上就可以看出,其实这里代表的有参初始化
和无参初始化
的扩容策略的不同。
删除操作
删除index位置上元素的基本逻辑就是:把index位置后面的元素全都往前挪动一个位置,这样就可以把index位置上的值覆盖,这就是删除了。
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*/
public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
/**
* Private remove method that skips bounds checking and does not
* 主要的删除的逻辑就是这一段
*/
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}
以上的删除的逻辑就是把,通过System.arraycopy(es, i + 1, es, i, newSize - i);
,把后一个给搬到前一个,然后,最后那个赋值为null主要是方便JVM的垃圾回收器的垃圾回收。
内置迭代器
理解这还是蛮重要的,因为涉及一些和集合类的使用的规范的问题。
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;
// prevent creating a synthetic constructor
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();
}
}
阿里的规范中明确的提到,不要在foreach的操作中,修改数据结构,这里指的是删除或者增加操作,我们这里举个例子
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
for (Integer item : list) {
list.remove(item);
System.out.println(item);
}
}
编译后,再反编译后的代码,编译工具jad
public static void main(String args[])
{
ArrayList arraylist = new ArrayList(Arrays.asList(new Integer[] {
Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5)
}));
Integer integer;
for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(integer))
{
integer = (Integer)iterator.next();
arraylist.remove(integer);// (1)
}
}
这foreach用的是,会被编译成迭代器,用的还是ArrayList的内置的迭代器。但是问题出现在(1)的位置,这里用的remove的方法,这方法会改变modcount,但是不会改变expectedModCount,这就会在
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这段代码里报错,这里的报错主要是为了实现一个叫做fail-fast机制。
之所以有这个机制,主要是为了防止出现两个线程,一个迭代,一个修改的情况。这种情况是不允许的,比如迭代的想找某个程序,结果被修改的给删了,倒是接下来的步骤出错。
所以,你如果想要实现删除,那么就不要用foreach的方式来删除,还是老老实实的用迭代器iterator.remove()
来删除吧。