HashMap:
jdk1.7和1.8的区别:
jdk7的HashMap使用的数据结构是:数组+链表
jdk8中会将链表转成红黑树(链表的长度超过8的时候),数组+链表+红黑树
为什么转成红黑树?链表的插入效率很高,但是查询效率较低,完全平衡二叉树的查询效率很高,但是插入效率很低,于是在链表和完全平衡二叉树之间做了一个折中,采用的是红黑树。
但是remove操作之后,如果红黑树的节点个数小于6,那么又会转成链表。
jdk1.8之后为什么会在链表和红黑树之间进行转换呢?主要是提高HashMap的查询和插入的性能
查询效率:完全平衡二叉树>红黑树>链表
插入效率:链表>红黑树>完全平衡二叉树
新节点插入链表的位置不同(1.7是头插,1.8是尾插)
1.8的hash算法简化
resize的逻辑修改(jdk7会出现死锁,jdk8不会,jdk7链表元素位置会逆序)
HashMap的几个重要参数:
// HashMap默认的数组初始值大小.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//在构造函数中未指定时使用的负载系数.
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//如果添加元素时链表的节点个数大于8,将链表转成红黑树
static final int TREEIFY_THRESHOLD = 8;
//如果删除元素时红黑树的节点个数小于6,将红黑树转化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//红黑树最小长度为 64
static final int MIN_TREEIFY_CAPACITY = 64;
jdk7与jdk8的hashmap的数据结构示意图:
代码示例:
public class HashMapTest {
public static void main(String[] args) {
Map<String,String> map=new HashMap<>();
map.put("周瑜","周瑜");
map.put("刘备","刘备");
map.put("孙权","孙权");
map.put("曹操","曹操");
map.put("张飞","张飞");
for (String key:map.keySet()) {
int code=key.hashCode(); //生成每个key的hashcode
假设此时有一个容量为8的hashMap
int index=code%8; //将每一个hashcode余8
System.out.println(String.format("%s的hashcode是%s,index=%s",key,code,index));
}
}
}
运行结果:
孙权的hashcode是751370,index=2
张飞的hashcode是794046,index=6
刘备的hashcode是674287,index=7
曹操的hashcode是842996,index=4
周瑜的hashcode是699636,index=4
上述代码的hashmap的示例图:
jdk1.7
jdk1.8
HashMap的扩容的时机:
当HashMap中的元素个数达到:加载因子 * HashMap的容量 时,HashMap会扩容为:HashMap容量*2。
hashmap的扩容示意图:
jdk1.7
jdk1.8
上述示意图可以看出jdk1.7的hashamp扩容会导致链表部分的元素的顺序发生逆序,1.8则不会。
HashMap为什么是线程不安全的:
在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
加载因子为什么是0.75:
hashmap的源码:
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
*
使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时给出了桶中元素个数和概率的对照表。
从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
加载因子是表示Hash表中元素的填满的程度。
加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。
冲突的机会越大,则查找的成本越高。反之,查找的成本越小。
因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。
HashMap为什么要重写hashCode 和equals方法:
HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,在根据 equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。
equals如果一般不重写的话,它是通过==来比较两个对象的引用地址
public boolean equals(Object obj) {
return (this == obj);
}
手写简单的HashMap:
public class MyHashMap<K,V> {
//数组
private Entry[] table;
//hashMap的大小
private int size;
//数组的初始化大小
private static Integer CAPACITY =8;
//构造方法初始化数组
public MyHashMap() {
this.table = new Entry[CAPACITY];
}
//获取hashmap的元素的个数
public int size(){
return size;
}
//获取hashmap的元素
public Object get(K k){
int code=k.hashCode(); //生成每个key的hashcode
int i=code%8; //将每一个hashcode余8 获取数组的下标
for(Entry<K,V> entry=table[i];entry!=null;entry=entry.next){
if(entry.k.equals(k)){
return entry.v;
}
}
return null;
}
//添加值到hashmap
public Object put(K k,V v){
int code=k.hashCode(); //生成每个key的hashcode
int i=code%8; //将每一个hashcode余8 获取数组的下标
//put相同的key,但是value不同,新的value替换旧的value 并返回新的值
//遍历链表
for(Entry<K,V> entry=table[i];entry!=null;entry=entry.next){
if(entry.k.equals(k)){
V old=entry.v;
entry.v=v;
return old;
}
}
addEntry(k,v,i);
return null;
}
//添加节点
public void addEntry(K k,V v,int i){
//增加节点
Entry entry=new Entry<> (k, v, table[i]);
//头节点下移
table[i]=entry;
size++;
}
//定义Entry(链表节点)
class Entry<K,V> {
private K k;
private V v;
private Entry<K,V> next;
public Entry(K k, V v, Entry<K, V> next) {
this.k = k;
this.v = v;
this.next = next;
}
}
//测试
public static void main(String[] args) {
MyHashMap<String,String> hashMap=new MyHashMap<>();
hashMap.put("key","value");
System.out.println(hashMap.get("key"));
System.out.println( hashMap.put("key","value1"));
System.out.println(hashMap.get("key"));
System.out.println(hashMap.size);
}
}