目录
哈希表
又称“散列表”,是根据关键码key直接访问内存中存储位置的数据结构,即通过关于key的函数,映射到一个地址来访问数据,这样加快查找速度
数组:查找容易,删除和插入不易
链表:查找不易,插入和删除容易
哈希表是对二者的综合,使得作为一个查找容易、插入和删除也容易的数据结构
哈希冲突
是指对不同的关键字得到同一散列地址;
无冲突时复杂度O(1),冲突较多时时间复杂度变为O(n)
常见解决方法有:
链地址法(也是HashMap在jdk1.8之前的实现结构)
开放地址法
1.HashMap使用
package collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
//哈希表 Map接口的简单使用
public class Test_Map {
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();//Map父类
map.put("zhangsan", 10);
map.put("lisa", 20);
map.put("wangwu", 50);
System.out.println(map.get("lisi"));
System.out.println(map.remove("lisi"));
System.out.println(map.size());
System.out.println(map.isEmpty());
System.out.println(map.containsKey("tulun"));
System.out.println(map.containsValue(50));
//返回Map中所包含的键值对所组成的Set集合,每个集合元素都是Map.Entry()对象,其中Entry是Map的内部类
Set<Map.Entry<String,Integer>> entries=map.entrySet();
//Set属于Collection,而COllection含有Iterator方法
Iterator<Map.Entry<String,Integer>> iterator=entries.iterator();
while(iterator.hasNext())
{
Map.Entry<String,Integer> next=iterator.next();
System.out.println(next.getKey()+"::"+next.getValue());
}
}
}
2.HashMap底层结构
3.HashMap源码分析
1)类的继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
AbstractMap实现Map接口部分方法,剩余方法在HashMap实现
Serializable表示HashMap中所有对象是可被序列化,即可永久的保存在磁盘上
Cloneable表示HashMap中所有对象是可被克隆(拷贝)的
HashMap允许空值和空键
HashMap是非线程安全
HashMap元素是无序,不能保证每次获取是一样的顺序 ;LinkedHashMap(插入和删除有序) TreeMap(大小排列有序)
(HashTable不允许为空 线程安全(即若多个线程操作某一个集合,最终结果和期望结果是相同的))
2)类的属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 即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>//如里面的get set方法
transient是暂时的意思
transient Node<K,V>[] table; //哈希表中的数组即桶(看是桶哪个位置以及此位置的链表上的何处)
transient Set<Map.Entry<K,V>> entrySet; //用于迭代器遍历。因为Set继承自collection,Collection提供iterator方法。
transient int size; 键值对个数
transient int modCount; 集合结构的修改次数(如put remove,而set只修改某个节点,对结构没有修改 )
int threshold; 桶大小的一个阈值
final float loadFactor;
3)类中重要的方法 (构造函数 put remove resize)
构造函数中并未给桶(即table)进行初始化,而是在第一次put中初始化
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是否重复
//判断第一个节点的key是否与所要插入的key相等,先判断hash,在进一步判断key
//hash中调用了hashCode方法 ,其能将对象的地址转为一个32位的整型返回 不同对象的hashCode有可能相等
//比较hash相比于使用equals更加高效
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) {//指binCount桶里的个数
//只有一个节点或跑到末尾了
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//大于8,小于64,先扩容;大于64再转化为红黑树 HashMap 755行
break;//在桶的深度(大)和链的长度(短)之间平衡,保证O(1)
}
//判断e是否是key重复的节点
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;//出去后执行新值覆盖旧值
p = e;//往后走一个
}
}
}
resize
resize时机
1)table==null
2)tqble需要扩容时
过程
参数合法检测
1)table进行扩容
2)table原先结点进行重哈希(红黑树)
注意:
a.HashMap的扩容指的是数组的扩容,因为数组的空间是连续的,所以需要数组的扩容即开辟一个更大空间的数组,将老数组上的元素全部转移到新数组上来
b.在HashMap中扩容,先新建一个2倍原数组大小的新数组,然后遍历原数组的每一个位置,如果这个位置存在节点,则将该位置的链表 转移到新数组
c.在jdk1.8中,因为涉及到红黑树, jdk1.8实际上还会用到一个双向链表去维护一个红黑树中的元素,所以jdk1.8在转移某个位置的元素时,首先会判断该节点是否是一个红黑树解节点,然后遍历该红黑树所对应的双向链表,将链表中的节点放到新数组的位置当中
d.最后元素转移完之后,会将新数组的对象赋值给HashMap中table属性,老数组将会被回收
HashMap的简单实现
重哈希
以每次扩容为2倍为例,index有以下特点:
注意:图中表长由4到8;
带框的数是指该位置存放的数,不带框的表示该处下标编号
put方法
(1)key-> hash(key)得到 散列码 -> hash & table.length-1得到 index
(2)table[index] == null 是否存在节点
a.不存在 直接将key-value键值对封装成为一个Node 直接放到index位置
b. 存在
key重复:考虑新value去覆盖旧值
key不重复: 尾插法 (将key-value键值对封装成为一个Node 插入新节点)
HashMap迭代器实现
1)由于哈希表数据分布是不连续的,所以在迭代器初始化的过程中需要找到第一个非空的位置点,避免无效的迭代
2)当迭代器的游标到达某一个桶链表的末尾,迭代器的游标需要跳转到下一个非空的位置点
自定义的hash算法类比HashMap中hash算法,用链地址法解决哈希冲突,实现了put(key, value), get(K key), remove(K Key)等方法
package collection;
import java.util.Iterator;
/**
* 源码思想:
* 基于哈希表(或散列表),具体来讲jdk1.8之前采用数组+链表(或开发地址法等)的结构解决哈希冲突,jdk1.8开始 采用数组+链表+红黑树
* 期望:key->f(key)->index O(1)
* 链过于长时:key->f(key)->index->LinkedList的时间复杂度接近 O(N)
* 对此引入-》红黑树 O(log2 N) (在jdk1.8 链表长》=8时,自动转化为红黑树)
*/
//<K> the type of keys maintained by this map
//<V> the type of mapped values
class MyHashMap<K,V> {
//属性
private int size; //有效节点个数 表示map中有多少个键值对
private Node<K, V>[] table;//数组 引用 《==》HashMap底层的桶
private static final int initalCapacity=16;
//结点
class Node<K, V> {
protected K key;
protected V value;
private Node<K, V> next;//相当于链表
private int hash;//值的哈希地址
public Node(int hash, K key, V value) {
this.hash = hash;
this.key = key;
this.value = value;
}
}
//构造函数
public MyHashMap() {
this(initalCapacity);//数组默认初始容量
}
public MyHashMap(int capacity) {
table = new Node[capacity];
}
//直接复制HasMap源码里的
public int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//自己简单实现的,没有用到红黑树
public void put(K key, V value){
//key->Hash值->index
int hash = hash(key);//散列码
int index = table.length-1 & hash;//按位与 达到取余的效果
//当前数组index位置 不存在节点
Node<K, V> firstNode = table[index];//当前位置第一个结点
if(firstNode == null){
//table[index]位置不存在节点 直接插入
table[index] = new Node(hash, key, value);
size++;
return;//退出
}
//若结点存在要保证key不重复的
if(firstNode.key.equals(key)){
firstNode.value = value;//key同,值覆盖
}else{
//遍历当前 链表
Node<K, V> tmp = firstNode;
//找到最后一个结点 或者 找到一个与key相等的结点
while(tmp.next != null && !tmp.key.equals(key)){
tmp = tmp.next;
}
//对应上面循环退出的两种情况
if(tmp.next == null){
//表示最后一个节点之前 的所有节点都不包含key
if(tmp.key.equals(key)){
//最后一个节点的key与当前所要插入的key是否相等,考虑新值覆盖旧值
tmp.value = value;
}else{
//如果不存在,new Node,尾插法 插入链表当中
tmp.next = new Node(hash, key, value);
size++;//有效个数
}
}else{
//如果存在,考虑新值覆盖旧值
tmp.value = value;
}
}
}
//获取key所对应的value
public V get(K key){
//找位置:key->index
int hash = hash(key);
int index = table.length-1 & hash;
//在index位置的所有节点中找与当前key相等的key
Node<K,V> firstNode = table[index];
//当前位置点是否存在节点 不存在
if(firstNode == null){
return null;
}
//判断第一个节点
if(firstNode.key.equals(key)){
return firstNode.value;
}else{
//遍历当前位置点的链表进行判断
Node<K,V> tmp = firstNode.next;
while(tmp != null && !tmp.key.equals(key)){
tmp = tmp.next;
}
if(tmp == null){
return null;
}else{
return tmp.value;
}
}
}
public boolean remove(K key){
//key->index
//当前位置中寻找当前key所对应的节点
int hash = hash(key);
int index = table.length-1 & hash;
Node<K,V> firstNode = table[index];
if(firstNode == null){
//表示table桶中的该位置不存在节点
return false;
}
//删除的是第一个节点
if(firstNode.key.equals(key)){
table[index] = firstNode.next;
size--;
return true;
}
//相当于在链表中删除 中间 某一个节点
while(firstNode.next != null){
if(firstNode.next.key.equals(key)){
//firstNode.next是所要删除的节点
//firstNode是它的前一个节点
//firstNode.next.next是它的后一个节点
firstNode.next = firstNode.next.next;
size--;
return true;
}else{
firstNode = firstNode.next;
}
}
return false;
}
//HashMap的扩容
public void resize(){
//table进行扩容 2倍的方式 扩容数组
Node<K, V>[] newTable = new Node[table.length*2];
//index -> table.length-1 & hash
//对原 先哈希表中 每一个 有效节点进行 重哈希
for(int i=0; i<table.length; i++){
rehash(i, newTable);
}
this.table = newTable;
}
public void rehash(int index, Node<K,V>[] newTable){
//暂存老index对应的结点
Node<K,V> currentNode = table[index];
if(currentNode == null){
return;
}
//每调用一次rehash都要重置一次
//用于标记index位置的链上的头尾,用于链接结点
Node<K,V> lowHead = null; //低位的头
Node<K,V> lowTail = null;//低位的尾
Node<K,V> highHead = null;//高位的头
Node<K,V> highTail = null;//高位的尾
//index对应 的currentNode结点若为空,不操作
//不空,则遍历index位置的 所有节点并尾插
while(currentNode != null){
//计算扩容后的index值,看wps示意图
int newIndex = hash(currentNode.key) & (newTable.length-1);
//新老index一样,即在原位置 (低 位位置)
if(newIndex == index){
//当前节点链到lowTail之后
if(lowHead == null){//之前在该处没存
lowHead = currentNode;
lowTail = currentNode;
}else{//链不空,直接找末尾
lowTail.next = currentNode;
lowTail = lowTail.next;//指向新尾
}
}else{
//新老index不同,即跑到原位置 + 扩容前长度 (高 位位置)
//当前节点链到highTail之后
if(highHead == null){
highHead = currentNode;
highTail = currentNode;
}else{
highTail.next = currentNode;
highTail = highTail.next;
}
}
//index位置所在链上的下一个
currentNode = currentNode.next;
}
//因为长度扩容为2倍
//index要么在原位置 (低 位位置)
if(lowTail != null){//tail不为空是lowHead肯定不为空
lowTail.next = null;//因为上面没处理.next
newTable[index] = lowHead;//lowHead初始为空,若进入while循环就有值了。需将链的头结点和index低位置关联,就能从顺着链找了
}
//要么跑到原位置 + 扩容前长度 (高 位位置)
if(highTail != null){
highTail.next = null;
newTable[index + table.length] = highHead;
}
}
public Iterator<Node<K,V>> iterator(){
return new Itr();
}
class Itr implements Iterator<Node<K,V>> {
private int cursor; //游标 指向当前遍历到的元素所在位置点
private Node<K,V> currentNode; //具体的元素节点
private Node<K,V> nextNode; //下一个元素节点
public Itr(){
//由于哈希表数据分布是不连续的,所以在迭代器初始化的过程中需要找到第一个非空的位置点,避免无效的迭代
//currentIndex currentNode nextNode 初始化
if(MyHashMap.this.size <= 0){
return;
}
for(int i=0; i<table.length; i++){
if(table[i] != null){
cursor=i;
nextNode = table[i];
return;
}
}
}
//重写,下面331行调用
@Override
public boolean hasNext() {//下一个非空结点
return nextNode != null;
}
//返回下一个结点,下面334行调用
@Override
public Node<K,V> next() {
//暂时保存需要返回的元素节点
currentNode = nextNode;
//更新下一次要用的nextNode
nextNode = nextNode.next;
//迭代器的游标到达某一个桶链表的末尾
if(nextNode == null){
//迭代器的游标需要跳转到下一个非空的位置点
for(int j=cursor+1; j<table.length; j++){
if(table[j] != null){
//table[j]表示该位置的第一个元素
cursor = j;
nextNode = table[j];
break;
}
}
//如果仍找不到非空位置,289行hasNext()就知道桶找完了
}
return currentNode;
}
}
}
public class Teacher_1_13_HashMap {
public static void main(String[] args) {
MyHashMap<Integer, String> map = new MyHashMap<>(16);
map.put(1, "dksjfkjd");
map.put(17, "jd");
map.put(43, "tree");
map.put(21, "hgf");
map.put(67, "uytr");
map.put(7, "iiuyt");
map.put(19, "ygv");
map.put(25, "rdfc");
map.put(33, "edx");
map.put(77, "asdf");
Iterator<MyHashMap<Integer, String>.Node<Integer, String>> itr = map.iterator();
while(itr.hasNext()){
MyHashMap<Integer, String>.Node<Integer, String> next = itr.next();
System.out.println(next.key + ":: "+next.value);
}
}
}
输出结果:
1:: dksjfkjd
17:: jd
33:: edx
67:: uytr
19:: ygv
21:: hgf
7:: iiuyt
25:: rdfc
43:: tree
77:: asdf
4.HashMap常见面试题分析
1)JDK1.7与JDK1.8HashMap有什么区别和联系
2)用过HashMap没?说说HashMap的结构(底层数据结构+ put方法描述)
3)说说HashMap的扩容过程
4) HashMap中可以使用自定义类型作为其key和value吗?
5) HashMap中table .length为什么需要是2的幂次方
6) HashMap与HashTable的区别和联系
7) HashMap、LinkedHashMap、TreeMap之间的区别和联系?
8) HashMap与WeakHashMap的区别和联系
9) WeakHashMap中涉及到的强弱软虚四种引用
10) HashMap是线程安全的吗?引入HashTable和ConcurrentHashMap