一 HashMap使用
1) 概述:HashMap是基于哈希表的Map接口的非同步实现(Hashtable跟HashMap很像,唯一的区别是Hashtalbe中的方法是线程安全的,也就是同步的)。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2)数据结构:
链表+数组实现 (底层结构)
jdk1.8开始 常采用数组加链表加红黑树
用链表是为了解决hash冲突
hash冲突:当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。
解决hash冲突的方法:HashMap即是采用了链地址法,也就是数组+链表的方式
3)实现方法:put(K key, V value), get(K key), remove(K key), resize()
4)hashmap常用方法:clear()
从此映射中移除所有映射关系。
containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。
containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
entrySet()
返回此映射所包含的映射关系的 Set 视图。
get(Object key)
返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
remove(Object key)
从此映射中移除指定键的映射关系(如果存在)。
size()
返回此映射中的键-值映射关系数。
HashMap还有一个实现接口是Map.Entry<K,V>:
二 hashmap底层分析
1)散列表结构:数组+链表的结构
2)hash:Hash也称散列、哈希,对应的英文单词Hash,基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出 不同的数据它对应的哈希码值是不一样的 ,哈希算法的效率非常高。
3)底层储存结构:当链表长度到达8时,升级成红黑树结构
4)put数据原理分析:
首先put进去一个key----value
根据key值会计算出一个hash值
经过扰动使数据更散列
构造出一个node对象
最后在通过路由算法得出一个对应的index
自定义put方法:
- 自定义put方法
- 1)key-> hash(key) 散列码 -> hash & table.length-1 index
- 2)table[index] == null 是否存在节点
- 3)不存在 直接将key-value键值对封装成为一个Node 直接放到index位置
- 4)存在 key不允许重复
- 5)存在 key重复 考虑新值去覆盖旧值
- 6)存在 key不重复 尾插法 将key-value键值对封装成为一个Node 插入新节点
5 扩容过程:
1)table进行扩容
2)table原先节点进行重哈希
a.HashMap的扩容指的是数组的扩容,因为数组的空间是连续的,所以需要数组的扩容即开辟一个更大空间的数组,将老数组上的元素全部转移到新数组上来 - b.在HashMap中扩容,先新建一个2倍原数组大小的新数组,然后遍历原数组的每一个位置,如果这个位置存在节点,则将该位置的链表转移到新数组
- c.在jdk1.8中,因为涉及到红黑树,jdk1.8实际上还会用到一个双向链表去维护一个红黑树
中,所以jdk1.8在转移某个位置的元素时,首先会判断该节点是否是一个红黑树节点,然后遍历该红黑树所对应的双向链表,将链表中的节点放到新数组的位置当中 - d.最后元素转移完之后,会将新数组的对象赋值给HashMap中table属性,老数组将会被回收
3)扩容时机:table==null或者table需要扩容的时候
6 HashMap迭代器实现
1)由于哈希表数据分布是不连续的,所以在迭代器初始化的过程中需要找到第一个非空的位置点,避免无效的迭代
2)当迭代器的游标到达某一个桶链表的末尾,迭代器的游标需要跳转到下一个非空的位置点
三 hashmap源码分析
1)类的继承关系
-
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
-
HashMap允许空值和空键
-
HashMap是非线程安全
-
HashMap元素是无序 LinkedHashMap TreeMap
-
(HashTable不允许为空 线程安全)
2)类的属性 -
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 16 默认初始容量 用来给table初始化
-
static final int MAXIMUM_CAPACITY = 1 << 30;
-
static final float DEFAULT_LOAD_FACTOR = 0.75f; //扩容机制
-
static final int TREEIFY_THRESHOLD = 8; //链表转为红黑树的节点个数
-
static final int UNTREEIFY_THRESHOLD = 6;//红黑树转为链表的节点个数
-
static final int MIN_TREEIFY_CAPACITY = 64;
-
static class Node<K,V> implements Map.Entry<K,V>
-
transient Node<K,V>[] table; //哈希表中的桶
-
transient Set<Map.Entry<K,V>> entrySet; //迭代器遍历的时候
-
transient int size;
-
int threshold;
-
final float loadFactor;
-
3)类中重要的方法 (构造函数 put remove resize)
-
构造函数中并未给桶进行初始化
-
put
-
if ((tab = table) == null || (n = tab.length) == 0)
-
n = (tab = resize()).length; //resize() 初始化(和扩容)
-
if ((p = tab[i = (n - 1) & hash]) == null)
-
tab[i] = newNode(hash, key, value, null);//当前位置不存在节点,创建一个新节点直接放到该位置
-
else{
-
//当前位置存在节点 判断key是否重复
-
if (p.hash == hash &&
-
((k = p.key) == key || (key != null && key.equals(k))))
-
e = p;
-
//判断第一个节点的key是否与所要插入的key相等
-
//hashCode 表示将对象的地址转为一个32位的整型返回 不同对象的hashCode有可能相等
-
//比较hash相比于使用equals更加高效
-
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;
-
//判断e是否是key重复的节点
-
p = e;
-
}
-
}
-
}
四 HashMap常见面试题分析
1)JDK1.7与JDK1.8HashMap有什么区别和联系 -
最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;
-
jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;
-
插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
-
jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;
*扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而且1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前;
*jdk1.8是扩容时通过hash&cap==0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;
*扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容
2)用过HashMap没?说说HashMap的结构(底层数据结构 + put方法描述 见上文)
3)说说HashMap的扩容过程(见上文)
4)HashMap中可以使用自定义类型作为其key和value吗?
可以,但必须用自定义类作为key,必须重写equals()和hashCode()方法。Object类的hashCode()方法返回这个对象存储的内存地址的编号。而equals()比较的是内存地址是否相等。通过hashCode()能够计算出一个hash值,通过hash值来判断两个对象的值是否相等,如果hash值不相等则说明这两个对象不相等,如果hash值相等则继续用equals()去判断两个对象是否相等。
5)HashMap中table.length为什么需要是2的幂次方
HashMap存储数据时要避免位置碰撞且数据分配均匀,于是采用位移运算的算法计算存储链表的位置,假设HashMap的长度不为2的幂次方则有可能产生碰撞。
例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;
6)HashMap与HashTable的区别和联系
父类不一样。HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
联系:
*HashMap是Hashtable的轻量级实现,他们都实现了共同的父接口Map。
*.都采用了Hash方法进行索引,底层都是Hash表结构,具有很快的访问速度。
区别:
*.1HashMap允许空键值(最多只允许一条记录的键为null,不允许多条记录的值为null),Hashtable不允许空键值。
*.Hashtable的方法是线程安全的,HashMap的方法是线程不安全的。
- Hashtable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。
*HashMap把Hashtable的contains的方法去掉了改成containsvalue和containsKey。Hashtable继承自Dictionary类,而HashMap是java1.2引进的Map interface的一个实现。
Hashtable中hash数组默认大小是11,扩容方式为old2+1;HashMap中默认大小是16,扩容一定是2的指数。
*Hash值的使用不同,Hashtable直接使用对象的HashCode。
7)HashMap、LinkedHashMap、TreeMap之间的区别和联系?
a联系:
HashMap,LinkedHashMap,TreeMap都属于集合Map接口的子类。都是键值对集合。线程都不安全,效率高。
b 区别:
*HashMap键是哈希表结构,可以保证键的唯一性,key能null,值也可为null。 - LinkedHashMap是HashMap的子类,内部依赖哈希表和链表列实现。由hash保证键的唯一性,由LinkedList保证有序性(存取顺序一致)。key能为Null,value也能为null。
*TreeMap类,键是红黑树结构,可以保证键的排序(自然排序),并且不能重复。key不能为null,value可以为null
8)HashMap与WeakHashMap的区别和联系
a .HashMap
*HashMap是基于Key-Value的散列表(JDK7:数组+链表,JDK8:数组+链表+红黑树bai),采用拉链法实现的。一般用于单线程当中,非线程安全,HashMap的键是"强键"。
*.继承于抽象类AbstractMap,并且实现Map接口。遍历时,取得的数据完全是随机的。
*默认容量大小是16,加载因子是0.75。
*.最多只允许一条key为Null,允许多条value为Null。
*.HashMap实现了Cloneable和Serializable接口,而WeakHashMap没有。
*.HashMap实现Cloneable,说明它能通过clone()克隆自己。
*.HashMap实现Serializable,说明它支持序列化,能通过序列化去传输。
*.添加、删除操作时间复杂度都是O(1)。
b weakHashMap
*.weakHashMap是基于Key-Value的散列表(数组+链表),采用拉链法实现的。一般用于单线程当中,非线程安全,weakHashMap中的键是"弱键"。
备注:当"弱键"被GC会收时,它对应的键值也会从weakHashMap中删除。
*继承于抽象类AbstractMap,并且实现Map接口。
*.默认容量大小是16,加载因子是0.75。
*.最多只允许一条key为Null,允许多条value为Null。
9)WeakHashMap中涉及到的强弱软虚四种引用
Java中四大引用: - 强引用
A a = new A(); //a是强引用
只要是强引用,GC就不会回收被引用的对象 - 软引用SoftReference
一般用户实现Java对象的缓存,缓存可以有可以没有,一般将有用但是非必须的
对象用软引用关联
只要是软引用关联的对象,在Java发生内存溢出异常之前,会将这些对象列入要
回收的范围,如果回收之后发现内存还是不够,才会抛出OOM异常
map -》 SoftReference -》 SoftReference.get() - 弱引用 WeakReference
弱引用是用来一些非必须的对象,比软引用更弱一些
只要是被弱引用关联的对象,只能够生存到下一次垃圾回收之前,一旦发生垃圾回收,
无论当前内存是否够用,都会回收掉被弱饮用关联的对象 - 虚引用 PhantomReference
别名幽灵引用 最弱的引用关系,一个对象是否具有虚引用的存在,完全是不会对其生命
周期产生影响,也无法通过虚引用获取一个对象的实例,它存在的唯一目的就是在对象被
垃圾回收之后收到一个系统通知
特殊的HashMap,WeakHashMap的键是弱引用对象,只能存活到下一次垃圾回收之前
10)HashMap是线程安全的吗?引入HashTable和ConcurrentHashMap(见上文)