浅析Java集合类源码(二)--- HashSet, HashMap, Hashtable
2016-09-16 16:41
429 查看
4.HashSet
HashSet继承了AbstractSet,实现了Set,Cloneable,Serializable等接口。HashSet不是线程安全类。public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
HashSet通过一个HashMap来保存对象,Map的键是每次添加的元素,值是一个 static final Object对象PRESENT。
// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
(1)初始化
初始化可设置初始容量initialCapacity和加载因子loadFactor,HashSet调用HashMap的初始化方法,因此默认初始容量initialCapacity和HashMap相同,是16;默认加载因子loadFactor也和HashMap相同,是0.75。当 【元素数量(size) > 容量(capacity) * 加载因子(loadFactor)】时,HashSet的容量会相应的进行扩展至原来的2倍
public HashSet() { map = new HashMap<>(); }
public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); }
public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); }
(2)添加
HashSet的添加方法调用HashMap的添加方法public boolean add(E e) { return map.put(e, PRESENT)==null; }
(3)删除
HashSet的删除方法调用HashMap的删除方法public boolean remove(Object o) { return map.remove(o)==PRESENT; }
5.HashMap
HashMap继承了AbstractMap,实现了Map,Cloneable,Serializable。HashMap不是线程安全类。public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
HashMap对键key取hash值,然后将同一个hash值的元素保存在数组某个位置中。所谓同一个hash值,是指将元素的hash值与(数组容量-1)取(与&)关系后得到的值,目的是去除hash值的高位,保留hash值的低位,将其作为key的hash值。
HashMap中hash值的计算程序:将Key的原始hash值与(数组容量-1)取与(&)关系得到的值。因为数组容量是2的幂次方,减1后低位则全部变为1,对hash值进行与(&)运算后得到hash值的低位,去除了高位。
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
当HashMap中的元素不太多时,使用Node链表来存储;当HashMap中的元素过多时,使用红黑树TreeNode的存储结构来保存元素。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; }
使用Node节点保存HashMap元素的数据结构:
(1)初始化
HashMap初始化可设置初始容量initialCapacity和加载因子loadFactor。默认初始容量是16,会在添加第一个元素的时候初始hash数组的长度。默认加载因子是0.75。public HashMap(int initialCapacity, float loadFactor)
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
初始容量需为2的幂次方,当初始容量不为2的幂次方时,HashMap会调用tableSizeFor()方法把容量变为2的幂次方。
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; }
(2)查找
当调用get()方法对查找某一个Key值的元素时,getNode()方法计算键Key的hash值,先找到对应hash值在数组中的位置,然后对数组中对应位置的元素链表遍历比较,直到节点Node的Key值与查找的Key值相同。public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> 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<K,V>)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; }
(3)添加
添加元素时,若Key的hash值对应的数组位置还没有元素,则直接添加到该位置if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
若该位置已有元素,则先判断该元素的Key值是否与添加的Key值相同,若相同直接替换该Node节点的value值。
Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
否则遍历该链表的元素。若没有发现相同的Key值,则将该元素添加到链表的末尾。若发现相同的Key值,则替换该Node节点的value值。
else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;
(4)删除
删除某个节点和添加节点相似,当查找到某个节点后,将后继节点赋值到前驱节点的next,相当于把查找的节点移除if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; }
(5)调整元素
若HashMap中的元素数量达到了threshold门限值(当前数组大小Capacity * 加载因子loadFactor),便通过resize()方法调整数组大小。基本思想:设置loHead,loTail,hiHead,hiTail四个变量:其中lo变量是用于保存hash值在原数组的节点,hi变量是用于保存hash值在新数组的节点;Head变量用于保存同一个hash值链表的第一个元素,对于一组hash链表元素进行遍历时,只被初始化一次,Tail变量用于保存同一个hash值链表的最后一个元素。
例子:
1.初始HashMap的数组长度是16,先插入Key的hash值为{1}的节点,再插入Key的hash值为{17}的节点,再插入Key的hash值为{31}的节点。由于{1},{17},{31}对15取(与&)运算的值均为1,因此均保存在hash数组的table[1]元素中,以链表的方式存储。
2.当数组长度需要扩充至32时:
(1){1}对16取(与&)运算后仍为0,因此仍然在hash[1]位置,此时loHead和loTail均为{1};
(2){17}对16取(与&)运算后不为0,而原来{17}位于hash[1]的位置,因此其位于新table数组的[1+16]=[17]的位置,此时hiHead和hiTail均为{17};
(3){31}对16取(与&)运算后仍为0,因此仍在table[1]位置,此时loTail变为{31},loHead不变,仍为{1}。
for (int j = 0; j < oldCap; ++j) { Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> 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; } } } }
5.Hashtable
Hashtable继承Dictionary,实现了Map,Cloneable,Serializable接口。Hashtable是线程安全类。public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
和HashMap一样,Hashtable也是将Key的hash值相同但Key不同的元素放入数组的同一个位置,保存为Entry节点,然后将节点用链表的形式串起来。
private static class Entry<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Entry<K,V> next; }
Hashtable的hash值计算程序:将hash值与0x7FFFFFFF取与(&)后,对当前数组容量取模(%)。取模(%)的目的是保证hash值在当前table[]数组的范围内。
int index = (hash & 0x7FFFFFFF) % tab.length;
Hashtable的数据结构如下图
(1)初始化
Hashtable在初始化时设置初始容量initialCapacity和加载因子loadFactor,默认initialCapacity为11,loadFactor为0.75public Hashtable() { this(11, 0.75f); }
public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); }
public Hashtable(int initialCapacity, float loadFactor)
(2)查找
通过调用get()方法来查找相应Key的Value时,先通过Key的hash值计算获取在数组中的索引index值,再遍历该index值的链表,找到相同Key值的元素Entry,返回其值。public synchronized V get(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; }
(3)添加
添加的过程和查找很相似,都是先通过Key的hash值计算获取在数组中的索引index值,再遍历该index值的链表。当发现相同Key值的元素Entry,则更改此Entry的为新值,返回旧值。否则,添加一个新的Entry作为链表的头节点。public synchronized V put(K key, V value) { // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; Entry<K,V> entry = (Entry<K,V>)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; }
private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; // Creates the new entry. @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; }
(4)删除
删除时先计算出数组的索引值,然后遍历该位置的链表。找到符合的Key值,然后进行链表的删除操作。public synchronized V remove(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; for(Entry<K,V> 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; }
(5)调整元素
当数组需要扩展时,Hashtable调用rehash()方法调整元素的位置。当数组需要扩展时,需要先对旧数组的每个索引位置的链表进行扫描,计算链表中每个元素的新的索引值,将其插入到该索引值的链表的头部。
for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } }
相关文章推荐
- 深入源码剖析 HashSet、HashMap、HashTable
- HashMap、HashSet、Hashtable源码学习笔记
- 浅析Java中Map与HashMap,Hashtable,HashSet的区别
- 源码剖析——HashMap、HashTable、HashSet的区别
- 源码分析—HashMap、HashSet、HashTable
- 浅析Java中Map与HashMap,Hashtable,HashSet的区别
- 浅析Java中Map与HashMap,Hashtable,HashSet的区别
- 浅析Java中Map与HashMap,Hashtable,HashSet的区别
- HashMap与HashTable的区别 |HashSet和HashMap的区别
- HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别(转)
- Java中HashSet HashTable HashMap的区别
- 为什么序列化对象中的HashMap、HashSet或HashTable等集合不能包含对象自身的引用
- java中的HashTable,HashMap和HashSet的区别
- 分析HashSet,HashMap源码,如何保证HashSet集合元素唯一性。
- 遍历hashMap、hashSet、Hashtable
- HashMap、TreeMap、Hashtable、HashSet和ConcurrentHashMap区别
- JDK中ArrayList、HashMap和HashSet的equals方法源码分析
- hashMap,hashTable,hashSet,TreeMap的区别
- HashTable HashMap HashSet区别(java)
- 黑马程序员 HashMap和Hashtable及HashSet的区别