一、分析一下hashmap原理
1、DEFAULT_INITIAL_CAPACITY = 1 << 4; 默认初始容量为16;
2、MAXIMUM_CAPACITY = 1 << 30; 最大容量为2^30次方;
3、DEFAULT_LOAD_FACTOR = 0.75f; 默认加载因子为0.75f;
4、TREEIFY_THRESHOLD = 8; 默认链表长度大于等于8时(并且数组长度不小于64)时转为红黑树;
5、UNTREEIFY_THRESHOLD = 6 默认小于等于6时从红黑树转为链表;
6、MIN_TREEIFY_CAPACITY = 64; 默认数组长度小于等于64。
7、size: 获取数组元素容量,modCount:记录操作的次数,threshold:阈值
二、开始分析put()方法
1、初始化前:table = null; threshold = 0;
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
2、初始化后:newCap = DEFAULT_INITIAL_CAPACITY(默认初始化16); newThr = 12(即DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
int newCap, newThr = 0;
if (oldCap > 0) { //判断容量是否大于0(即是否已初始化)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 初始容量设置为阈值
newCap = oldThr;
else { // 零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY; //默认容量初始化16
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
这时候新new了一个Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]将newCap值赋值到新的newTab的Node<K,V>上。(即newTab容量为16)
公式推理:table = newTab = newCap=16
三、开始分析get()方法
1、进入get方法会进行判断hash值是否相同,然后通过key获取hashCode值,并判断当前hashCode值是否为空。(不为空直接返回hashCode值)然后通过计算返回hash值。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
通过返回hash值得到对应指针的元素的位置,然后判断hash值和hash所对应的键是否相同。若判断相同,则直接返回该元素所对应的值。否则会去在该节点下判断是否有下一个元素(如果有会判断是否是红黑树,若不是会进入循环并判断hash值和对应的键是否相同,会一直查找下一个,直到遍历到最后一个元素),没有返回为null。
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // 总是检查第一个节点
((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;
四、判断两个对象是否相同
1、演示:创建两个对象并put()方法到容器中
代码如下:
HashMap<Person, String> map = new HashMap<Person, String>();
Person person = new Person();
person.setUsername("mbql");
person.setPassword("123");
map.put(person, "aa");
Person person1 = new Person();
person1.setUsername("mbql");
person1.setPassword("123");
接下来跟踪源码可知:
person: static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //hash = 357860815
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
能否从容器中get()方法获取到该对象?
System.out.println(map.get(person1));
继续深入源码可知:
person1: static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //hash = 1811037448
}
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;
}
此时可知:
这两个对象的hash值并不相同,所以在进入第一个if条件时就不成立(即返回为null)
综述:获取到该对象为空
怎么解决hash值不同的问题?
1、在Person对象中重写hashCode(即可解决hash不一致问题)
继续源码分析:
put():
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((password == null) ? 0 : password.hashCode());
result = prime * result + ((username == null) ? 0 : username.hashCode());
return result;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); // hash = 4855413
}
get():
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //hash = 4855413
}
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 && // 总是检查第一个节点
((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;
}
分析源码可知:
重写了hashCode()方法,真的解决了hash不相同问题。但在进入上面的第二个if条件时,分析发现这个持有键的对象的key不是同一个对象(即返回的对象还是为null)
可以看一下在这判断里面的equals()方法怎么实现的?
public boolean equals(Object obj) {
return (this == obj);
}
分析发现equals()方法用的是==号判断(即比对的是当前指针的内存地址)
2、怎么解决equals判断对象的方式?
在Person类中重写equals()方法(即可解决)
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
继续分析源码:
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;
}
在此时可知:
进入到第二个if条件时,会进行判断equal()方法判断对象是否相同。由于我们上面重写了equals()方法,所以此时用equals()方法判断对象属性时是属于同一个对象(即返回为该对象) return first。
这样分析下来可知,在使用HashMap时重写hashCode()和equals()方法是很重要的(能够解决hashCode的冲突性)。
五、总结
最后,做个总结:
1、在使用hashmap集合时,尽量重写hashCode和equals()方法,避免在后期遇到对象找不到问题,也能在一定程度上解决hash冲突问题。
2、创建对象时,最好指定hashmap的容量,可以通过算法计算出所需的容量,这样能够解决程序资源的浪费问题。
3、日常开发中,在使用hashmap时尽量根据业务的需求来做,对业务上资源的遗留优化问题,能够提升程序在运行的性能问题。
本文觉得有帮助,记得给博主一个关注及点赞哦~~~
后期会继续更新相关文章