手写HashMap JDK1.7(无红黑树)

Posted 蔡徐坤1987

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写HashMap JDK1.7(无红黑树)相关的知识,希望对你有一定的参考价值。

public interface MyMap <K,V>

    V get(K k);
    V put(K k, V v);
    int size();
    V remove(K k);
    boolean isEmpty();
package main.java.com.hashmap;

public class MyHashMap <K, V> implements MyMap<K, V>

    final static int DEFAULT_CAPACITY = 16;
    final static float DEFAULT_LOAD_FACTORY = 0.75f;

    private  int capacity;
    private float loadFactor;
    private int size = 0;

    Entry<K,V>[] table;

    public MyHashMap() 
        this(DEFAULT_CAPACITY,DEFAULT_LOAD_FACTORY);
    

    public MyHashMap(int capacity, float loadFactor)
        this.capacity = upperMinPowerOf2(capacity);     //获取为2的幂次方的容量大小
        this.loadFactor = loadFactor;                  //加载因子,用于扩容,本次实现中尚未用到该字段
        this.table = new Entry[capacity];
    

    private int upperMinPowerOf2(int capacity) 
        int power = 1;
        while (power <= capacity)
            power *= 2;
        
        return power;
    

    @Override
    public V get(K k) 
        int index = k.hashCode() % table.length;
        Entry<K, V> current = table[index];
        //遍历链表直至找到相同的hashcode
        while(current != null)
            if(current.k == k)
                return current.v;
            
            current = current.next;
        
        return null;
    

    @Override
    public V put(K k, V v) 
        // 通过k换算hashcode取模绑定数组位置
        int index = k.hashCode() % table.length;
        Entry<K,V> current = table[index];
        //如果index下没有绑定元素,则直接头插法
        if(current == null)
            table[index] = new Entry<K,V>(k,v,null);
            size ++;
            return null;
        
        //如果index下有元素
        if(current != null)
            while(current != null)
                if(current.k == k)
                    V oldValue = current.v;
                    current.v = v;
                    return oldValue;
                
                current = current.next;
                table[index] = new Entry<K,V>(k,v,table[index]);
                size ++;
                return null;
            
        
        return null;
    

    @Override
    public int size() 
        return size;
    

    @Override
    public V remove(K k) 
        int index = k.hashCode() % table.length;
        Entry<K, V> current = table[index];
        V result = null;
        Entry<K,V> pre = null;

        while(current != null)
            if(current.k == k)
                result = current.v;
                size --;
                if(pre!=null)
                    pre.next = current.next;
                else
                    table[index] = current.next;
                
                return result;
            
            //向下遍历
            pre = current;
            current = current.next;
        
        return null;
    

    @Override
    public boolean isEmpty() 
        return size == 0;
    

    class Entry<K, V>
        K k;
        V v;
        Entry<K,V> next;

        public Entry(K k, V v, Entry<K, V> next) 
            this.k = k;
            this.v = v;
            this.next = next;
        
    
package main.java.com.hashmap;

public class HashMapTest 

    public static void main(String[] args) 
        MyHashMap<Integer, Integer> hashMap = new MyHashMap<>();
        hashMap.put(1,101);
        hashMap.put(2,202);
        hashMap.put(3,303);
        hashMap.put(1,111);

        int[] keys = new int[]1,2,3;
        for (int i = 0; i < keys.length; i++) 
            System.out.println(keys[i]+"---"+hashMap.get(keys[i]));
        

        hashMap.remove(1);
        hashMap.remove(3);
        System.out.println("hashMap size:"+hashMap.size());
        System.out.println(1+": "+hashMap.get(1));
        System.out.println(3+": "+hashMap.get(3));
        System.out.println(2+": "+hashMap.get(2));
    

理解:

HashMap在JDK1.8之前是数组+链表的数据结构,比如数组长度是10 (默认长度是1<<4 16), 在put存储的时候会将key换算成hashcode,并取模length的余数 0~9之间,以便于分散在数组的各个下标中;

而1%10和 11%10 的余数都是1,这种情况下会产生哈希碰撞也叫哈希冲突,1.7中才用单向链表存储这类数据,并使用头插法,查到链表的头部,而其余的元素向下移动一位;

当map中元素过多,且大于数组的75%时,数组会扩容 2的N次幂,此时,比如数组长度由10变成了20,那么取模的范围变成0~19,所有10以上的元素位置发生了变化重新计算位置也就是再哈希,其map在重新计算位置时,查询或者插入的效率会有抖动。

JDK1.8以后,hashmap是数组+链表+红黑树,对查询效率,时间复杂度进行了优化,也就是说 在 n>8 时 查询效率由n/2 优化为了 olog(n),之所以还有8位的链表,是因为n<8时,n/2 的效率是大于 olog(n)的

HashMap实现原理(jdk1.7/jdk1.8)

HashMap的底层实现: 

1、简单回答 

  JDK1.7:HashMap的底层实现是:数组+链表
  JDK1.8:HashMap的底层实现是:数组+链表/红黑树
  
  为什么要红黑树?
  红黑树:一个自平衡的二叉树
  当结点多了用红黑树,少了用链表
  因为少的话用红黑树太复杂,多了话用红黑树可以提高查询效率。
  红黑树:(自动调整根结点,保证左右两边的结点数差不多),它会左旋,右旋来实现。


   

 JDK1.7:HashMap的底层实现是:数组+链表

2、复杂回答v1.0版

 
 每一个对映射关系的存储位置:
  存储的位置:
(1)先计算key的hash值,通过hashCode()就可以得到,
(2)再用hash值经过各种(异或)操作,得到一个int数字,目的是使得更均匀的分布在数组的每一个元素中。
(3)再用刚才的int值,与table数组的长度-1做一个“按位与&"
运算,目的是得到一个[i]
    因为数组的长度是2的n次方,长度-1的数的二进制是前面都是0,后面都是1,任何数与它做“&”,结果一定是[0,该数]之间
 
 
 为什么会出现链表?
  (1)两个不同的key对象,仍然可能出现hashCode值一样
  (2)就算hashCode不一样,经过刚才的计算,得到[i]是一样的
  (3)而我们的数组的元素table[i]本来只能放一个元素,那么现在有多个元素需要存储到table[i]中,只能把这几个元素用链表连接起来
 
   简单比喻:
   y = f(x)
  两个不一样的x,可能得到一样的y
 
 那么存储到HashMap中的是什么样的元素呢?
  (1)首先是Map.Entry类型:映射项(键-值对)。
  (2)其次HashMap有一个内部类HashMap.Entry实现了Map.Entry接口类型
 
  内部类Entry由四部分组成:
  (1)key
  (2)value
  (3)下一个Entry:next
  (4)hash值计算的整数值,为了后面查询快一点
 
 
  如何避免重复?
  如果两个key的hash值是一样的,还要调用一下equlas()方法,如果返回true,就会把新的value替换旧的value。
  如果两个key的hash值是一样的,还要调用一下equlas()方法,如果返回false,就会把新的Entry连接到旧的Entry所在链表的头部(first)
 如果两个key的hash值是不一样的,但是[i]是一样的,就会把新的Entry连接到旧的Entry所在链表的头部(first)
  如果两个key的hash值是不一样的,并且[i]不一样的,肯定存在不同table[i]中
 
  我们把table[i]称为“桶bucket”。
 
 
  回忆:两个对象的hash值:
  (1)如果两个对象是“相等”,他们的hash值一定是一样
   (2)如果两个对象的hash值是一样,他们可能是相同的对象,也可能是不同的对象。
 


  3、复杂追踪源代码v2.0版


  (1)什么时候扩容
  当元素的个数达到“阈值”,并且新添加的映射关系计算出来的table[i]位置是非空的情况下,再扩容
  默认加载因子 (0.75):DEFAULT_LOAD_FACTOR
  阈值 = table.length * 加载因子(loadFactor)
  第一次阈值:16 * 0.75 = 12
  初始容量:DEFAULT_INITIAL_CAPACITY:16
  第二次阈值:32 * 0.75 = 24
  ...

 HashMap中table的长度有一个要求:必须是2的n次方
 
 
  (2)跟踪一下put方法的源代码
  第一步:如果table是空的,会先把table初始化为一个长度为16的数组,如果你指定的长度不是2的n次方,会往上纠正为最接近的2的n次方
          并且把阈值 threshold= table.length * 加载因子(loadFactor) = 12。
  第二步:如果key是null,首先确定的位置是table[0],
     如果原来table[0]已经有key为null的Entry,用新的value替换旧的value
   如果原来table[0]没有key为null的Entry,那么创建一个新的Entry对象,作为table[0]桶下面的链表的头,原来的那些作为它next。
 
  第三步:如果key不是null,那么用key的hashCode值,通过hash()函数算出一个int值称为"hash"
  第四步:通过刚才的“hash”的int值与table.length-1做&运算,得到一个下标index,表示新的映射关系即将存入table[index]
  第五步:循环判断table[index]位置是否为空,并且是否有Entry的key与我新添加的key是否“相同”,如果相同,就用新的value替换旧的value
  第六步:添加新的映射关系
    (1)判断是否要扩容:        
        当元素的个数达到“阈值”,并且新添加的映射关系计算出来的table[i]位置是非空的情况下,table再扩容为原来的2倍长
        如果扩容了,那么要重新计算hash和index
    (2)把新的映射关系创建为一个Entry的对象放到table[index]的头部。

 

 

 JDK1.8:HashMap的底层实现是:数组+链表/红黑树 


  1、复杂回答v1.0


  (1)映射关系的类型
 添加到HashMap1.8种的映射关系的对象是HashMap.Node类型,或者是HashMap.TreeNode类型。
  它也是Map.Entry接口的实现类。
 
 (2)映射关系添加到table[i]中时,如果里面是链表,新的映射关系是作为原来链表的尾部
  “七上八下”:JDK1.7在上,JDK1.8在下。
  
  为什么要在下面,因为如果是红黑树,那么是在叶子上,保持一致,都在下面。
 
 (3)扩容的时机不同
  第一种扩容:元素个数size达到阈值threshod = table.length * 加载因子  并且table[i]是非空的
 第二种扩容:当某个桶table[index]下面的结点的总数原来已经达到了8个,这个时候,要添加第9个时,会检查
  table.length是否达到64,如果没达到就扩容。如果添加第10个时,也会检查table.length是否达到64,如果没达到就扩容。
 为什么,因为一旦扩容,所有的映射关系要重新计算hash和index,一计算原来table[index]下面可能就没有8个,或新的映射关系也可能不在table[index],
  这样就可能均匀分布。
 
 (4)什么时候从链表变成红黑树
 当table[index]下面结点的总数已经达到8个,并且table.length也已经达到64,那么再把映射关系添加到
  table[index]下时,就会把原来的链表修改红黑树
 
  (5)什么时候会从红黑树变为链表
  当删除映射关系时table[index]下面结点的总数少于6个,会把table[index]下面的红黑树变回链表。


2、put的源代码v2.0


 第一步:计算key的hash,用了一个hash()函数,目的得到一个比hashCode更合理分布的一个int值
 第二步:如果table是空的,那么把table初始化为长度为16的数组,阈(yu)值初始化为= 默认的长度16 * 默认的加载因子0.75 = 12
 DEFAULT_INITIAL_CAPACITY:默认的初始化容量16
 DEFAULT_LOAD_FACTOR:默认加载因子 0.75
 第三步:查看table[index]是否为空,如果为空,就直接new一个Node放进去
     index = hash的int值 & table.length-1
  第四步:先查看table[index]的根节点的key是否与新添加的映射关系的key是否“相同”,如果相同,就用新的value替换原来的value。
   第五步:如果table[index]的根节点的key与新添加的映射关系的key不同,
  还要看table[index]根结点的类型是Node还是TreeNode类型,
  如果是Node类型,那么就查看链表下的所有节点是否有key与新添加的映射关系的key是否“相同”,如果相同,就用新的value替换原来的value。
  如果是TreeNode类型,那么就查看红黑树下的所有叶子节点的key是否与新添加的映射关系的key是否“相同”,如果相同,就用新的value替换原来的value。
  
 第六步:如果没有找到table[index]有结点与新添加的映射关系的key“相同”,那么
 如果是TreeNode类型,那么新的映射关系就创建为一个TreeNode,连接到红黑树中
 如果是Node类型,那么要查看链表的结点的个数,是否达到8个,如果8个,并且table.length小于64,那么先扩容。
 TREEIFY_THRESHOLD:树化阈值8
 MIN_TREEIFY_CAPACITY:最小树化容量64


 UNTREEIFY_THRESHOLD:反树化,从数变为链表的阈值6

以上是关于手写HashMap JDK1.7(无红黑树)的主要内容,如果未能解决你的问题,请参考以下文章

手写HashMap(基于JDK1.7)

HashMap实现原理(jdk1.7/jdk1.8)

HashMap常见面试题

JAVA集合:TreeMap红黑树深度解析

(多图)那些年,面试被虐过的红黑树

一文看懂 HashMap 中的红黑树实现原理