Java集合:HashMap
一、集合概览
1. 集合与数组:数组是定长的,集合是变长的
2. 集合的接口是Collection,数据结构有Map/List/Set
3. 集合继承关系
二、HashMap定义:
1. Hash表(key-value),根据key(hash code)找到对应的value;会有hash冲突
2. 是基于Hash表的Map实现,Map即key-value的接口
public class HashMap<K,V> extends AbstractMap<K,V> // 继承AbstractMap抽象类,这个抽象类提供了Map接口的最主要功能的实现 implements Map<K,V>, Cloneable, Serializable // 实现Map接口,这个接口定义了键映射到值的规则
三、数据结构
1. JDK1.7的HashMap是由数组和链表组合实现的,一个数组的值加上其链表,叫做一个散列桶
2. 链表的每一项为Entry,Entry为HashMap的内部类,包含4个字段:key(键),value(值),next(下一节点),hash(hash值)
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ....... }
3. JDK1.8的HashMap是由数组+链表/红黑树组成
a. 当链表长度大于8,会转换成红黑树,树高比链表短,可以提高查询效率
b. 当红黑树节点小于6,会转化为链表
四、 装载因子和扩容:
1. 装载因子表示hash表中元素的填满程度,装载因子越大则空间利用率高,冲突机会也大,查找耗时也越大
2. HashMap容量:即数据结构中数组的大小,默认16,默认装载因子0.75,即阈值12,一旦大于等于阈值,2倍扩大空间,最大容量2^30
3. 扩容会使key进行rehash,即重新计算hash值,损耗性能,如果能预估HashMap中元素个数,预设个数能避免扩容,提高性能
HashMap():默认初始容量(16)和加载因子(0.75)
HashMap(int initialCapacity):可以自定义初始容量,预估个数能提高性能,因为动态扩容会损耗性能
HashMap(int initialCapacity, float loadFacotor):可以自定义初始容量和加载因子
4. JDK1.7的HashMap扩容会存在条件竞争,多线程下扩容,会造成死锁,因此不是线程安全的
5. JDK1.8的HashMap解决了扩容死锁的问题;扩容时也不再进行rehash,提高了扩容效率;
五、存储实现过程:put(key,value)
1. key可以为null,当key为null时,调用putForNullKey方法
2. 通过key的hashCode()方法计算key的hash值,根据hash值确认在table数组中的索引位置
3. 如果table中没有元素,直接插入
4. 否则遍历Entry,通过equals()方法比较key,如果不同则插入链表头,调用addEntry方法;否则替换掉旧值,所以没有相同的key
5. 如果链表长度超过了阈值8,就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;
a. 使用红黑树,因为红黑树是平衡二叉树
b. 不使用二叉树,因为二叉树在极端的情况下,会变成一条线性结构
6. 如果数组容量超过了装载因子,就需要扩容
public V put(K key, V value) { //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因 if (key == null) return putForNullKey(value); //计算key的hash值 int hash = hash(key.hashCode()); ------(1) //计算key hash 值在 table 数组中的位置 int i = indexFor(hash, table.length); ------(2) //从i出开始迭代 e,找到 key 保存的位置 for (Entry<K, V> e = table[i]; e != null; e = e.next) { Object k; //判断该条链上是否有hash值相同的(key相同) //若存在相同,则直接覆盖value,返回旧value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; //旧值 = 新值 e.value = value; e.recordAccess(this); return oldValue; //返回旧值 } } //修改次数增加1 modCount++; //将key、value添加至i位置处 addEntry(hash, key, value, i); return null; }
六、读取实现过程:get(key)
1. 根据key的hash值找到Entry
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
2. 通过keys.equals()方法找到链表中正确的位置
七、如何减少碰撞
1. 使用扰动函数,使不相等的对象返回不同的hashcode
2. 使用包装类(例如Integer, String)作为key,String是不可变的,作为key,键值就是不变的,可以缓存其hashcode
八、 HashMap和HashTable的区别(几乎可以等价,HashTable已经过时,不推荐使用):都实现了Map接口
1. HashMap不是同步(synchronized)的,所以不是线程安全的,即多线程环境下可能有问题
a. 可以用ConcurrentHashMap解决线程安全问题
b. 用Collections.synchronizeMap(hashMap)方法解决线程安全问题
2. HashTable是同步的,是线程安全的,但在单线程比较慢,同步会加同步锁,意味着一次仅有一个线程能够更新对象
3. HashMap可以接受key为null,而HashTable不行
4. HashMap的迭代器(Iterator)是fail-fast迭代器,可能在多线程环境下更改结构时出ConcurrentModificationException异常;而HashTable的迭代器(Enumerator)不是
5. HashMap不能保证元素的排列次序,所以次序可能会变,即不是有序的,可以使用TreeMap或者LinkedHashMap代替
6. TreeMap实现了SortMap接口,基于红黑树,对key进行排序
7. LinkedHashMap通过插入排序和访问排序,让key变得有序
九、使用线程安全的集合类:ConcurrentHashMap和CopyOnWriteArrayList
1. 同步的集合类(HashTable和Vector),同步的封装类(使用Collections.synchronizeMap()和Collections.synchronizeList()方法返回的对象)是线程安全的
2. 但是不适合高并发的系统,它们仅有单个同步锁,并且对整个集合加锁,以及为了防止ConcurrentModificationException异常经常在迭代的时候将集合锁定一段时间
十、ConcurrentHashMap和HashTable的区别
a. HashTable的大小增加到一定的时候,性能会急剧下降,因为迭代的时候会对整个对象锁定很长时间
b. ConcurrentHashMap引入了分割(Segmentation),在迭代的过程中,仅仅锁定map的某个部分,而HashTable是锁住整个Map,因此比HashTable性能好
十一、HashMap与HashSet的区别,存储不同数据类型
1. HashMap实现了Map接口,存储键值对,不允许有重复的Key;HashSet实现了Set接口,存储对象,不允许有重复的值
2. 添加元素:HashMap: put(Object key, Object value); HashSet: add()
引申:
1. 为什么扩容只能是2个n次方?
求余数操作太耗时,位操作能快速得到hash值在数组中的位置,2的n次方会方便位操作(与操作,异或操作)
2. 为什么默认装载因子是0.75?
3. 为什么链表长度是8的时候,会转为红黑树?
4. 为什么JDK1.7的HashMap扩容会死锁?
扩容的时候,链表是倒序插入新的HashMap中,用变量e和next指向链表的当前节点和下一个节点,当前线程指向节点后,切换到另一个线程,另一个线程倒序生成了链表,next会发生变化,就产生了循环链表
5. 为什么重写对象的equals方法时,要重写hashcode方法?
如果用到了hashMap,把对象作为key,另一个对象equals当前对象,但get另一个对象的时候会获取不到值,因为get方法会判断对象的hashcode是否相等
6. 为什么hashMap的key要用不可变类型,比如String,或者加final?
如果对象改变了,key就变了,用这个对象get的时候就获取不到了