说道最常用的数据结构,莫过于ArrayList和LinkedList了,有一点数据结构知识的人都知道一个是变长数组,另一个是链表形式的数组。但是它们究竟是在底层如何运行的呢?我们来看看。
首先我们观察这个类的定义:
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
{
//...
}
啊我们知道了这是AbstractList的子类,然后继承了,List,RandomAccess,Conable,Serializable接口。
这样我们知道,ArrayList是可以随机访问,克隆(复制),序列化的。
public interface RandomAccess {
}
这是RandomAccess接口的定义。
public interface Cloneable {
}
这是Cloneable的。。。
public interface Serializable {
}
这是Serializable的。。。
有人说,这仨接口咋都是空的呢?其实这几个接口都是一个标识符而已,这个标识符表明了这个类是否有这样的能力去克隆,序列化,随机访问。
接下来看看List接口,我们用同样的方法打开,然后发现一堆备注,但是我们只看方法,整理结果如下:
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator iterator();
Object[] toArray();
T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection> c);
boolean addAll(Collection extends E> c);
boolean addAll(int index, Collection extends E> c);
boolean removeAll(Collection> c);
boolean retainAll(Collection> c);
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator listIterator();
ListIterator listIterator(int index);
List subList(int fromIndex, int toIndex);
default void replaceAll(UnaryOperator operator) {
Objects.requireNonNull(operator);
final ListIterator li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
default void sort(Comparator super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
default Spliterator spliterator() {
if (this instanceof RandomAccess) {
return new AbstractList.RandomAccessSpliterator<>(this);
} else {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}
static List of() {
return ImmutableCollections.emptyList();
}
static List of(E e1) {
return new ImmutableCollections.List12<>(e1);
}
static List of(E e1, E e2) {
return new ImmutableCollections.List12<>(e1, e2);
}
static List of(E e1, E e2, E e3) {
return new ImmutableCollections.ListN<>(e1, e2, e3);
}
static List of(E e1, E e2, E e3, E e4) {
return new ImmutableCollections.ListN<>(e1, e2, e3, e4);
}
static List of(E e1, E e2, E e3, E e4, E e5) {
return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5);
}
static List of(E e1, E e2, E e3, E e4, E e5, E e6) {
return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
e6);
}
static List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) {
return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
e6, e7);
}
static List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) {
return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5,
e6, e7, e8);
}
static List copyOf(Collection extends E> coll) {
return ImmutableCollections.listCopy(coll);
}
这List接口里面真的是,一坨方法啊,但是我们按照方法名猜测一波,为了方便,我们一组一组猜测,以备注形式给出:
int size();//获取大小
boolean isEmpty();//是否为空
boolean contains(Object o);//是否包含某个元素
Iterator iterator();//生成一个迭代器
Object[] toArray();//转化为Object数组
T[] toArray(T[] a);//根据传入类型转化为对应类型数组
上面的显然是一些最基本的方法
boolean add(E e);//添加元素
boolean remove(Object o);//移除某个元素,如果移除就返回true
boolean containsAll(Collection> c);//是否包含所有
boolean addAll(Collection extends E> c);//添加所有集合元素
boolean addAll(int index, Collection extends E> c);//从index开始添加所有集合元素
boolean removeAll(Collection> c);//移除所有集合元素
boolean retainAll(Collection> c);//???
void clear();//清除元素
boolean equals(Object o);//是否相等(指两个ArrayList)
boolean equals(Object o);//俩ArrayList是否相符
int hashCode();//返回hashcode
E get(int index);//按下标获取
E set(int index, E element);//按下标set一个元素
void add(int index, E element);
E remove(int index);//按下标删除
int indexOf(Object o);//获取某元素第一次出现下标
int lastIndexOf(Object o);//获取某元素最后一次出现下标
ListIterator listIterator();//与下边的方法都是生成一个List迭代器
ListIterator listIterator(int index);
List subList(int fromIndex, int toIndex);//生成子列表
好了就这么多,其余的都是java8之后添加的一些stream相关的方法,包括一些默认方法。
所以实际上我们只要知道上面的最基本方法就行了。
然后现在看看这些都是咋实现的。
值得一提的是
public abstract class AbstractList extends AbstractCollection implements List {
就是说AbstractList是继承了抽象集合并且实现了List接口。。。虽然不懂为啥,但是说明了AbstractList应该是实现了一部分方法。
接下来从类中搜索这一个个方法。
首先是几个常量,整理一下大概是:
private static final long serialVersionUID = 8683452581122892189L;
//是序列化过程中的版本号uid,一个long数字
private static final int DEFAULT_CAPACITY = 10;
//顾名思义了,默认的容量是10
private static final Object[] EMPTY_ELEMENTDATA = {};
//后面两个未知,我们慢慢往后看
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//这是ArrayList的基础,是不可被序列化的Object数组,这时候还没初始化
transient Object[] elementData;
private int size;
一个私有变量,这还用说嘛?就是元素数量啊,这里需要说一下,元素数量一般是小于等于capacity的。
好的,接下来我们要初始化一个ArrayList了。
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;
}
public ArrayList(Collection extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
我把里面的一些注释去掉了,可以更直观的看到:
1、当你使用的是
new ArrayList<>(1024);
这种形式的,就是调用了第一个方法,如果给的capacity是0,就把之前的final的空数组赋值给元素数组。
小于0那肯定就抛出异常了。(不合法参数异常)
2、当你使用的是
new ArrayList<>();
相信大多数人和我一样都不爱给它设定容量,都想的是先凑合用呗,反正不够他能扩容。
那么调用的是第二个方法。
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
表明此时已经初始化了,但是依然是个空数组!
虽然DEFAULTCAPACITY_EMPTY_ELEMENTDATA看上去和EMPTY_ELEMENTDATA有点区别,但是现在依然是一个空的,长度为0的Object数组。
3、当你使用的是:
Collectioncollection=new ArrayList<>(); new ArrayList<>(collection);
这种形式的,那么调用第三个构造器,那么实际上就会把集合元素复制进来。
看完了“构造”,现在看一看“添加”,毕竟增删改查是每个数据结构的必有特点嘛。
现在,我想调用一个add方法,来为ArrayList增加一个元素。
public boolean add(E e) {
modCount++;//modCount是AbstractList里的数字,记录列表被修改的次数
add(e, elementData, size);//调用下边那个add
return true;//加入元素成功
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();//当发现插入的位置已经达到了元素数组的容量极限length时,扩容。
elementData[s] = e;//简单的插入步骤,并且size+1
size = s + 1;
}
private Object[] grow() {//与下边的调用生成了一个新的elementData,原来的数组复制到新的数组中
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);//新容量是旧的容量的1.5倍
if (newCapacity - minCapacity <= 0) {//当新容量小于等于最小容量进行下面操作
//我们考虑一下什么时候新容量是小于等于最小容量的 答案是新建并且插入第一个元素的时候
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
//返回最开始那个默认Capacity和
//minCapacity更大一点的,一般的话,除非一开始设定cap为1,否则就会变成默认容量10
if (minCapacity < 0) // overflow,溢出了
throw new OutOfMemoryError();
return minCapacity;//如果是不非空的情况,那么会会返回只增加一个长度的
}
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
//最终如果新长度还是小于MAX_ARRAY_SIZE(实际上是最大int-8)返回,否则扩容到最大int值
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
所以总的来说,我们常规操作一般是,new一个默认的arraylist,然后没插入前长度为0,(被一个长度为0的fina数组赋值),然后插入一个元素因为长度不够就扩容,扩容变为10,之后就是15,22 。。。。这样的序列,如果发现扩容的过程中新数组长度即将超过MaxArraySize那么就考虑是不是要直接扩容到MaxInteger这么大的长度2^31。接下来就是正常的放置操作。
接下来我们看看其他的add方法。
public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();//如果index是最后的那个位置,那么就相当于原来的add(element)
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);//复制函数会把index之后的元素往后一个位置复制
elementData[index] = element;
size = s + 1;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
这个比较好理解,相当于是insert了,这个会把插入位置的后面元素统统向后移动一个位置,所以最坏状况是O(n)的
时间复杂度,所以要小心使用!
public boolean addAll(Collection extends E> c) {
Object[] a = c.toArray();
modCount++;
int numNew = a.length;
if (numNew == 0)
return false;
Object[] elementData;
final int s;
if (numNew > (elementData = this.elementData).length - (s = size))
elementData = grow(s + numNew);
System.arraycopy(a, 0, elementData, s, numNew);
size = s + numNew;
return true;
}
public boolean addAll(int index, Collection extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
modCount++;
int numNew = a.length;
if (numNew == 0)
return false;
Object[] elementData;
final int s;
if (numNew > (elementData = this.elementData).length - (s = size))
elementData = grow(s + numNew);
int numMoved = s - index;
if (numMoved > 0)
System.arraycopy(elementData, index,
elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size = s + numNew;
return true;
}
接下来两个addAll就不说了,依然是先检查是否要扩容,然后一段一段的复制,或者是插入式一段段复制。
依然遵循1.5倍扩容和最大MAX_INTEGER的原则。
说完了“增”,现在说说“改“
public E set(int index, E element) {
Objects.checkIndex(index, size);//检查是否下标越界
E oldValue = elementData(index);//获得旧的值
elementData[index] = element;//设为新的值
return oldValue;//返回旧的值
}
”
嘿嘿嘿,不知道吧?set方法的返回值实际上是旧值啊。好吧其实我也是前不久才知道的。。。。
说完了“改”我们看看查是怎么查
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
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;
}
很好,符合预期,就是简单的线性查找,contain方法也是基于indexOf和IndexOfRange来搜索的,不过,被搜索的对象要分为null和 非null两种对象的查找。
接下来,看看ArrayList的删除。
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 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;
}
所以啊,实际上按下标删除元素也是最坏O(n)时间复杂度的,慎重使用!
现在看看组删除,组删除就是一次性删除许多元素。
public boolean removeAll(Collection> c) {
return batchRemove(c, false, 0, size);
}
boolean batchRemove(Collection> c, boolean complement,
final int from, final int end) {
Objects.requireNonNull(c);
final Object[] es = elementData;
int r;
// Optimize for initial run of survivors
for (r = from;; r++) {
if (r == end)
return false;
if (c.contains(es[r]) != complement)
break;
}
int w = r++;
try {
for (Object e; r < end; r++)
if (c.contains(e = es[r]) == complement)
es[w++] = e;
} catch (Throwable ex) {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
System.arraycopy(es, r, es, w, end - r);
w += end - r;
throw ex;
} finally {
modCount += end - w;
shiftTailOverGap(es, w, end);
}
return true;
}
private void shiftTailOverGap(Object[] es, int lo, int hi) {
System.arraycopy(es, hi, es, lo, size - hi);
for (int to = size, i = (size -= hi - lo); i < to; i++)
es[i] = null;
}
其实删除依然是线性搜索,但是我们考虑一下这个算法,它实际上就是在循环的过程中有两个指针r,w。
当发现搜索过程中的元素不是要删除的集合中的元素时,指针w就写一次,就覆盖了那些删除的元素,然后把后面的元素都置为null。
这种算法比较节省空间,但是最坏时间复杂度依然是O(n)。
综上所述,ArrayList的增删改查都已经讲解完毕。
说些题外话,我们看看它如何读写(序列化的)。
首先我们在字段名的地方可以发现一些事情,那就是
transient Object[] elementData;
元素数组是在序列化时被忽略的,那么我们怎么知道反序列化之后就能正确的得到一个ArrayList呢?
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioral compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i
看来,也并不是没有写元素嘛,还是通过for循环来把ArrayList的元素写进输出流中了,而且,defaultWriteObject实际上已经把类的信息写入了。实际上,由于前后检查的modCount导致并发条件下根本无法写入被修改的ArrayList的,所以ArrayList是线程不安全的,但是,我们可以看出:
defaultWriteObject //写入当前的ArrayList除了元素数组的部分。
然后写入size,和size个元素的值。
与此对应的:
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);
}
}
与写过程完全对等的过程:
先用defaultReadObject读取出ArrayList对象
然后读取元素个数size
然后把每个元素写回数组。
好了,剩下的就是java8流相关的一些东西,和迭代器之类的东西了。我们暂且不表。有机会的话写一篇有关的,哈哈哈。