HashMap总结

一、HashMap使用

Map接口是哈希表的基本接口。有以下特点:

  1. 底层数据结构是哈希表(数组+链表),哈希表存储键值对: key value
  2. 元素重复问题:key不能重复、value可以重复
  3. 元素有序问题:插入是无序的
  4. 元素是否可以为null:key和value都可以为null
  5. 默认大小:Map中哈希表的初始默认大小为16
  6. 扩容时机:按照2倍关系进行扩容

使用

  1. 新建一个HashMap对象
    Map<String, Integer> map = new HashMap<>();
  2. put方法将键值对放入map集合中map.put("zs", 10);
  3. 已知键,移除键值对map.remove("zs");,返回所要移除键所对应的值。
  4. 获取当前键所对应的值map.get(zs);
  5. 获取键值对组数map.size();
  6. 判断map集合是否为空map.isEmpty()
  7. 判断是否包含键map.containsKey("zs")
  8. 判断是否包含值map.containsValue(10)
  9. 获取所有节点的集合
    Set<Map.Entry<String, Integer>> entries = map.entrySet();
  • 迭代器使用
    HashMap当中所有的元素作为一个entry节点存在,所有节点封装为一个Set
while(iterator.hasNext()){
	//判断是否含有下一个可迭代的元素
    //获取下一个可迭代的元素
    Map.Entry<String, Integer> next = iterator.next();
    //分别获取键和值并打印
    System.out.println(next.getKey()+":: "+next.getValue());
}

二、HashMap底层结构

1.概念

  • 哈希表又被称为散列表,是根据关键码 key直接访问内存中存储位置的数据结构,即通过关于 key的函数,映射到一个地址用来来访问数据,这样加快查找速度。
  • 数组:查找容易(index快速定位),删除和插入不易(需要移动当前节点之后的节点)
  • 链表:查找不易(需要从头节点或尾节点开始遍历,直到找到目标节点),删除和插入容易(只要修改当前节点前后节点的next或prev即可)
  • 哈希表则是对二者的综合,是一个查找容易插入和删除也容易的数据结构。
    index定位索引位置(数组) -> 遍历找到节点进行增、删、改操作(链表)

2.哈希冲突

哈希冲突简单说就是不同关键字得到相同的哈希地址

  • 解决哈希冲突的方法:
    1)开放地址法
    2)链地址法(数组+链表)

3.HashMap的数据结构

  • HashMap本身处理海量数据,当位于同一个位置中的元素越来越多,hash值相等的元素越来越多,使用查找效率降低
  • 某一个位置链表的长度超过阈值8时,会将链表的结构转为红黑树。
  • 二叉排序树(AVL树、红黑树)
    红黑树特性:
    a.红黑树每个节点要么是黑色要么是红色
    b.根节点是黑色
    c.叶子节点是黑色
    d,如果一个节点是红色,叶子节点必须是黑色
    e.每个节点到叶子节点所经过的黑色节点的数目是一样的

4.HashMap的实现

hash函数类比 jdk中HashMap的hash函数,解决哈希冲突采用链地址法,即链表+数组实现。
下面是几个方法的自定义实现:

put(K key, V value)

实现逻辑:

  1. 根据 key获取当前 index(直接使用HashMap里的哈希函数),将key放入哈希函数中得到一个散列码(定义为h),通过 table.length-1 & h 得到 index。
  2. 要将当前 key,value键值对 放到 index位置。在这之前应该先判断:
    判断 index当前位置是否存在值
    (1) 若不存在,直接将当前 key,value封装为一个Node,插入到该index位置;
    (2) 若存在节点(保证HashMap中key不重复) ->
    判断 key是否重复
    a. 如果key有重复,新值覆盖旧值;
    b. 如果key没有重复,将当前 key,value封装为一个Node,尾插法插入当前 index位置。

代码实现:

public void put(K key, V value) {                                    
    int hash = hash(key);//通过key得到散列码                                
    int index = table.length - 1 & hash;                             
    //判断index位置是否存在节点                                                
    if (table[index] == null) {                                      
        //如果不存在,直接将当前key,value封装为一个Node,插入到该index位置                  
        table[index] = new Node(hash, key, value);                   
        size++;                                                      
    } else {                                                         
        //如果存在节点(保证HashMap中key不重复)                                   
        Node<K, V> firstNode = table[index];//获取该位置第一个节点             
        if (firstNode.key.equals(key)) {//当前位置存在节点 判断key是否重复         
            //相等则新值覆盖旧值                                              
            firstNode.value = value;                                 
        } else {                                                     
            Node<K, V> tmp = firstNode;//定义一个临时引用,从头节点开始             
            //遍历当前链表,判断key是否重复                                       
            while (tmp.next != null && !tmp.key.equals(key)) {       
                //tmp一直跑,要么跑到最后一个节点,要么找到一个key与之相等的节点                 
                tmp = tmp.next;                                      
            }                                                        
            if (tmp.next == null) {                                  
                //跑到最后一个节点                                           
                if (tmp.key.equals(key)) {                           
                    //如果key有重复,考虑新值覆盖旧值                              
                    tmp.value = value;//替换最后一个节点的值                   
                } else {                                             
                    //如果key没有重复,将当前key,value封装为一个Node 尾插法 插入当前index位置
                    tmp.next = new Node(hash, key, value);           
                    size++;//添加新节点则size++                            
                }                                                    
            } else {                                                 
                //如果key有重复,新值覆盖旧值                                    
                tmp.value = value;                                   
            }                                                        
        }                                                            
    }                                                                
}                                                                    

get(K key)

get方法就是已知一个key,将key作为该方法的一个参数,返回value值。
实现逻辑:

  1. 与put方法第一步相同,通过key得到一个散列码,由table.length-1 & h 得到 index。
  2. 在 index的所有节点中找与当前key相等的节点,得到该节点的 value并返回。

代码实现:

public V get(K key){                                
    //获取key所对应的value                                
    //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;                                
    }                                               
    //判断第一个节点与要查找的key是否相等                           
    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;//tmp.key.equals(key)                   
        }                                           
    }                                               
}                                                   

remove(K key)

实现逻辑:

  1. 与以上两个方法相同,由key得到散列码,进而得到 index。
  2. 判断该位置是否存在节点,不存在返回 false,存在则在链表中找到所要删除节点,删除并返回 true,遍历到最后一个节点也没找到则返回 false。

代码实现:

public boolean remove(K key){                    
    //key->index                                 
    int hash = hash(key);                        
    int index = table.length - 1 & hash;         
    //当前位置中寻找 当前key所对应的节点                        
    Node<K,V> firstNode = table[index];          
    if(firstNode == null){                       
        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.next.next;//前一个节点和后一个节点相连
            size--;
            return true;                              
        }else {                                  
            firstNode = firstNode.next;          
        }                                        
    }                                            
    return false;                                
}                                                

resize()

实现逻辑:

  1. 对table以2倍的方式扩容(对数组扩容)
  2. 重哈希

代码实现:

public void resize(){
	//HashMap扩容                                                                     
    //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){                  
    //相当于对原先哈希表中每一个有效节点 进行 重哈希的过程  
    //要么在原位置(低位位置)
    //要么在 原位置+扩容后长度(高位位置)                                  
    Node<K,V> currentNode = table[index];  //获取当前节点                          
    if(currentNode == null){//该位置没有节点                                        
         return;                                                     
    }                                                                
    Node<K,V> lowHead = null;//低位的头                                        
    Node<K,V> lowTail = null;//低位的尾                                   
    Node<K,V> highHead = null;//高位的头                               
    Node<K,V> highTail = null;//高位的尾                              
                                                                     
    while (currentNode != null){                                     
        //遍历index位置的所有节点                                             
        int newIndex = hash(currentNode.key) & (newTable.length-1);  
        if(newIndex == index){                                       
            //当前节点链到lowTail之后                                        
            if(lowHead == null){                                     
                lowHead = currentNode;                               
                lowTail = currentNode;                               
            }else{                                                   
                lowTail.next = currentNode;                          
                lowTail = lowTail.next;                              
            }                                                        
        }else{                                                       
            if(highHead == null){                                    
                highHead = currentNode;                              
                highTail = currentNode;                              
            }else{                                                   
                highTail.next = currentNode;                         
                highTail = highTail.next;                            
            }                                                        
        }                                                            
        currentNode = currentNode.next;                              
    }                                                                
    //要么在原位置 (低位位置)                                                  
    if(lowHead != null){                                             
        newTable[index] = lowHead;                                   
    }                                                                
    //要么跑到 原位置 + 扩容后前度 (高位位置)                                         
    if(highHead != null && highTail != null){                        
        newTable[index + table.length] = highHead;                   
        newTable[index + table.length] = highTail;                   
    }                                                                
}                                                                                                                         

三、HashMap源码分析

(一)类的继承关系

  1. 实现Map接口,定义一些通用方法。一部分在HashMap中实现,一部分由AbstractMap实现。
  2. 实现Cloneable接口,表示当前HashMap中所有对象可被克隆,可被拷贝的。
  3. 实现Serializable接口,表示当前HashMap里面所有的对象都是可以被序列化的(即可以将当前对象转化为二进制,永久地保存到磁盘上)
  • HashMap允许空值和空键,是非线程安全的,元素是无序的。(HashTable不允许为空 线程安全)

(二)类的属性

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16 默认初始容量,用来给table初始化
static final int MAXIMUM_CAPACITY = 1 << 30;//table最大容量
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;
transient Node<K,V>[] table;//哈希表中的 桶(数组)
transient Set<Map.Entry<K,V>> entrySet;//用于迭代器遍历时
transient int size;//键值对个数
transient int modCount;//集合结构的修改次数
int threshold;
final float loadFactor;

(三)类中的重要方法(构造函数)

主要函数: put()、remove()、 resize()

  • 构造函数中并未给table进行初始化,在第一次put时对table进行初始化

put方法:
主要调用了一个putVal方法,下面是对putVal方法内容的分析

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) 
    	//resize() 初始化 (扩容)                    
        n = (tab = resize()).length;                                       
    if ((p = tab[i = (n - 1) & hash]) == null)
    	//表示当前位置不存在节点,创建一个新的节点放到该位置                              
        tab[i] = newNode(hash, key, value, null);                           
    else { 
    	//当前位置存在节点,判断key是否重复                                                                 
        Node<K,V> e; K k;                                                   
        if (p.hash == hash &&                                               
            ((k = p.key) == key || (key != null && key.equals(k))))         
            e = p;//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) {  
            //比较当前for循环走过的每一个节点的值与当前的key是否相等                     
                if ((e = p.next) == null) {//此处p为第二个节点                                 
                    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;                                                      
            }                                                               
        }                                                                   
        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;                                                            
}                                                                           

下面是put方法的流程图:
在这里插入图片描述
resize()调用时机

  1. table == null
  2. table需要扩容的时候

扩容过程

  1. table进行扩容
  2. table原先节点进行重哈希
  • HashMap的扩容指的是数组的扩容,因为数组的空间内是连续的,所以需要数组的扩容即开辟一个更大空间的数组,将老数组上的元素全部转移到新数组上来。
  • 在HashMap中扩容,先新建一个2倍原数组大小的新数组,然后遍历原数组的每一个位置,如果这个位置存在节点,则将该位置的链表转移到新数组。
  • 在jdk1.8中,因为涉及到红黑树,jdk1.8实际上还会用到一个双向链表去维护一个红黑树中的元素,所以jdk1.8在转移某个位置的元素时,首先会判断该节点是否是一个红黑树解节点,然后遍历该红黑树所对应的双向链表,将链表中的节点放到新数组的位置当中。
  • 最后元素转移完之后,会将新数组的对象赋值给HashMap中table属性,老数组将会被回收。

四、HashMap迭代器实现

特点:

  1. 由于哈希表数据分布是不连续的,所以在迭代器初始化的过程中需要找到第一个非空的位置点,避免无效的迭代。
  2. 当迭代器的游标到达当前某一个桶链表的末尾,迭代器的游标需要跳转到下一个非空的位置点。

代码实现:

class Itr implements Iterator{                               
    private int cursor;//表示当前遍历到的元素                          
    private Node<K,V> currentNode;//具体的元素节点                  
    private Node<K,V> nextNode;//下一个元素节点                     
                                                             
    public Itr() {                                           
        //由于哈希表数据分布是不连续的,所以在迭代器初始化的过程中需要找到第一个非空的位置点,避免无效的迭代
        if (MyHashMap.this.size <= 0) {                      
            return;                                          
        }                                                                                    
        for (int i = 0; i < table.length; i++) {             
            if (table[i] != null) {                          
                cursor = i;                                  
                nextNode = table[i];                         
                return;                                      
            }                                                
        }                                                    
    }                                                        
    @Override                                                
    public boolean hasNext(){                                
        return nextNode != null;                             
    }                                                        
                                                             
    @Override                                                
    public Node<K,V> next() {                                
        //暂时保存需要返回的元素节点                                      
        currentNode = nextNode;                              
        //nextNode往后走一个,如果没有到达末尾nextNode                     
        nextNode = nextNode.next;                            
        //当迭代器的游标到达某一个桶链表的末尾                                 
        if (nextNode == null) {                              
            //迭代器的游标跳转到下一个非空的位置点                           
            for (int j = cursor + 1; j < table.length; j++) {
                if (table[j] != null) {                      
                    //table[j]表示该位置的第一个元素                    
                    nextNode = table[j];                     
                    cursor = j;                              
                    break;                                   
                }                                            
            }                                                
        }                                                    
        return currentNode;                                  
    }                                                        
                                                             
}                                                            

五、HashMap常见面试题分析

1.JDK1.7与JDK1.8HashMap有什么区别和联系?

  1. JDK1.7采用数组+链表形式解决哈子冲突。JDK1.8数组+链表改成了数组+链表或红黑树;前者解决hash冲突的方式存在一定的问题: 定位到索引位置之后需要遍历找到节点,若链表过长,也就是hash冲突严重,这时候就有查找性能的问题,查找性能链表为O(n),红黑树为O(logn),使用红黑算法可以提高操作效率。
  2. 1.7头插法扩容时,会使链表发生反转,多线程环境下会产生环;1.8将表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
  3. 在插入时,1.7先判断是否需要扩容,再插入1.8先进行插入,插入完成再判断是否需要扩容
  4. 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;

2.说说HashMap的扩容过程

使用resize() 方法: 2的幂次方机制扩容

//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;
  1. HashMap的扩容指的是数组的扩容,因为数组的空间内是连续的,所以需要数组的扩容即开辟一个更大空间的数组,将老数组上的元素全部转移到新数组上来。
  2. 在HashMap中扩容,先新建一个2倍原数组大小的新数组,然后遍历原数组的每一个位置,如果这个位置存在节点,则将该位置的链表转移到新数组。
  3. 在jdk1.8中,因为涉及到红黑树,jdk1.8实际上还会用到一个双向链表去维护一个红黑树中的元素,所以jdk1.8在转移某个位置的元素时,首先会判断该节点是否是一个红黑树解节点,然后遍历该红黑树所对应的双向链表,将链表中的节点放到新数组的位置当中。
  4. 最后元素转移完之后,会将新数组的对象赋值给HashMap中table属性,老数组将会被回收。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值