哈希表的使用与剖析

本文详细探讨了HashMap的底层实现、构造原理、put、remove和resize方法,以及它与HashTable、LinkedHashMap和TreeMap的区别。涵盖了JDK1.8前后的变化、自定义哈希算法和常见面试问题剖析。
摘要由CSDN通过智能技术生成

哈希表

  又称“散列表”,是根据关键码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 :

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、LinkedHashMapTreeMap之间的区别和联系?
8) HashMap与WeakHashMap的区别和联系
9) WeakHashMap中涉及到的强弱软虚四种引用
10) HashMap是线程安全的吗?引入HashTable和ConcurrentHashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值