转载请注明出处:jiq•钦's technical Blog
一、HashMap
HashMap,基于散列(哈希表)存储“Key-Value”对象引用的数据结构。
存入的键必须具备两个关键函数:
(1)equals(): 判断两个Key是否相同,用来保证存入的Key的唯一性;
(2)hashCode(): 根据k-v对象的Key来计算其引用在散列表中存放的位置;
HashMap底层结构是一个数组:
transientEntry<K,V>[] table
而其中Entry<K,V>定义如下:
static classEntry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
包含了key,value以及hash值,更重要的是还有一个指向下一个节点的next指针。
结合下面将要介绍的put方法可知HashMap底层是一个哈希表,以链接法解决冲突。
在网上找到一张画的比较好的图片:
1、public V put(K key, V value)
直接看代码:
public V put(K key,V value) { if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//key为null时的插入
if (key == null)
return putForNullKey(value);
//根据key计算hash值
int hash = hash(key);
//返回哈希表索引位置
int i = indexFor(hash, table.length);
//在哈希表中该索引处的链表中查找相同key的Entry<K,V>
//注意table[i]是指向Entry<K,V>链表头结点的指针
for (Entry<K,V> e = table[i]; e!= null; e = e.next) {
Object k;
if (e.hash == hash && ((k =e.key) == key || key.equals(k))) {
//通过equals方法判断找到相同key的节点,用新value覆盖旧value并返回旧value
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//创建一个新的Entry<K,V>实体,头插法插入到位置i处
addEntry(hash, key, value, i);
return null;
}
其中针对 key 为 null 的处理如下:
private V putForNullKey(V value) {//在hash表的第0个位置开始找是否已经有了key为null的节点 for (Entry<K,V> e = table[0]; e!= null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //在hash表的第0个位置用头插法插入key为null的这个节点 addEntry(0, null, value, 0); return null; }
其中通过 key 计算 hash 值方法如下:
final inthash(Object k) { int h = hashSeed; if (0 != h && k instanceofString) { returnsun.misc.Hashing.stringHash32((String) k); } //调用Key的hashCode()方法计算hash值 h ^= k.hashCode(); // This function ensures that hashCodesthat differ only by // constant multiples at each bitposition have a bounded // number of collisions (approximately8 at default load factor). h ^= (h >>> 20) ^ (h>>> 12); return h ^ (h >>> 7) ^ (h>>> 4); }
由此可以得出以下结论:
(1)当插入一个<Key, Value>,发现此Key已经存在时,将用新的value覆盖旧的value;
(2)当插入的<Key, Value>key为null时,将插入到hash表的位置0处,并且只会有一个key为null的节点;
(3)当插入一个<Key, Value>时通过Key的hashCode()方法计算在hash表中的索引,通过Key的equals()方法判断两个Entry<K,V>的Key是否相同,相同会覆盖,所以说插入的<Key, Value>中的Key必须实现这两个方法。
2、public V get(Object key)
根据Key返回Value方法就相对简单:
public V get(Objectkey) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null :entry.getValue(); }
其中 getEntry (key) 方法为主要实现:
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //根据Key计算hash值 int hash = (key == null) ? 0 :hash(key); //indexFor(hash,table.length)根据hash值返回其在hash表中索引位置 //在该索引位置指向的链表中查找Key相同的节点并返回其Value for (Entry<K,V> e =table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key !=null && key.equals(k)))) return e; } return null; }
3、散列表容量
HashMap有默认的装载因子loadFactor=0.75,默认的entry数组的长度为16。装载因子的意义在于使得entry数组有冗余,默认即允许25%的冗余,当HashMap的数据的个数超过12(16*0.75)时即会对entry数组进行第一次扩容,后面的再次扩容依次类推。
HashMap每次扩容一倍,resize时会将已存在的值从新进行数组下标的计算,这个是比较浪费时间的。在平时使用中,如果能估计出大概的HashMap的容量,可以合理的设置装载因子loadFactor和entry数组初始长度即可以避免resize操作,提高put的效率。
下面看看resize操作时如何进行的:
void resize(intnewCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //根据新的容量重新创建hash表 Entry[] newTable = newEntry[newCapacity]; //逐个将节点拷贝至新hash表,较为耗时 transfer(newTable,initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity *loadFactor, MAXIMUM_CAPACITY + 1); }
4、类似结构
(1)Hashtable:
HashMap的早期版本,底层和HashMap类似,也是hash表存储,链接法解决冲突,通过synchronized关键字保证线程安全,下面看看其put方法,和HashMap的put方法很像:
public synchronizedV put(K key, V value) { //不允许Key为null的情况 if (value == null) { throw new NullPointerException(); } Entry tab[] = table; //利用key的hashCode()方法计算hash值 int hash = hash(key); //除留余数法计算索引 int index = (hash & 0x7FFFFFFF) %tab.length; //通过equals()方法找到key相同的节点覆盖 for (Entry<K,V> e = tab[index] ;e != null ; e = e.next) { if ((e.hash == hash) &&e.key.equals(key)) { V old = e.value; e.value = value; return old; } } modCount++; if (count >= threshold) { // Rehash the table if thethreshold is exceeded rehash(); tab = table; hash = hash(key); index = (hash & 0x7FFFFFFF) %tab.length; } //插入新节点 Entry<K,V> e = tab[index]; tab[index] = new Entry<>(hash,key, value, e); count++; return null; }
(2)ConcurrentHashMap
HashMap的线程安全版本,底层和HashMap几乎类似,也是采用hash表存储,链接法解决冲突,通过Key的hashCode()方法计算hash表索引,通过key的equals()方法判断两个Key是否相同。
不同点在于,ConcurrentHashMap针对hash表提出了一个“分段”的概念,每次插入一个<Key,Value>的时候,都先逐个分段请求获取锁,获取成功之后再执行在该hash表分段的插入操作。
总结:三者底层实现都是一样,但是不同之处在于是否线程安全,以及实现线程安全的方式。HashMap不支持线程安全,是一个简单高效的版本,Hashtable通过synchronized关键字简单粗暴地实现了一个线程安全的HashMap,而新的ConcurrentHashMap通过一种叫做分段的灵活的方式实现了线程安全的HashMap。
所以无论出于什么原因,旧的Hashtable不建议再使用,若没有并发访问需求,推荐HashMap,否则推荐线程安全的ConcurrentHashMap。
二、HashSet
HashSet是为独立元素的存放而设计的哈希存储,优点是快速存取。
HashSet的设计较为“偷懒”,其直接在HashMap上封装而成:
public classHashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable,java.io.Serializable{ static final long serialVersionUID =-5024744406713321676L; private transient HashMap<E,Object>map; // Dummy value to associate with an Objectin the backing Map private static final Object PRESENT = newObject();
可以看到底层就是一个 HashMap , Key 存放放入集合的元素,而对应的 Value 则是一个任意对象 PRESENT 。
下面是其Put方法:
public boolean add(Ee) { return map.put(e, PRESENT)==null; }
下面是返回迭代器的方法:
publicIterator<E> iterator() { return map.keySet().iterator(); }