毕业两个星期了,开始成为一名正式的java码农了。
一直对偏底层比较感兴趣,想着深刻本身的java技能,看书、读源码、总结、造轮子实践都是付诸行动的方法。
说到看源码,就应该由简入难,逐渐加深,那就从jdk的源码开始看起吧。java
ArrayList和Vector是java标准库提供的一种比较简单的数据结构,也是最经常使用的一种。算法
<!-- more -->数组
表这种抽象概念指的是一种存放数据的容器,其中数据A1, A2, A3, ..., Ai, ... AN有序排列。安全
那么在表这一抽象的数据结构模型中:数据结构
它的操做有:并发
表有两种实现方式,数组实现和链表实现。这两种实现方式各有优劣,其中这里所说的ArrayList是数组实现方式。jvm
线性表的实现中是将元素放到数组中的。若是是C的数组,那实际上是一块连续的内存。
在java中也是java的原生数组,内存布局中逻辑上是连续的,物理上不必定。印象中有的jvm实现为了解决内存碎片搞相似逻辑内存的这种操做。函数
往线性表取出数据或拿出元素都能很直接对应到原生数组中去。
可是,线性表插入数据的这一操做要求表是可以动态增加的,可是原生数组的大小是固定的。源码分析
线性表实现的一大重点是实现表动态增加这一要求,将其转换、化归到固定大小的原生数据上去。思路是,当线性表内部保存数据的数组若是不够用了,就申请一个更大的数组,将原来数组的数据拷贝进去。布局
如上图所示,在设计上,它的结构应该理解成 ArrayList
--> List
--> Collection
--> Iterable
:
List
接口表示抽象的表,也就是上面所说的表。Collection
接口表示抽象的容器,能放元素的都算。Iterable
接口表示可迭代的对象。也即该容器内的元素都可以经过迭代器迭代。可是在继承结构上,ArrayList
继承了AbstractList
。这是java中通用的一种技巧,因为java8以前的接口没法指定默认方法,若是你要实现List
接口,你须要实现其中的全部方法。
可是,List
接口中的全部方法是大而全的,有大量方法是为了方便调用者使用而设计的衍生方法,并不是实现该接口的必须方法。好比说,isEmpty
方法就能够化归到size
方法上。
因此,java对于List接口会提供一个AbstractList
抽象类,里面提供了部分方法的默认实现,而继承该类的只须要实现一组最小的必须方法便可。
总而言之一句话,AbstractList
的做用是给List
接口提供某些方法的默认实现。
先看ArrayList内部保存的属性和状态。
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;
其中,最关键的为elementData
和size
。 elementData
也即真正存储元素的java原生数组。
因为ArrayList中实际保存的元素个数是少于elementData
数组的大小的,也即会有部分空间的浪费,所以size
属性是用来保存ArrayList
存储的元素个数。
值得注意的是,elementData
没有访问修饰符,咱们知道默认是对包内可见的。这样作的目的是:
构造函数一共有三个,下面是其中两个。第三个是从其它容器初始化,没什么好看的地方。
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; }
第一个带参数的构造函数,行为上这个都知道,建立出来的依然是个空表,看代码也可发现执行完后size
属性为0。initialCapacity
的含义是指定内部存储数据的原生数组的初始化大小。
代码逻辑很简单,initialCapacity
大于0时对elementData
分配该大小的原生数组。可是问题是,initialCapacity
为0时,并非直接new一个大小为0的数组,而是使用静态变量EMPTY_ELEMENTDATA
来代替。
为何?EMPTY_ELEMENTDATA
是静态变量,全部实例共享,我的猜想应该是为了省点内存吧。但是这个占不了多大的内存啊,这个理由可能说服力不太强。
第二个默认构造函数,这个函数行为上也是建立空表。可是会预先将存储数据的原生数组的大小设置为DEFAULT_CAPACITY
,也就是默认值10。这样,能避免在大小较小时频繁扩容带来性能损耗。
按照这个思路,代码应该长这样才对:
public ArrayList() { this.elementData = new Object[DEFAULT_CAPACITY]; }
可是实际上,咱们发现DEFAULTCAPACITY_EMPTY_ELEMENTDATA
实际上是个空表,也是静态变量,多实例共享的。
这实际上也能够认为是一种优化手段,由于不少场景都是直接默认构造的一个空表在哪里放着,为了节约内存,jdk实现先不实际分配空间,仅仅作一个相似标记做用的操做,以后真正使用了才会分配空间,达到一个相似“延迟分配”的效果。这些思路在扩容相关的代码中有所体现。
get和set没什么好说的,其实是直接操做内部的原生数组。
不过,从下面的代码中能够看到,在get和set以前,会作越界检查。
private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } public E get(int index) { rangeCheck(index); return elementData(index); } E elementData(int index) { return (E) elementData[index]; } public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }
下面再来看插入元素。插入有两种:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } 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)); }
思路都特别显然,主要是在操做以前,调用了ensureCapacityInternal
函数,这个函数调用完毕后会保证内部数组的空间能存储下这么多元素。
此外,void add(int, E)
函数还作了越界检查。
接下来看ensureCapacityInternal
函数,相关实现涉及到四个函数。
这个函数的目的是保证执行完后,内部的原生数组至少能容纳minCapacity
个元素。
以前所说的那种“延迟分配”操做在这里就体现出来了。分析代码流程不难发现,当elementData
为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
,也即前面所说到的那个标记,那么以后扩容后的原生数组空间必定不小于DEFAULT_CAPACITY
,也就是前面定义处的10。
private void ensureCapacityInternal(int minCapacity) { 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); } 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; }
再看具体的扩容逻辑,也即grow
函数。逻辑还不单一。首先扩容尝试按照原来大小的1.5倍扩容,为了性能,这里除以2优化成了右移运算。
若是1.5倍不够怎么办?若是是我,我可能会优雅的递归再扩大,然而,jdk的作法是若是1.5倍不够的话直接按照须要的大小扩容。
最后,若是实在太大,也要作一下限制,最大可达到的大小为Integer.MAX_VALUE
,不然就超过了int的范围,溢出了。
下面是移除相关的函数,有两个:
E remove(int)
是经过索引移除。boolean remove(Object)
是经过元素移除。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; } private void fastRemove(int index) { modCount++; 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 }
按索引移除的的思路很简单,首先把须要移除的元素拿出来,而后后面的都往前挪一个,经过数据拷贝实现。
可是,有个 特别须要注意的地方是,假设删除以后的表大小为N,往前挪一个后,elementData[N - 1]
和elementData[N]
都指向了最后一个元素。这时elementData[N]
仍然持有对该元素的引用。若是以后试图移除表中最后一个元素,它可能不会及时的被gc干掉,形成无心义的额外内存占用。
按元素移除的思路也很显然,先是找到该元素的索引,而后按索引移除。fastRemove
的代码几乎和remove(int)
如出一辙,不知道为何复用一下。。。
最后,从代码能够看到一点,表内元素会被移除,可是jdk对ArrayList的实现只会扩容表,而不会缩小表以减少内存占用。不过,它提供了一个trimToSize
方法,将表中原生数组的空闲空间去掉:
public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
很明显,它从新分配一个正好合适的原生数组,而后拷贝过去。
这两个函数很好理解:
这两个函数算法几乎如出一辙,所以抽象出一个函数batchRemove
来实现。
public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, false); } public boolean retainAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, true); } private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) elementData[w++] = elementData[r]; } finally { // Preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws. if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; }
核心在于try中的那三行代码。这个算法简化了就是这样:
有两个数组data和isRemove,isRemove[i]为true表示data[i]应该被移除。
要求在O(n)时间复杂度,O(1)空间复杂度,将data中isRemove[i]为true的元素移除。
算法这种东西,思路我能在脑子里想象出来画面,可是说不清楚。。。 大学里学习算法时候都写过,就很少说了。
感受写的有点多。。。以上是和ArrayList
操做密切相关的,其它的简单总结下吧。
SubList
中。Itr
。基本都是一些包装代码。Arrays.sort
实现的,因此,具体的排序算法要看Arrays.sort
。还有一个没有看懂的问题,即在AbstractList中有一个modCount
字段,ArrayList
的实现中屡次操做该字段。可是仍然没有理解该字段的做用。
Vector提供的容器模型和ArrayList几乎如出一辙,只不过它是线程安全的。
它的代码思路上和ArrayList差很少,可是有一些实现细节上的小区别。
首先,它有一个参数capacityIncrement
,可以控制扩容的细节,看构造函数:
public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; } public Vector(int initialCapacity) { this(initialCapacity, 0); }
若是不指定的话,这个initialCapacity
默认值为0。在内部使用属性capacityIncrement
保存。
其次,再看控制扩容的核心函数grow
,研究下扩容逻辑是否是有什么差别:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
能够发现,capacityIncrement
控制的是扩容的步长。
若是没有指定步长,那么则是按照两倍扩容,这是与ArrayList
不一样的地方。
整体上,它至关于synchronized同步过的ArrayList,也即它对线程安全的实现很是暴力,并未用到太多的技巧。很显然,在并发环境下,对vector的操做直接锁住整个vector,至关于操做vector的线程是串行操做vector,性能不高。