基本信息
HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对。
注意点
1.当两个对象的hashcode相同会发生什么?
因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
2.如果两个键的hashcode相同,你如何获取值对象?
找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
3.如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
4.为什么String, Interger这样的wrapper类适合作为键?
String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
简单实现一个hashmap
package base_struct.hashmap;
/**
* 基于链表数组的实现
* @author dh
* @param <V>
* @param <K>
*
*/
public class DhHashMap<K, V> {
private class Entry<K,V>{
int hash;
K key;
V value;
Entry<K,V> next;
Entry(int hash, K key, V value, Entry<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
private static final int DEFAULT_CAPACITY = 1 << 4;
private Entry<K, V>[] table;
private int capacity;
private int size;
public DhHashMap(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException();
} else {
table = new Entry[capacity];
size = 0;
this.capacity = capacity;
}
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0 ? true : false;}
/**
* 这个hash算法采用位运算提高了效率,由于采用位运算,因此默认长度最好是2的幂最好
* @param key
* @return
*/
private int hash(Object key) {
return (key == null) ? 0 : key.hashCode()&(capacity-1);
}
/**
* 按照以前邏輯,默认key一样则只修改value,
* @param key
* @param value
*/
public void put(K key, V value) {
if (key == null) {
throw new IllegalArgumentException();
}
int hash = hash(key);
Entry<K, V> nEntry = new Entry<K, V>(hash, key, value, null);
Entry<K, V> entry = table[hash];
while (entry != null) {
if (entry.key.equals(key)) {
entry.value = value;
return;
}
entry = entry.next;
}
nEntry.next = table[hash];
table[hash] = nEntry;
size++;
}
/**
* h很简单的逻辑,找到确认的hash值后,一个个遍历
* @param key
* @return
*/
public V get(K key) {
if (key == null) {
throw new IllegalArgumentException();
}
int hash = hash(key);
Entry<K, V> entry = table[hash];
while (entry != null) {
if (entry.key.equals(key)) {
return entry.value;
}
entry = entry.next;
}
return null;
}
}
>>>操作符
这个操作符的作用是将当前整数,转换为2进制后,右移。比如
2>>>1,就是10右移1位剩下了一个1,就是1。
案例如下:
public static void main(String [] args)
{
System.out.println(4>>> 1);
}
结果是2.
public static void main(String [] args)
{
System.out.println(4>>> 2);
}
结果是1
1.8增加了对其优化。
其原因是hash的的平均分布可能有问题,有可能导致链表过长,从而使得效率变的很低。因此加了一个新的结构红黑树,如果说1.8之前的hashMap是数组+链表,那么现在版本的就是数组+链表+红黑树。当链表长度<8时,默认以前的方式加元素,也就是数组+链表,当链表>8时则将链表转换为红黑树。(红黑树是一个自平衡的二叉查找树,其插入删除时为了保持红黑树的平衡特征,回自旋来保证,可以将其理解为一个具有自平衡的查找2叉树)8时,默认以前的方式加元素,也就是数组+链表,当链表>
hashmap线程不安全
hashmap没有做加锁处理,举个简单例子,2个线程插入一个数据,其hashcode都是一样的,在极端情况下,可能导致插入少了一个数据。
java 1.8源码分析
这里只分析下put方法,get方法很简单,自己去看看。突然觉得put方法也很简单。
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)
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))))
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
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);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
这里我直接说下流程:
- 先判断table是否为空,为空,则创建table。
在判断key所在table的位置是否为null,空的话直接创建链表node。否则进入else。
else里面先判断这个链表node的key是否和要put的key一样,一样则覆盖
- 在判断是否是树节点,是的话,插入树节点的方式插入数据。
- 否则,遍历链表,要么找到一样的key覆盖,要么创建新的节点,如果是创建节点,那么再判断这个链表长度是否>=8,是的话则将链表转换为红黑树。
- 再然后,覆盖的已经返回了,创建新节点的,走下一步,hashmap的size加一。在判断是否大于限定值,大于的话则是则扩张table。
- 好了 over,,,
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
这个是ConcurrentHashMap的put源码。
- 先根据key计算出位置,然后这个位置为null则cas看看可否插入值成功。
- 判断是否需要扩容
- 加锁写数据,和hashmap类似