从这篇开始写关于
Java容器相关的,主要分为三类:List,Set,Map.目前来看只会涉及主要的实现,但也不排除穿插'老家伙'的情况.
1.Collection
既然要写容器,那肯定先从Collection入手,Collection是List和Set的父接口.Map则是单独的接口.在这里提下Collection,后面就不再赘述了.
public interface Collection<E> extends Iterable<E> {
Collection继承Iterable接口,实现了Iterable就表示允许该对象成为for-each的目标对象,者也是Java的一个语法糖.该类就包含一个个返回Iterator的方法和两个接口默认方法,具体可以自行查看.
Collection作为List,Set接口的根接口,并没有什么实质性的内容,只是定义了些通用方法用户在不同类型中传参,具体的内部细节有子类去掌控,如是否允许为空之类的.而且JDK也没有提供该接口的具体实现.
2. List
public interface List<E> extends Collection<E> {
以上便是List接口的定义信息,从官方文档注释来看,List代表的是有序的集合,可以精确的控制每个元素的插入位置,可以通过整数索引获取指定位置的元素.并且允许重复,可以有多个null值或不为null,取决于具体的实现(大部分都允许包括null值).
官方还提示了一点,在你不知道List的具体类型的情况下,迭代列表比通过索引获取更加可取.同时需要慎用通过索引获取元素,这将执行高代价的线性搜索.
3.ArrayLsit
1. 概览
ArrayList可以说是老大哥的存在,绝大部分情况下List实现首选,先看下结构:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
先看下ArrayList所继承的类:
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
AbstractList提供了List接口的基本实现,以减少采用随机访问(数组)实现Lsit接口所需的工作.
上一层
public abstract class AbstractCollection<E> implements Collection<E> {
AbstractCollection提供了Collection的基本实现,以减少实现该接口所需要的工作.如果你只需要实现一个不可修改的集合,那么扩展该类即可,只需重写iterator()和size()即可,要实现可修改的,那么你还需要重写add()方法,因为该add()方法默认抛出异常.
AsbtractCollection和AbstractList的区别就是一个是针对需要实现Collection服务的,一个是针对使用数组实现List服务的.
再回到ArrayList,实现了RandomAccess接口,该接口只是一个标记接口,表示该类支持随机访问,在选择对应算法时就可以通过该接口选择合适的算法.Cloneable也是一个标记接口,表示该类支持使用clone()方法,如果没有实现Cloneable而去调用clone()则会产生CloneNotSupportedException异常.最后一个Serializable就不说了,相信大家都知道.
2.方法
上面提了一下ArrayList的类定义信息,这里我们先粗略的看一下基本熟悉:
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 慢性膨胀,和下面这个主要用去区分第一次膨胀的大小
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 第一次膨胀为DEFAULT_CAPACITY
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 实际存储元素数组 transient不被序列化
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 数组包含的元素数量
* @serial
*/
private int size;
刚开始令我疑惑的就是上面的EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA属性,为什么需要两个默认的空数组,后续讲到扩容的时候便会体现出这个区别,这里先透露一下: 使用带了指定长度的构造函数使用的是EMPTY_ELEMENTDATA,而默认的空参构造方法使用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA.
构造函数这里就略过了,只是一些初始化操作.下面看点常用方法
indexOf()
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
indexOf()用于查找元素第一次出现的索引,挺简单的.contains()复用了这个方法,与之相识的还有lastIndexOf(),实现也很简单,只是方向遍历查找值.
clone()
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
ArrayList的clone()只是浅拷贝.并没有去处理对象值的情况.
add()
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这个方法算是比较重要的方法,涉及到扩容之类的,上面便是add()的定义,比较重要的还是ensureCapacityInternale().我们就着重讲一下该方法,我们假设第一次调用,也就是参数为1调用该方法.
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
elementData也就是我们正在存储数据的地方,还记得开头说的elementData具体的是EMPTY_ELEMENTDATA还是DEFAULTCAPACITY_EMPTY_ELEMENTDATA吗?我们把这两种情况都带上:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
从上面方法功能看出,如果是DEFAULTCAPACITY_EMPTY_ELEMENTDATA则我们这个传递的1就被淘汰了,换来的便是默认的DEFAULT_CAPACITY,也就是10,如果不是则还是我们的1,那要是一开始参数就大于默认的10,则还是以参数的为准.
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
上面判断是否需要扩容,默认第一次初始化为1-0>0为true,所以,只有在初始化和容器满时才需要扩容.同时modCount++记录操作,用于快速失败.
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);
}
把参数带进这个方法便得出扩容结果是多少, 如果是EMPTY_ELEMENTDATA,则第一次为1, 是DEFAULTCAPACITY_EMPTY_ELEMENTDATA则第一次为10, 多带入几次, 就会发现如果一开始初始化时如果指定了长度,则使用指定长度的. 如果是指定长度是0, 也是会有自动增长的, 但是增长都是从低处开始1,2,3,4,6.....相对于使用无参数的构造方式,扩容的大小不是那么大,但前期每次都会试探性的扩容,逐渐越来越大.
所以,尽量能确定所需要使用的空间,避免扩容发生数组复制.如不能确定,也可以估算一个相对大一点的值.
还有一个add()方法,就是在指定位置添加,尽量少使用该方法,因为每次都会进行数组复制.
remove()
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;
}
remove()没什么好说的,就是把后面的往前移动一个位置. 这也看出每次复制都会进行数组复制.还有一个remove()的重载方法就是删除指定的元素,这个效率更低了,因为还多了层遍历操作.
还有一个比较重要的就是iterator()方法,这一块涉及到fail-fast机制:
iterator()
iterator()方法属于iterable接口定义的,上面也说了,实现iterable接口就可以成为for-each的目标对象,也就是诸如下面这种形式:
for(String str:list){}
如果没有实现iterable是不能成为该目标的,我们可以自己尝试下:
// 编译通过
public class Demo implements Iterable<String>{
public static void main(String[] args) {
Demo demo = new Demo();
for (String s : demo) {}
}
@Override
public Iterator<String> iterator() {
return null;
}
}
// 编译失败
public class Demo{
public static void main(String[] args) {
Demo demo = new Demo();
for (String s : demo) {}
}
}
而这种形式也就是Java的语法糖,除去语法糖就是下面这种形式:
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
list.remove(s);
}
Iterator 就是集合的一个迭代器,从Doc注释来看,这个迭代器替换了 Enumeration, 比Enumeration有两个不同之处:
- 允许在迭代的时候删除元素.
- 改进的方法名称.
接下类看下ArrayList的实现:
private class Itr implements Iterator<E> {
int cursor; // 判断是否有下一个元素
int lastRet = -1; // 返回最后一个元素的索引,没有则返回-1
int expectedModCount = modCount; // 用于快速失败
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();
}
}
ArrayList使用一个内部类来实现Iterator,其实所谓的快速失败,就是通过modCount来实现的,在每次使用ArrayList进行修改操作时,都会执行modCount++,而同时使用迭代器进行遍历时,next()会校验modCount的值是否发生变化,如果不同,则会抛出异常.
ArrayList内容大致就以上几点.
4.LinkedList
1.概览
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
LinkedList所继承的类:
public abstract class AbstractSequentialList<E> extends AbstractList<E> {
AbstractSequentialList和ArrayList的AbstractList相似.AbstractSequentialList提供了采用顺序访问(如链表)实现List接口的工作. AbstractList就不多说了,上面已经提过了.
回到LinkedList中,它实现了Deque接口,它所定义的就是一个双端队列,可以在头部或尾部插入,其他实现就不多介绍了,前面都有提及.
从类定义可以看出,LinkedList支持双端队列的操作,同时又有List的操作能力.所以,在使用LinkedList时,最好使用本类去引用的方式,而不要使用父类,不然有的方法会无法使用.LinkedList也允许元素为null值
关于LinkedList的双向链表结构就不提了,也比较简单,网上也很多例子.
2. 方法
由于是双向链表,这里只讲一些相对重要的方法.
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
上面这个方法是LinkedList中通用的通过位置检索元素的方法,他在内部采取了一个半分的思想:如果要检索的位置比总数量的一半还要大那么从尾部开始检索,否则从头检索.
在使用get(index)之类的方法时,都用到了上述定义的方法.
LinkedList带给我们的其实不多,因为是链表的结构,也不涉及扩容之类的操作.需要注意的也就是些元素读取.
5.CopyOnWriteArrayList
1. 概述
根据官方的定义,该类时ArrayList的线程安全的变体,所有操作都是创建底层数组的新副本进行的,类的定义和ArrayList类似,这里就不多说了.
2.方法
获取方法和ArrayList相似,主要不同的是修改操作.
add()
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
可以看到,add操作时先上锁,如果复制一个新数组,在新数组上操作,然后再把原有的替换为新数组,该类其实再性能上并不好,因为每次修改操作都会上锁,复制数组,消耗较大.但是可以避免并发修改异常,能避免的原因就是迭代器不支持修改操作,并且迭代器所持有的数据只有获取迭代器时之前的数据,因为在原数组上添加新元素,是复制的一个新数组,而不是原来的数组.
虽然性能不好,但是你在不想使用外部同步时,可以考虑这种方式.我认为迭代器不支持修改的主要原因是: 为了保持写时复制数组这以特性,不然在迭代器里面进行写操作时,也需要加上同一把锁进行同步,防止出现并发修改异常.这带来的效率非常低.
其主要场景就是在你不想使用外部同步,但遍历操作远远大于修改操作时,或者修改操作少时可以考虑使用.
6.Vector
1.概述
这老大哥本来不太想写的,但其扩容机制有点特殊,和市面上提的二倍有点差异,这才写上一点.
2.方法
就直接进入核心方法吧, 前提须知: Vector有一个增量属性,该属性会影响扩容,默认不传为0,并且Vector没有延迟初始化的功能,不传指定长度,直接为10.
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就是前面说的增量,可以看到,如果我们设置了增量,则扩容的大小为原来的大小加上我们指定的增量大小,没有指定则是旧的加上旧的,也就是两倍.
7.杂谈
List慢慢看还是挺容易理解的,无非就是继承关系容易看懵.多品品就好了.还有一点就是在使用List的iterator()时,尽量看看能否使用ListIterator去接受,该接口继承了Iterator接口,并提供了更多特性.但是如果使用Iterator去接受就享受不了该特性了.
在网上还有众多的关于List实现如何选择的文章,这里就不说了,毕竟也得根据业务来做具体选择.差距较大的还是体现在LinkedList按索引获取,ArrayList删除操作和头插入之类的操作中.
List就目前来说就先写以上几点,后续有什么新发现会更上的.