图中实线边框表示的是实现类(ArrayList, Hashtable等),虚线边框的是抽象类(AbstractCollection,AbstractSequentialListd等),而点线边框的是接口(Collection, Iterator, List等)。
从图中可以发现,所有集合类都实线了Iterator接口,下面是Iterator接口的源码
public interface Iterable<T> {
Iterator iterator();
}
Iterator中提供的iterator方法是用于遍历集合中元素的,各个具体实现类可以按照自己的方式来实现对自身元素的遍历。具体的实现会在后面章节进行分析。
Iterator接口源码为:
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
从Iterator源码中可以看到,其提供了对当前集合的三种操作:判断集合是否有下一个元素、获取下一个元素、移除元素。
Collection接口继承了Iterator接口,并添加了其他一些方法,如isEmpty()判断集合是否为空,add(E e)添加元素等方法,其各方法如下所示:
方法名 | 功能 |
---|---|
int size() | 返回集合大小 |
boolean isEmpty() | 判断是否为空 |
boolean contains(Object o) | 判断元素o是否在集合中 |
Iterator iterator() | 返回集合的迭代器 |
Object[] toArray() | 将元素转化为数组 |
boolean add(E e) | 添加元素 |
boolean remove(Object o) | 将元素O移除 |
void clear() | 将集合内元素全部移除 |
… | … |
AbstractCollection类实现了Collection接口中的部分方法,并且多提供了一个抽象方法:
public abstract Iterator iterator();
由其他继承AbstractCollection类的类自己去实现自己想要的iterator()。因为不同类型的集合(如List,Set、Map之间的实现是不同的,无法提供统一的实现方法,这里抽象出来由它们自己定义自己的iterator)
这里只解读部分方法的实现
- contains(Object o)判断集合中是否包含对象O
public boolean contains(Object o) {
Iterator e = iterator();
if (o==null) {
while (e.hasNext())
if (e.next()==null)
return true;
} else {
while (e.hasNext())
if (o.equals(e.next()))
return true;
}
return false;
}
阅读源码可以知道,AbstractCollection类是先获取集合的迭代器iterator,然后对集合中的元素进行遍历,逐个比较每个元素与对象o是否相等,若在遍历过程中存在于o相等的对象则返回true,否则返回false,所以该方法的时间复杂度为O(n),n为集合中所包含的元素。
其他方法像remove(Object o),toArray(),containsAll(Collection
AbstractList类则是对所有List类做了抽象,封装了List中一些常用的操作方法。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>
方法名 | 功能 | 是否实现 |
---|---|---|
add(E e) | 添加元素 | √ |
get(int index) | 根据索引获取元素 | 抽象方法 |
set(int index, E element) | 设置指定索引位置的值 | × |
remove(int index) |
AbstractList类继承了AbstractCollection类并实现了List接口,AbstractList类中有两个内部类Itr和ListItr,两个不同的迭代器。
从源码中可以看到,其对AbstractCollection的抽象方法public abstract Iterator iterator()的实现如下所示:
public Iterator iterator() {
return new Itr();
}
直接new Itr()并返回,在看看其是如何实现自己的Iterator的。
Itr类继承了Iterator接口
private class Itr implements Iterator<E> {
/**
* 记录的遍历元素的索引游标
*/
int cursor = 0;
/**
* 记录最近一次被返回的元素的索引。若该元素进行了remove操作,则设置为-1
*
*/
int lastRet = -1;
/**
* modCount记录的是对当前列表改变的次数
*/
int expectedModCount = modCount;
public boolean hasNext() {//实现是否遍历完
return cursor != size();//当前遍历的元素索引是否=集合长度
}
public E next() {//获取下一个元素
checkForComodification();
try {
E next = get(cursor);//获取当前游标所指向的元素
lastRet = cursor++;//将当前游标索引赋值给lastRet,并将游标后移
return next;//返回当前元素
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)//lastRet为-1时,表示当前元素已经执行过remove操作
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
/*
*如果lastRet比cursor小,也就是说lastRet记录的索引对应的元素
*已经遍历过,cursor则需要后移一位,因为remove操作会使数组的
*长度-1,后面的元素会向前移动一位,所以cursor也需要相应的
*向前移动一位
*/
if (lastRet < cursor)
cursor--;
lastRet = -1;//标记为-1
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Itr类中定义了一个索引游标cursor,以及lastRet变量,cursor的作用是记录list中元素遍历的索引,每调用next方法一次,cursor后移一位,并将lastRet变量指向当前元素的索引,而remove操作会将lastRet赋值为-1。这个迭代器是针对线性列表的迭代器。而且在Itr类中nexth和remove操作都要调用checkForComodification方法,目的是判断在遍历的过程中,检查当前list有没有被改变(从后面具体类的实现,如Vector、ArrayList的实现中可以看到,他们进行添加或移除元素的时候,会改变modCount的值),一旦发生了变化,modCount 必然和expectedModCount不相等,就会抛出ConcurrentModificationException异常。
Vector类可以动态增长和收缩的存储对象,Vector类继承了AbstractList类并实现了List接口,支持随机访问,可以被克隆,也可以被序列化。类的继承关系如下:
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
vector类与继承类的关系图如下图所示:
Vector类用了一个Object[] elementData数组来存储元素数据,用elementCount
记录Vector中元素的个数。
//一个数组来存储数据
protected Object[] elementData;
protected int elementCount;
Vector提供了四组构造方法,也就是说可以有四种方式初始化一个Vector
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement)
public Vector(Collection extends E> c)
构造函数中initialCapacity可以指定Vector的初始容量,capacityIncrement可以指定Vector在容量已满时,每次增长的容量大小。也可以通过给定初始集合来初始化Vector。再看看Vector类中是如何对元素进行添加、删除、更新操作是如何进行的。
- 添加元素
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
vector中对元素的添加的步骤为:
1.先判断vector容量是否容纳的下新添加的元素。
private void ensureCapacityHelper(int minCapacity) { int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { Object[] oldData = elementData; /*新的容量是根据初始化capacityIncrement的值, *来进行更新的,设置了大于0的值,就原来的容量 *再加上设置的增长容量作为新的容量,否知直接扩 *容为当前容量的两倍 */ int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement) : (oldCapacity * 2); if (newCapacity < minCapacity) { newCapacity = minCapacity; } elementData = Arrays.copyOf(elementData, newCapacity); } }
2.然后再在尾部添加该元素,元素个数+1
这里我们可以看到,add方法是被==synchronized==关键字修饰的,而被synchronized关键字修饰的方法是线程安全的,只允许一个线程执行add方法,可以防止在多线程的情况下,多个线程同时对vector做修改。
- 删除元素
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount) //索引越界
throw new ArrayIndexOutOfBoundsException(index);
Object oldValue = elementData[index];
//判断是否移动(若index不是最后一个元素索引,则需要将索引在index后
//的元素向前移动一位)
int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; //会被GC,不存在内存泄露
//返回被移除的元素
return (E)oldValue;
}
删除过程是将索引号在index之后的元素全部向前移动一位,然后元素个数-1,
并返回被删除的元素。(删除操作也是线程安全的。)
vector还提供了其他一些方法,这里就不一一解读,下表vector提供的方法做了总结
方法名 | 功能 | 返回值 | 原理 |
---|---|---|---|
copyInto(Object[] anArray) | 将Vector内的元素复制到给定数组中 | 无 | 调用System.arraycopy()函数 |
lastIndexOf(Object o) | 返回给定对象的最后的索引 | 若o在vector中,则返回最后的索引值,否知返回-1 | 遍历vecotr中的元素 |
小节
vector内部实现使用一个Object数组存储元素的,可以动态的扩容,支持随机访问,根据索引获取元素的时间复杂度为O(1),而添加元素和删除元素操作的时间复杂度都为O(n),其中n为vector中所存储的元素个数。
Stack类继承了Vector类,其实现也是用数组存储元素,但是对元素的访问和插入做了一定的限制,只能在数组尾部添加元素,也只能在数组尾部删除元素,所以Stack的一个特性就是后进先出(LIFO)。下表中给出了Stack类中提供的方法:
方法名 | 功能 | 返回值 |
---|---|---|
E push(E item) | 元素进栈操作 | 进栈的元素 |
E pop() | 元素出栈) | 出栈的元 |
boolean empty() | 栈是否为空 | 为空时true,否者false |
int search(Object o) | 找出给定元素的索引 | 给定元素的索引,未找到为-1 |
ArrayList和Vector的功能都类似,唯一一点的不同的是,对比ArrayList和Vector各方法的实现,Vector的方法都是同步的(方法前都加了Synchronized关键字修饰),是线程安全的(thread-safe),而ArrayList则不是。由于Vector加了线程同步,所以会影响性能,因此,ArrayList的性能表现的会比Vector的好。另外,ArrayList和Vector的动态扩容机制也不同,Vector可以设置capacityIncrement来设定每次扩容时扩容的小,而ArrayList的扩容机制可以在ensureCapacity方法中可以看出
int newCapacity = (oldCapacity * 3)/2 + 1;
每一次扩容都是扩大为原容量大小的1.5倍。
这个类提供了一个基本的List接口实现,为实现序列访问的数据储存结构的提供了所需要的最小化的接口实现。对于支持随机访问数据的List比如数组,应该优先使用AbstractList。
这里类是AbstractList类中与随机访问类相对的另一套系统,采用的是在迭代器的基础上实现的get、set、add和remove方法。
为了实现这个列表。仅仅需要拓展这个类,并且提供ListIterator和size方法。
对于不可修改的List,编程人员只需要实现Iterator的hasNext、next和hasPrevious、previous和index方法
对于可修改的List还需要额外实现Iterator的的set的方法
对于大小可变的List,还需要额外的实现Iterator的remove和add方法
AbstractSequentiaList和其他RandomAccess的List的主要的区别是AbstractSequentiaList的主要方法都是通过迭代器实现的。而不是直接实现的(不通过迭代器,比如ArrayList实现的方式是通过数组实现的。)因此对于一些可以提供RandomAccess的List方法,直接使用方法可能更快。因为迭代器移动需要一定代价。
其提供了一个抽象的迭代器方法:
public abstract ListIterator listIterator(int index);
需要继承AbstractSequentiaList类的类自己去实现,
而ListIterator接口的定义如下所示:
public interface ListIterator<E> extends Iterator<E>
ListIterator接口继承了Iterator接口,除了Iterator
接口提供的hastNext,next,remove方法之外,还新增了如下功能的方法:
方法名 | 功能 |
---|---|
boolean hasPrevious() | 如果逆序遍历List还有下一个元素则返回true,否则返回false |
E previous() | 返回当前元素的前一个元素值 |
int nextIndex() | 返回当前元素后一个元素的索引 |
int previousIndex() | 返回当前元素前一个元素的索引 |
void set(E e) | 设置当前索引的值 |
void add(E e) | 插入值 |
ListIterator接口提供了对List的顺序和逆序遍历,在迭代的过程中运行修改列表中的值(set和add操作)
LinkedList类的定义如下:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
有LinkedList类的定义可以看出,LinkedList实现了List接口,能对它进行List的常用操作,实现了Deque接口,则将其当作双端队列使用,也实现了Cloneable接口,覆盖了clone()函数,可被克隆,也实现了java.io.Serializable接口,表明LinkedList可以被序列化,用于网络传输。
LinkedList的数据结构是基于双向循环链表的,切头结点不存放数据
LinkedList中定义了一个Entry header的头结点和int size的链表大小两个属性
//头结点
private transient Entry header = new Entry(null, null, null);
private transient int size = 0;//链表大小
而Entry是LinkedList的内部类,该类定义了双向循环链表
private static class Entry {
E element;//保存当前节点的元素
Entry next;//指向下一个节点
Entry previous;//指向前一个节点
Entry(E element, Entry next, Entry previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
LinkedList插入双向循环链表的插入操作如图所示:
LinkedList中提供了链表插入元素的操作
public boolean add(E e) {
addBefore(e, header);
return true;
}
其调用了向header前插入元素e的addBefore方法,addBefore方法就实现了向指定节点之前插入节点的操作:
private Entry addBefore(E e, Entry entry) {
Entry newEntry = new Entry(e, entry, entry.previous);
newEntry.previous.next = newEntry;//改变指针
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
还有addFirst(E e)和addLast(E e)方法提供了向header节点前后插入新节点的操作。
LinedList删除操作的指针变化如下图所示
LinkedList迭代器
public ListIterator listIterator(int index) {
return new ListItr(index);
}
LinkedList中提供了listIterator(int index)方法返回迭代器以遍历List,其中index参数指定了迭代器遍历的起始索引位置。
从代码中可以看到,其直接new 了一个ListItr并返回,而ListItr类是LinkedList的内部私有类,ListItr类的定义如下:
private class ListItr implements ListIterator<E>
实现了ListIterator中提供的add、remove、set等操作。
ListItr类中有四个属性:
private Entry lastReturned = header; //最后一次返回的节点,默认header
private Entry next;//下一个要返回的节点
private int nextIndex;//下一个返回节点的索引
private int expectedModCount = modCount;//记录当前LinkedList中被改变的次数
这里需要解释一下的是expectedModCount的作用,expectedModCount的值是在初始化LinkedList就指定为modCount,expectedModCount是用来判断在遍历过程中,是否有非LinedList中的方法对当前列表进行了操作(add,remove等操作都会改变modCount值),因为LinkedList是线程不安全的,索引,ListItr会判断expectedModCount与modCount的值是否相等,所以ListItr方法中很多方法中都加上checkForComodification()的方法,而checkForComodification()方法就是用来检查在遍历期间,List是否有被其他地方操作过,改变了ListItr的结构,一发现,迭代器就会抛出ConcurrentModificationException的异常。
现在看ListItr的构造函数
ListItr(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
if (index < (size >> 1)) {
next = header.next;
for (nextIndex=0; nextIndex<index; nextIndex++)
next = next.next;
} else {
next = header;
for (nextIndex=size; nextIndex>index; nextIndex--)
next = next.previous;
}
}
要找到指定的索引节点,他这里用了一个小技巧,用了一个if判断语句
size >> 1 //size/2
如果index < size >> 1,说明起始位置在列表的前半部分,可以从前往后,否则就在后半部分,就可以从后往前找,这样就节省了一半的时间。
而ListItr类中的其他方法(add, remove, set等操作)与LinkedList中相关的操作都类似,这里就不累述了。
LinkedList(jdk 1.6以后的版本)还提供了一个descendingIterator
public Iterator descendingIterator() {
return new DescendingIterator();
}
该迭代器可以逆序遍历LinkedList。
Map接口中给定了一些通用的函数模板
header 1 | header 2 |
---|---|
int size() | 返回键值对的个数 |
boolean isEmpty() | 判断map是否为空 |
boolean containsKey(Object key) | 判断是否包含指定主键Key |
boolean containsValue(Object value) | 判断是否包含指定的值value |
V get(Object key) | 根据给定key获取value |
V put(K key, V value) | 向map中添加键值对 |
V remove(Object key) | 根据主键移除对应的键值对 |
void putAll(Map m) | 添加给定map集合 |
void clear() | 清空map中的键值对 |
Set keySet() | 返回包含所有key的集合 |
Collection values() | 返回map中所有的value集合 |
Set |
AbstractMap抽象类的定义如下:
public abstract class AbstractMap<K,V> implements Map<K,V>
AbstractMap抽象类继承了Map接口中,并实现了一些骨干方法。
AbstractMap类中只有一个抽象方法
public abstract Set> entrySet();
也就是说,要实现一个自定义的map只需要实现entrySet方法就可以了,但是只能实现一个无法被改变的Map类,因为在AbstractMap中对put方法是默认不支持的。
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
所以,要实现一个可以有对Map中的元素进行改动的自定义类,还需要实现put方法,以及entrySet().iterator()返回的迭代器的remove()方法。
AbstractMap中有两个属性:
transient volatile Set keySet = null;
transient volatile Collection values = null;
两个属性都由transient和volatile关键词修饰。transient表明这两个属性在序列化时是不会被序列化的,volatile表明两个属性对所有进程是可见的,而且值有变化的时候,会通知所有进程。
HashMap继承了AbstractMap抽象类,实现了Map接口,可以被克隆及序列化。其定义如下:
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap中提供了三个构造函数:
- HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap
- HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
- HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和负载因子的空 HashMap
初始容量是指哈希表中桶的数量
加载因子是指哈希表中可用的散列表空间,当散列表的负载因子超过了加载因子,散列表将自动扩容。(因为散列表被填的越满,冲突发生的就越频繁,查询效率和插入效率会降低)
- 存储结构
HashMap是通过哈希表实现的,存取速度非常快,因为哈希表中会有冲突,而HashMap是怎么实现散列和碰撞避免的呢。其实Java对HashMap是一个“链表散列”,也就是用“拉链法”来解决碰撞的。其数据结构如下图所示:
可以很直观的看出,java中是用一个数组来实现的,而数组中的每一项是一个链表,从源代码中也可以看出,用一个了table数组,而数组的元素是Entry类型的。
transient Entry[] table;//不被序列化
Entry是HashMap中的内部类,继承了Map类中的Entry,其定义如下所示:
static class Entry<K,V> implements Map.Entry<K,V>
Entry中定义了四个属性
final K key;
V value;
Entry next;
final int hash;
其中next属性就是用来将hash到相同位置的(即发生碰撞)键值对链在一起的。可以在其构造函数看出,在碰撞链,HashMap是采用插表头法实现的。
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
散列位置的确定
在HashMap中,不论是put、get还是remove操作,第一步要做的就是定位当键值对在哈希表对应的位置,HashMap的内部存储结构是数组+链表实现的,在插入元素的时候,HashMap里面的元素位置尽量分布均匀,尽量使得每个位置上的元素数量只有一个,那么当用hash算法求得这个位置的时候,可以马上定位元素,无需遍历链表,大大优化了查询的效率。而HashMap中确定元素在散列表中的位置进行了两步操作:
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
第一步是根据key的hashCode调用hash函数获取hash值,然后根据indexFor来获取在哈希表中的索引位置。下面在看下hash函数和indexFor是如何实现的
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
可以看出,hash函数中对h进行了无符号左移和异或运算,indexFor函数是根据hash
值与哈希表长度进行了取模运算。对于任意给定的对象,只要它的hashCode()返回值
相同,那么程序调用hash函数所计算得到的Hash码值总是相同的。我们首先想到的就
是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但
是,模运算的消耗还是比较大的,在HashMap中是这样做的:通过indexFor方法来计算
该对象应该保存在table数组的哪个索引处。这个方法非常巧妙,它通过h &
(table.length-1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,
这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对
length取模,也就是h%length,但是&比%具有更高的效率。所以这就是为什么在初始化
的是要两容量转化为2的n次方。
HashMap的put方法
之前提过想要通过继承AbstractMap是实现一个可以改变的map,实现put方法,以及entrySet().iterator()返回的迭代器的remove()方法。下面是put方法的执行过程:
可以看HashMap对put方法是如何实现的。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());//获取key在hash表中的位置
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
可以知道,在put操作的时候,先判断key是否为null,因为HashMap对key为null的做了特殊处理。直接存储在索引位置为0的位置。若key不为null,则先获取要put的key在哈希表中的索引位置,然后判断该位置是否已经有值(是否发生碰撞e!=null),如果有值(发生了碰撞),就遍历该索引位置的链表,再查找该key值是否已经存在,如果存在,则更新value,否者跳出循环,调用addEntry方法将当前key-value插入到哈希表中,完成put操作。
再查看addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];//获取当前位置之前的值
table[bucketIndex] = new Entry(hash, key, value, e);//新建并更新
if (size++ >= threshold)
resize(2 * table.length);
}
addEntry方法通过获取给定bucketIndex中原有的值(null也无所谓),然后新建一个Entry更新到bucketIndex位置。HashMap认为,当table中的元素达到一定程度,也就是之前说的负载因子达到一定程度时,碰撞发生的几率会增大,HashMap会自动扩容。
size++ >= threshold
在分析HashMap的扩容机制之前,看看size、threshold属性的作用。
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient int size;
int threshold;
final float loadFactor;
从上述属性可以看到,HashMap默认的初始容量是16,最大允许扩容到的最大容量是2^30,默认的加载因子是0.75,size用来记录hashMap中存储的key-value对个数,threshold是根据加载因子*容量得到的,loadFactor是用来记录用户自定义的加载因子。HashMap允许在初始化时自定义容量和加载因子.
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
从上述构造函数可以看到,可以指定一个大于0的initialCapacity,作为初始容量,和一个非NaN大于0的加载因子,但HashMap对自定义的初始容量做了一个处理,处理成了离initialCapacity最近的大于initialCapacity的是2的n次方的一个数作为初始容量。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;//2^n
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内
部的数组无法装载更多的元素时,对象就更大的数组,以便能装入更多的元素。然而
Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,
将原有的元素移到新的哈希表中去。
我们可以分析resize的源码
void resize(int newCapacity) {//给定新的容量值
Entry[] oldTable = table;//新建一个数组
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {//达到最大容量的时候
threshold = Integer.MAX_VALUE;//则无上限的向表中插入元素
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);//调用该transfer函数
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
在扩容操作中,调用了transfer方法,其作用就是将旧的数组中的元素复制到新的数组中去。
void transfer(Entry[] newTable) {
Entry[] src = table;//src引用旧的数组
int newCapacity = newTable.length;//新的数组容量
for (int j = 0; j < src.length; j++) {//遍历就得数组中的元素
Entry e = src[j];//保存当前索引下的Entry
if (e != null) {
src[j] = null;//释放内存,让gc垃圾回收
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity);//重新计算位置
e.next = newTable[i];//插表头
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
进过了上诉的do{}while()循环对链表进行遍历后,因为是插表头法构建新的链,原先的链表顺序会被反转。下面举例演示HashMap的扩容过程。假设其中的哈希桶数组table的size=2, 所以key = 3、7、5,put顺序依次为 5、7、3。在mod 2以后都冲突在table[1]这里了。这里假设负载因子 loadFactor=1,即当键值对的实际大小size 大于 table的实际大小时进行扩容。
- 线程安全
源码中可以看出HashMap是线程不安全的,所以在多线程的环境下,应该尽量避免使用线程不安全的HashMap,可以使用线程安全的ConcurrentHashMap。
- 小结
(1)扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
LinkedHashMap继承了HashMap,并多了一个双重链表的结构。HashMap和LinkedHashMap最大的区别就是HashMap的元素是无序存放的,而LinkedHashMap的元素是有序存放的。
- LinkedHashMap的数据结构
LinkedHashMap的数据结构是散列表+循环双重链表,如下图所示
(图中的数据仅仅代表节点插入的顺序)
所以LinkedHashMap维护的双向链表,可以让元素有序的输出。LinkedHashMap没有重写HashMap的put方法,只是需改了put方法中调用的addEntry方法,用来构建双向循环链表
void addEntry(int hash, K key, V value, int bucketIndex) {
createEntry(hash, key, value, bucketIndex);
Entry eldest = header.after;
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
}
public static void main(String[] args) {
//默认按插入的顺序输出
LinkedHashMap lhm = new LinkedHashMap();
lhm.put(1, "Lucy");
lhm.put(12, "Fic");
lhm.put(23, "Nick");
System.out.println("----linked hash map output----");
PrintHelper.showMap(lhm);
HashMap hm = new HashMap();
hm.put(1, "Lucy");
hm.put(12, "Fic");
hm.put(23, "Nick");
System.out.println("----hash map output----");
PrintHelper.showMap(hm);
}
运行程序,可以得到如下输出:
----linked hash map output----
key:1 value:Lucy
key:12 value:Fic
key:23 value:Nick
----hash map output----
key:1 value:Lucy
key:23 value:Nick
key:12 value:Fic
可以看出LinkedHashMap和HashMap输出的顺序是不同的,LinkedHashMap是按照插入的顺序输出的,而HashMap是无序输出的。
private transient Entry header;
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMapde 的继承关系如下图所示:
由图中以很直观的看出:
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
TreeMap 实现了Cloneable接口,意味着它能被克隆。
TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,
或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n)。所以TreeMap的查询效率非常高
- 红黑树
红黑树又称红-黑二叉树,它首先是一颗二叉树,它具体二叉树所有的特性。同时红黑树更是一颗自平衡的排序二叉树。
红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树二叉树而言我们必须增加如下规则:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
下图是一颗典型的红黑树
对于红黑树的操作有:左旋、右旋、着色。
1. 左旋操作
2. 右旋操作
我们的重点放在java TreeMap的具体实现,对于红黑树的操作细节就不做详解了。
private final Comparator super K> comparator;//比较器
private transient Entry root = null;//根节点
private transient int size = 0;//节点数
private transient int modCount = 0;//改变次数
private static final boolean RED = false;//红节点
private static final boolean BLACK = true;//黑节点
root节点保存了当前红黑树的根节点。是一个Entry类型的属性。Entry类是TreeMap的内部类,定义了节点的数据结构,其定义如下:
static final class Entry implements Map.Entry
声明了左孩子、右孩子、父节点、节点颜色、以及保存的key-value对
K key;//键
V value;//值
Entry left = null;//左孩子
Entry right = null;//右孩子
Entry parent;//父节点
boolean color = BLACK;//节点颜色,默认为黑
TreeMap中put方法的流程如下图所示
从流程图中可以看出,put方法先判断整个树是否为空(root==null),若为空树,这直接将节点作为根节点插入,否者判断是否定义了比较器,若未定义比较器,这默认用key的比较器,这种情况下不允许key为null,否者会抛出NullPointException。然后根据比较器在树中找出是否存在key,若存在则覆盖原来的value,否者找到要插入的父节点,然后插入,在做调整(左旋、右旋、着色操作),使得树重新平衡。
下面是put方法实现的源码
public V put(K key, V value) {
Entry t = root;//获取root节点
if (t == null) {//判断是否为空
//若为空,直接创建节点作为根节点
root = new Entry(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
//用来保存当key不存在时,新节点要插入位置的父节点
Entry parent;
//获取比较器
Comparator super K> cpr = comparator;
if (cpr != null) {//不为空
do {
parent = t;//保存父节点
//比较大小
cmp = cpr.compare(key, t.key);
if (cmp < 0)//往左支走
t = t.left;
else if (cmp > 0)//往右支走
t = t.right;
else//找到,更新,并返回旧的value
return t.setValue(value);
} while (t != null);
}
else {//若未设置比较器
if (key == null)//key 不能为null
throw new NullPointerException();
Comparable super K> k = (Comparable super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//新建节点
Entry e = new Entry(key, value, parent);
if (cmp < 0)//插入左支
parent.left = e;
else//插入右支
parent.right = e;
//调整树
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
fixAfterInsertion方法对新增节点后可能失衡的红黑树进行调整,使得新的树符合红黑树的性质。包括左旋(rotateLeft())、右旋(rotateRight())、着色(setColor())操作。
/**
* 新增节点后的修复操作
* x 表示新增节点
*/
private void fixAfterInsertion(Entry x) {
x.color = RED; //新增节点的颜色为红色
//循环 直到 x不是根节点,且x的父节点不为红色
while (x != null && x != root && x.parent.color == RED) {
//如果X的父节点(P)是其父节点的父节点(G)的左节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取X的叔节点(U)
Entry y = rightOf(parentOf(parentOf(x)));
//如果X的叔节点(U) 为红色(情况三)
if (colorOf(y) == RED) {
//将X的父节点(P)设置为黑色
setColor(parentOf(x), BLACK);
//将X的叔节点(U)设置为黑色
setColor(y, BLACK);
//将X的父节点的父节点(G)设置红色
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
}
//如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五)
else {
//如果X节点为其父节点(P)的右子树,则进行左旋转(情况四)
if (x == rightOf(parentOf(x))) {
//将X的父节点作为X
x = parentOf(x);
//右旋转
rotateLeft(x);
}
//(情况五)
//将X的父节点(P)设置为黑色
setColor(parentOf(x), BLACK);
//将X的父节点的父节点(G)设置红色
setColor(parentOf(parentOf(x)), RED);
//以X的父节点的父节点(G)为中心右旋转
rotateRight(parentOf(parentOf(x)));
}
}
//如果X的父节点(P)是其父节点的父节点(G)的右节点
else {
//获取X的叔节点(U)
Entry y = leftOf(parentOf(parentOf(x)));
//如果X的叔节点(U) 为红色(情况三)
if (colorOf(y) == RED) {
//将X的父节点(P)设置为黑色
setColor(parentOf(x), BLACK);
//将X的叔节点(U)设置为黑色
setColor(y, BLACK);
//将X的父节点的父节点(G)设置红色
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
}
//如果X的叔节点(U为黑色);这里会存在两种情况(情况四、情况五)
else {
//如果X节点为其父节点(P)的右子树,则进行左旋转(情况四)
if (x == leftOf(parentOf(x))) {
//将X的父节点作为X
x = parentOf(x);
//右旋转
rotateRight(x);
}
//(情况五)
//将X的父节点(P)设置为黑色
setColor(parentOf(x), BLACK);
//将X的父节点的父节点(G)设置红色
setColor(parentOf(parentOf(x)), RED);
//以X的父节点的父节点(G)为中心右旋转
rotateLeft(parentOf(parentOf(x)));
}
}
}
//将根节点G强制设置为黑色
root.color = BLACK;
}
下表中显示了treeMap提供给了对红黑树的操作方法
方法名 | 功能 |
---|---|
setColor(Entry p, boolean c) | 对节点p着色 |
rotateLeft(Entry p) | 左旋操作 |
rotateRight(Entry p) | 右旋操作 |
deleteEntry(Entry p) | 删除p节点 |
fixAfterInsertion(Entry x) | 调整插入节点后的红黑树 |
fixAfterDeletion(Entry x) | 调整删除节点后的红黑树 |
- TreeMap的遍历方式
TreeMap中提供的entrySet()方法会返回TreeMap中所有键值集合,可以通过返回的集合的EntryIterator来对键值对进行遍历(entrySet().iterator())
2. 遍历TreeMap的键
TreeMap中提供的keySet()方法会返回TreeMap中所有键的集合,可以获取对应的KeyIterator迭代器进行key的遍历
3. 遍历TreeMap的值
TreeMap中提供的valueSet()方法会返回TreeMap中所有值的集合,可以获取对应的ValueIterator迭代器进行value的遍历。
- TreeMap的使用示例
public static void showMap(Map tm){
Iterator> entrySetIter = tm.entrySet().iterator();
while(entrySetIter.hasNext()){
Entry e = entrySetIter.next();
System.out.println("key:"+e.getKey()+" value:"+e.getValue());
}
}
public static void showEntry(Entry e){
System.out.println("key:"+e.getKey()+" value:"+e.getValue());
}
public static void main(String[] args) {
TreeMap tm = new TreeMap();
tm.put(4, "four");
tm.put(1, "one");
tm.put(3, "three");
tm.put(5, "five");
TreeMapUsage.showMap(tm);
System.out.println("最后一个key:"+tm.lastKey());
SortedMap sm = tm.subMap(2, 5);
TreeMapUsage.showMap(sm);
//第一个
TreeMapUsage.showEntry(tm.ceilingEntry(5));
tm.remove(5);//删除
System.out.println("删除key:5后");
TreeMapUsage.showMap(tm);
String oldValue = tm.put(1, "ONE");
System.out.println("old value:"+oldValue);
System.out.println("更新key:1后");
TreeMapUsage.showMap(tm);
System.out.println("逆序输出");
TreeMapUsage.showMap(tm.descendingMap());
}
自定义比较器一般都在key为自定义类的情况下会使用到。我们可以新建如下Student类
class Student{
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public int hashCode(){
return this.name.hashCode() + age;
}
public String toString(){
return "name:"+name+" age:"+age;
}
}
自定义一个Student的比较器
class MyCmp implements Comparator{
public int compare(Student o1, Student o2) {
if(o1.age > o2.age){
return 1;
}else if (o1.age < o2.age){
return -1;
}else{
return o1.name.compareTo(o2.name);
}
}
}
接下来测试自定义比较器的测试类
public static void main(String[] args) {
TreeMap tm = new TreeMap(new MyCmp());
Student s1 = new Student("Lucy", 18);
Student s2 = new Student("Dive", 19);
Student s3 = new Student("Kit", 17);
Student s4 = new Student("Hill", 18);
tm.put(s1, "A");
tm.put(s2, "B");
tm.put(s3, "A");
tm.put(s4, "C");
TreeMapUsage.showMap2(tm);
}
将会得到按age升序排序的Student输出
key:name:Kit age:17 value:A
key:name:Hill age:18 value:C
key:name:Lucy age:18 value:A
key:name:Dive age:19 value:B
HashTable的实现原理跟HashMap的实现原理都是通过哈希表来实现的,解决碰撞也是用链表的方式。下面是HashTable的定义
- HashTable的定义
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
HashTable与HashMap的实现原理都是相同的,也是一个哈希表,存储内容是键值对的映射。而不同点可以见下表:
HashTable | HashMap |
---|---|
实现的接口与继承的父类 | Dictionary和Map |
线程安全 | 有同步,线程安全 |
key、value是否可以为null | key和value不允许为null |
所以他们最大的不同是Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步(Collections.synchronizedMap)。
Collection接口
Collection是最基本的集合接口,一个Collection代表一组Object的集合,这些Object被称作Collection的元素。
所有实现Collection接口的类都必须提供两个标准的构造函数:无参数的构造函数用于创建一个空的Collection,有一个Collection参数的构造函数用于创建一个新的Collection,这 个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。
可以通过iterator()的方法获得Collection的迭代器,该方法返回一个迭代器,使用该迭代器即可逐一访问Collection中每一个元素。典型的用法如下:
Iterator it = collection.iterator(); // 获得一个迭代器
while(it.hasNext()) {
Object obj = it.next(); // 得到下一个元素
}
根据用途的不同,Collection又划分为List与Set。
List接口
List继承自Collection接口。List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
跟Set集合不同的是,List允许有重复元素。对于满足e1.equals(e2)条件的e1与e2对象元素,可以同时存在于List集合中。当然,也有List的实现类不允许重复元素的存在。
除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素, 还能向前或向后遍历。
实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。
LinkedList类
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在 LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list = Collections.synchronizedList(new LinkedList(…));
ArrayList类
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。
size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。
每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并 没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
和LinkedList一样,ArrayList也是非同步的(unsynchronized)。
Vector类
Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的 Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。
Stack 类
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方 法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。
Set接口
Set继承自Collection接口。Set是一种不能包含有重复元素的集合,即对于满足e1.equals(e2)条件的e1与e2对象元素,不能同时存在于同一个Set集合里,换句话说,Set集合里任意两个元素e1和e2都满足e1.equals(e2)==false条件,Set最多有一个null元素。
因为Set的这个制约,在使用Set集合的时候,应该注意:
1,为Set集合里的元素的实现类实现一个有效的equals(Object)方法。
2,对Set的构造函数,传入的Collection参数不能包含重复的元素。
请注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
HashSet类
此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。它不保证集合的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用 null 元素。
HashSet不是同步的,需要用以下语句来进行S同步转换:
Set s = Collections.synchronizedSet(new HashSet(…));
Map接口
Map没有继承Collection接口。也就是说Map和Collection是2种不同的集合。Collection可以看作是(value)的集合,而Map可以看作是(key,value)的集合。
Map接口由Map的内容提供3种类型的集合视图,一组key集合,一组value集合,或者一组key-value映射关系的集合。
Hashtable类
Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。
添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。
Hashtable 通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。
由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方 法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相 同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如 果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希 表的操作。
如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。
Hashtable是同步的。
HashMap类
HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代器操作时间开销和HashMap 的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。
WeakHashMap类
WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收。
TreeMap类
TreeMap内部是基于红黑树实现的,所以元素会按照key进行排序,可以自定义对key的比较器,TreeMap的put、get、remove方法的时间复杂度为O(logn),所以TreeMap的查询效率非常高。TreeMap是非同步的,所以在多线程的环境下,线程不安全
LinkedHashMap类
LinkedHashMap继承了HashMap类,多维护了一个双重循环链表,以让元素输出可以按照一定顺序输出,LinkedHashMap是不同步的,所以是线程不安全的。