一、理解
hashmap是一个数组+链表+红黑树(1.8及以后才有)的存储结构,当添加一个key-value时,先计算元素key的hash值,找到bucket(理解为数组)中的位置来存储键值对对象,对于相同hash值的键值对对象将它存储在数组的同一位置以此添加在链表的下一个节点(链表结构主要是为了解决hash碰撞的问题);
二、主要的常量和节点(Node<k,v>)中的内容
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 初始容量 n
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子 容量超过 n * 0.75 时就进行扩容
static final int TREEIFY_THRESHOLD = 8; //阈值 链表超过8转为红黑树
节点中存在的内容:
hash key value next:下一个节点
注:扩容时,按原容量的两倍扩大新的容量
二、构造函数
有四种hashMap的new方法
构造函数,主要是设置容器大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {......}
public HashMap(int initialCapacity) {......}
public HashMap(){.....}
public HashMap(Map<? extends K, ? extends V> m){} //没有用过 ,用m的元素初始化散列映射
其中我们看一下第二个构造函数
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);
}
- 首先,容量小于0报错;
- 容量大于最大容量2^30 时,设置初始容量等于2^30
- 负载因子小于0报错
- tableSizeFor : 获取大于initialCapacity的最小的二的幂次方
- 设置负载因子和容量初始值
三、核心put方法 : put方法里面才做初始化,new HashMap并没有做初始化
原因:解决new HashMap后没有进行真正使用的问题,可以节约内存
HashMap.java
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- hash(key):获取key的hash值,以此找到对应的数组位置
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // (n - 1) & hash = hash % n : 找到数组的下标,使用位运算性能更快
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // equals方法用来比较如果hash相同的情况下,有没有存在key值也相同的情况,存在则获取这个相同key的节点
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表长度大于阈值8,看是否需要改变为红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1)
//treeifyBin: 首先比较table的长度是否大于64,如果大于64则变为红黑树结构;如果小于64,只进行扩容(resize)操作;
treeifyBin(tab, hash);
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); //存在同hash同key的对象,将原来的key对象值更新为新的添加进原链表节点信息中
return oldValue;
}
}
++modCount;
//如果当前大小大于 门限(初始容量 * 负载因子),则进行扩容,扩容到原来容量的两倍
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
- (n - 1) & hash : 上面计算数组下标是通过这个计算,等同于 hash % n (求余),使用位运算性能更快
- n-1:比如n=16,n-1=15 ,二进制就是1111
这样做&运算的话可以保证结果更加均匀,可以使每一位都参与运算(因为0与任何数进行与运算都为0)
hash : 10111100001010
n-1: 00000000001111
= 00000000001010
- 这里上面key值是通过equals比较相同的,这里的话key最好使用string和Integer类型,因为这两个类里面重写了equals和hashCode方法,如:string类型equals方法就比较了每个字符都相等才判断为key相等
String.java
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
四、核心方法get(key)
- 根据key值,提过hashCode算出hash值,然后根据hash & (n-1)找到链表数组中的first = tab[i = hash & (n-1],然后判断first的key值是否和key相等,相等返回对应的value,不相等继续向后面的链表遍历找到相同key值,返回对应的values即可;
public V get(Object key) {
Node<K,V> e;
// hash(key) : 提过hashCode()方法获取hash
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;
//first = tab[(n - 1) & hash] : 找到对应下标的链表数组
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断第一个链表节点是否符合(hash 、key匹配)
//key.equals(k) : equals方法可以比较每个字符是否相等
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
//遍历后面的链表,知道取到相同key值的节点
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;
}
重点:
1、ArrayList和LinkedList的性能分析
其中ArrayList对于查询和修改性能更高,因为是数组结构的,是一片连续的内存空间的,可以通过计算得出下标快速找到数据
LinkedList是链表结果,无法根据下标查找数据,进行查找时只能一个个去轮询,但是进行删除和插入时会更高效,只需要将前后的next修改指向就可以;ArrayList需要把后面的每个数组进行移位,频繁的使用ArrayCopy操作会影响性能
2、一个数组反复进行插入和删除如何提高性能?
进行删除时,根据下标招标对应的数值,就该数值改为null或者别的值代表已删除;
插入时:将需要插入的值设置在为null的位置上
3、HashMap有什么好处?
结合数组和链表的数据结构,提高增删改查的效率;
对于链表查询效率慢的问题,在hashMap中,链表长度超过8,转为红黑树(左边的节点值小于头节点,右边的节点值大于头节点);
3、为什么容量一定是二的幂次方
为了方便位运算
因为原来要通过取模运算(hash % n)获取下标找到对应位置,现在用与运算((n-1) & hash)代替,与运算效率大大高于取模运算;
4、加载因子为什么要是0.75?
因为如果为1的话,相当于要把数组全部填满才扩容,太过理想,这种会造成大量的hash碰撞,导致链表过长;
如果加载因子过小,又会造成数组才使用一点就进行扩容,造成内存浪费;
至于为什么是0.75,这是经过大量计算和试验,在尽可能减少hash碰撞和内存浪费的情况下,取到的数值
5、阈值为什么是8?
个人理解,链表长度在8以内的话,链表结构的查询和红黑树的查询性能应该是差不多的,所以8以内还是保持链表结果,长度大于8时红黑树的性能明显比链表结构快,所以大于8时转为红黑树性能更快;
注:必须同时当table长度大于64时,才能转为红黑树数据结构
6、如何提高hashMap的性能?
容量大小设置为2的幂次方
容量大小尽量的大:这样进行与运算时,可以尽可能的保证分布均匀(尽可能但不是无限大)
7、hashMap怎么实现同步?
Collections.synchronizeMap(new HashMap());
8、为什么最好使用String和Integer?
上文已经提过,这里补充下:String是不可变的,是final的,且才写了equals() 和 hashCode()方法,可以保证hashcode是不变的,两个不相等的对象返回不同的key,可以减少碰撞的几率,提高性能;
9、hashMap和hashTable的区别?
hashMap是线程不安全的;hashTable是线程安全的,因为在put方法中加了synchronized(关键字)锁;
hashMap可以接受为null的键值对,hashTable不可以;
hashMap在进行操作时,如果有其他的线程改变了hashMap的结构(删除和插入),iterator(迭代器)会抛出异常;hashTable不会;
hashMap是不加锁的,所以单线程环境下,效率更快;