Java集合框架中Map接口主要包括HashMap、HashTable、TreeMap,下面依次介绍
HashMap增加查询删除数据的方法为put
get
remove
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
//存储数据的结构
transient Node[] table;
transient Set> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
HashMap内部使用数组+链表的形式储存数据,当插入一个新值时,会首先新建一个节点,然后使用key的hash值对数组大小取余,得到的值就是数组插入位置的Index,在插入位置处,由于实际保存的是链表,因此会将新节点插入到链表末尾,因此,HshMap中每一条链上的节点的key的hash值对数组大小取余,都应该等于这条链表在数组的索引
由于HashMap要通过key值来确定在数组和链表中的唯一位置,因此HashMap不允许key值有重复,如果新增加一个数据时,数据key值和已有数据的key值相等,会执行替换操作,即将老数据替换为新数据
当链表的节点数大于8时,HashMap为了提高检索效率会将链表转换为红黑树。
HashMap内部比较重要的成员变量,table
存储数据的结构,以数组形式表示,类型为Node
,使用Node
能形成一个单向链表,内部包含hash
,key
,value
值,以及指向下一个节点的用next
。数组的初始大小为DEFAULT_INITIAL_CAPACITY
(16)
Node
结构如下:
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
}
threshold
用于判断HashMap是否需要扩容,当Table的节点数大于threshold
时,会进行扩容操作,loadFactor
指载因子,通过当前容量和loadFactor
来决定threshold
的大小。
在new一个HashMap时,我们可以选择无参构造,或者传入初始容量,加载因子,也可以传入一个Map。需要注意的是不管是传入初始容量还是传入Map都会调用tableSizeFor
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这是由于HashMap设计者将HashMap的数组大小设置为2的幂,因此通过这个函数能让我们在创建新数组时,保证数组大小是2的幂,那为什么HashMap的数组大小要设计成为2的幂呢?前面说到插入新节点时,要找对应在数组中的索引时是通过key的hash值对数组大小取余hash%length
,但是这样运算的话效率低,所以改为了hash&(length-1)
,另外有工程师发现如果length
是2的幂,效率会更高,因此HashMap大小就设计成为了2的幂
HashMap通过put
增加数据,put
会再次调用putVal
,我们注意到,HashMap是自己封装了一个函数hash()
来求key
的值,而不是直接使用key.hashCode()
作为hash值,这里我们也可以看出HashMap允许key
为null
,但是HashMap内部不会有重复的Key值,重复的话会替换数据。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap首先通过Hsh值找到对应的数组索引,然后再得到链表,最后将新节点插入到链表尾。
putVal
里面需要注意的主要有扩容操作resize
和树形化操作treeifyBin
,为什么要进行树形化呢,主要是单独对链表进行遍历的时间复杂度为O(n),由于随着链表的扩大,效率会很慢,采用红黑树的话,时间复杂度为O(log2n),因此效率得到了提高。
树形化treeifyBin
里面基本上都是红黑树的知识,不理解的话可以先看一下二叉搜索树、红黑树的知识,另外红黑树貌似是在JDK1.8的源码里才增加的
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//判断是不是第一次增加数据
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//调用resize函数扩容
//判断要插入的位置的链表头是否为null,是null的话直接将头指向新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//hash值相同,key值也相同,后面会进行替换操作
else if (p instanceof TreeNode)
//如果链表的节点数大于8之后,会转换为红黑树,节点类型也会变化为TreeNode,因此增加数据的方法会有一些区别
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//找到链表尾,然后使用key value值新建节点,并加入链表尾
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//节点数大于8之后,将链表树形化
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//找到了相同的key值,后面要进行替换操作
p = e;
}
}
//这里如果为true的话,表示hashmap中有相同的key值,那么执行替换操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
下面看一下扩容操作resize
,在HashMap插入数据时putVal
时,最后会执行判断操作if (++size > threshold)
,然后执行resize
,在这个函数中,每次扩容都会将数组大小扩展为原始数组大小的2倍,主要是为了保证数组的大小是2的幂
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//之前table不为空
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//数组扩大为原始大小的2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//如果初始化HashMap手动传入了初始数组大小或者传入了Map会走到这,newCap表示的是要形成的新数组的大小
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//初始化HashMap时采用无参构造方法,且未进行任何数据的操作
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//在扩容时我们需要将old数组的数据拷贝到新数组中,每个节点的位置会根据新数组的大小变化
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
//用于判断节点所在链表在数组内的索引是否小于原始数组大小
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashMap中通过get
方法来查询数据,首先通过key
的hash值来获取所在链表在数组中的索引,找到对应的链表后,再遍历链表找到key值相同的数据
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashMap通过remove
删除数据,本质上就是链表删除数据
HashMap在多线程下是线程不安全的,因为没有进行同步操作
HashTable增加查询删除数据的方法为put
get
remove
private transient HashtableEntry,?>[] table;
private int threshold;
private float loadFactor;
private transient int modCount = 0;
//默认数组大小为11
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new HashtableEntry,?>[initialCapacity];
// Android-changed: Ignore loadFactor when calculating threshold from initialCapacity
// threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
threshold = (int)Math.min(initialCapacity, MAX_ARRAY_SIZE + 1);
}
public Hashtable(Map extends K, ? extends V> t) {
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
HashTable内部也是采用与HashMap类似的结构来存储数据table
,不同于HashMap的是数组的初始为11,同时内部不存在红黑树,节点的类定义如下,依旧包含key
hash
value
next
private static class HashtableEntry<K,V> implements Map.Entry<K,V> {
// END Android-changed: Renamed Entry -> HashtableEntry.
final int hash;
final K key;
V value;
HashtableEntry next;
}
在HashTable中,hash值得求法跟HashMap不同,那就是这里hash值直接调用key.hashCode()
,也就是说HashTable不允许KEY
值为NULL
,否则的话会报空指针异常
HashTbale通过put
来添加数据,首先我们可以发现HashTable不允许value值为null,当然就如上所讲,key也不允许为null,否则会报空指针错误。
在获取数组的索引index值时也与HashMap不同,int index = (hash & 0x7FFFFFFF) % tab.length;
在插入数据之前首先会遍历链表,如果有重复的key
的话,会用新value
替换老数据
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
HashtableEntry,?> tab[] = table;
int hash = key.hashCode();
//找到数组索引
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
HashtableEntry entry = (HashtableEntry)tab[index];
//先查找是否有重复的值,有的话直接替换
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
我们看到插入新数据是通过addEntry
来实现的,下面分析一下这个函数
private void addEntry(int hash, K key, V value, int index) {
modCount++;
HashtableEntry,?> tab[] = table;
//判断是否需要扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
HashtableEntry e = (HashtableEntry) tab[index];
//直接将节点插入到链表尾
tab[index] = new HashtableEntry<>(hash, key, value, e);
count++;
}
函数很简单,主要是判断当前数据的数目是否大于threshold
,如果大于的话就要进行扩容rehash
,否则的直接将新节点插入到链表尾,下面分析rehash
@SuppressWarnings("unchecked")
protected void rehash() {
int oldCapacity = table.length;
HashtableEntry,?>[] oldMap = table;
// overflow-conscious code
//新大小为原来的两倍再加1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
HashtableEntry,?>[] newMap = new HashtableEntry,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (HashtableEntry old = (HashtableEntry)oldMap[i] ; old != null ; ) {
HashtableEntry e = old;
old = old.next;
//遍历数组里的每个链表,将每个节点加入到新数组中
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (HashtableEntry)newMap[index];
newMap[index] = e;
}
}
}
在rehash
中,数组容量为原始数组容量2倍加1 : int newCapacity = (oldCapacity << 1) + 1;
,同时在拷贝原数组数据到新数组时,挨个遍历数组的没个位置的链表的元素,根据运算之后的hash值对数组大小取余之后,放到合适的位置
查询数据时,先得到hash值,然后做了运算之后对数组大小取余得到数组索引,然后再遍历相应的链表,找到key值相同的节点
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
HashtableEntry,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (HashtableEntry,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
删除数据时先找到数据所在链表在数组中的索引,然后就是链表的基本操作,不再赘述
public synchronized V remove(Object key) {
HashtableEntry,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
HashtableEntry e = (HashtableEntry)tab[index];
for(HashtableEntry prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
put
add
remove
方法都有synchronized
修饰,因此在多线程下操作数据是线程安全的,但是数据效率比HashMap低,而且链表未像HashMap那样就行优化(树形化)
TreeMap增加查询删除数据的方法为put
get
remove
TreeMap内部采用类似红黑树的结构来保存数据,为什么说类似呢,因为传统红黑树一般默认左子节点的key值小于当前节点key值,右子节点key值大于当前节点,在TreeMap中,会有一个comparator来比较key的大小,也就是说具体怎么比较key值可以由我们自己规定,而不是定死的。
TreeMap有4中构造方法如下:
private final Comparator super K> comparator;
private transient TreeMapEntry root;
/**
* The number of entries in the tree
*/
private transient int size = 0;
/**
* The number of structural modifications to the tree.
*/
private transient int modCount = 0;
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator super K> comparator) {
this.comparator = comparator;
}
public TreeMap(Map extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
public TreeMap(SortedMap m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
TreeMap使用put
添加数据,大致流程就是从根节点开始,挨个通过comparator比较当前节点和要插入节点的通过key值大小,如果要插入节点的key值小于当前节点的key值,那么就将当前节点的左子节点再与要插入节点比较,否则将当前节点的右子节点与要插入节点比较,知道找到了树的叶节点,然后再将新节点放到叶节点下面(至于是左节点还是右节点通过key值判断),在插入完成后,通过fixAfterInsertion
去修正红黑树
public V put(K key, V value) {
TreeMapEntry t = root;
if (t == null) {
if (comparator != null) {
if (key == null) {
comparator.compare(key, key);
}
} else {
if (key == null) {
throw new NullPointerException("key == null");
} else if (!(key instanceof Comparable)) {
throw new ClassCastException(
"Cannot cast" + key.getClass().getName() + " to Comparable.");
}
}
// END Android-changed: Work around buggy comparators. http://b/34084348
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry parent;
// split comparator and comparable paths
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
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
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);
}
TreeMapEntry e = new TreeMapEntry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
红黑树操作,不再赘述
红黑树操作,不再赘述
线程不安全
类别 | HashMap | HashTable | TreeMap |
---|---|---|---|
内部实现 | 内部通过数组+链表的形式保存数据,当链表节点数大于8时,会将链表转换为红黑树 | 内部通过数组+链表的形式保存数据 | 红黑树 |
初始大小 | 数组的初始大小为16 | 数组的初始大小为11 | 0 |
Hash值 | (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) | key.hashCode() | 无hash值 |
key是否能为null | 允许key值为null,此时数组索引为0 | 不允许 | 如果我们在构造器初始化时传入了comparator,那么key可以为null,否则不允许为null |
value是否能为null | 允许 | 不允许 | 允许 |
扩容机制 | 数组大小变为原数组的两倍,始终保证数组大小是2的幂 | 数组大小为原数组大小的2倍再加1 | 树,挨个添加节点,再修正 |
多线程 | 不同步 | 同步 | 不同步 |
插入速度 | 比较快 | 比较快 | 比较快 |
查询速度 | 查询时间复杂度最差为O(n),最好O(log2n) | 查询时间复杂度恒为O(n | 查询时间复杂度O(log2n) |