我是这么理解的之HashMap(未完)

102 阅读6分钟

前言:别人是别人,要有自己的思想。没有自己想法的开发者还不如机器。

结构

继承AbstractMap
实现Map<K,V>、Cloneable、Serializable

元素:
//默认初始容量 必须是2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1<<4;
//最大容量 隐式指定时使用
static final int MAXIMUM_CAPACITY = 1<<30;
//默认负载因子 未指定时使用
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//列表转树的阈值
static int int TREEIFY_THRESHOLD = 8;
//取消树化的阈值
static int int UNTREEIFY_THRESHOLD = 6;
//table最小树化的阈值(树化的最小值)
static int int MIN_TREEIFY_CAPACITY = 64;
//元素 || bin || 节点
static class Node<K,V>;

常用方法

//根据k获取v
public V get(Object key);
//放入元素
public V put(K key,V value);
//根据k删除元素
public V remove(Object key);
//是否包含k
public boolean containsKey(Object key);
//是否包含v
public boolean containsValue(Object value);
//批量放入元素
public vaid putAll(Map<K,V> m);
//获取所有k
public Set<K> keySet();
//获取所有v
public Collection<V> values();
//判定k是否存在,不存在则放入元素
public V putIfAbsent(K key,V value);

我的理解

List 有序,允许重复
Set  无序,不允许重复
Map  键值对,K唯一,V允许重复

    根据代码执行的场景,我们可以选择以上三种集合进行适配。当需要键值对的形式进行数据操作的时候,我们可以选择Map进行
数据操作。
    那么我们为什么总是在使用Map集合的时候,总是第一时间先考虑HashMap呢?显然是效率至上的原因,而HashMap高效的前提下
又舍弃了什么呢?安全性,因为在JAVA中,一个集合或者一个容器高效的前提,基本都是不安全的。而安全的集合或者容器,基本都
是相对低效的。
    因此,我以处理效率的角度来阐述我的HashMap的片面看法。
(不敢太笃定,希望有前辈可以改变我的想法)

逐一分析

HashMap的思考

思考:为什么 DEFAULT_INITIAL_CAPACITY 是2的次幂?3的行不行?

HashMap的构造方法

new HashMap()、new HashMap(int initialCapacity)、new HashMap(int initialCapacity,float loadFactor)
、new HashMap(Map m)
四个构造方法,无参、初始长度、初始长度和负载因子、Map

new HashMap():构造一个HashMap,该HashMap所有元素皆为默认值。

new HashMap(int initialCapacity):构造一个HashMap,负载因子默认0.75。如果入参长度小于0,抛出异常。如果入参长度大于
最大长度MAXIMUM_CAPACITY,则强制赋值为MAXIMUM_CAPACITY。最后将得到的入参长度进行2次幂比较(tableSizeFor()方法,例
如入参为5,则转为8,如果为17,则转为32..总是2的次幂),初始化HashMap。

new HashMap(int initialCapacity,float loadFactor):除了负载因子是入参生成的,其他的与上面逻辑一样。

new HashMap(Map m):将一个map转为HashMap。如果table为空则初始化HashMap,并计算出下一个扩容的目标值。如果下一个扩容
的目标值小于当前map的size,则做扩容动作,最后遍历入参map进行循环put操作。

HashMap的常用方法

public V get(Object key);

返回指定K对应的V,如果不存在对应V(不存在映射关系),则返回null。但是,返回null不一定是不存在映射关系,也可能是对应的
V本身就是null1.计算K的hash值
    2.根据K的hash值、K本身获取到对应的映射关系Node
    3.Node为null则返回null,不为null则返回Node.V
hash为散列。计算K的hash值,是为了减少hash碰撞,最终目的是散列分布每个Node存放的位置。那么散列分布Node又是解决什么呢?
意义何在?
理解:
    在get(Object key)方法中,散列是无意义的(稍微有点偏颇),hash动作只是为了获取到K的hash值,从而定位到对应的Node。
仅仅是为了获取到Node对应位置而已。当然仅仅也是针对get(Object key)方法而言。
    散列分布是为了什么?意义何在?
    Hash,是将一个任意长度的消息(信息载体)压缩(也可以理解为转译)成为一个固定长度的摘要值(hash值)。通俗讲,就是以
Object获取到该Object的存放地址的映射值。
    那么根据计算得出的hash值作为Node的下标,那岂不是就可以得到一个完全不碰撞的数组?
    但事实并非如此。第一,hash并不是唯一的,并非是与数据绝对的一对一映射,可能A、B两个对象对应的hash是同一个。第二,
hash一般是32位的,那么如果作为数组的下标,考虑数据越界的情况下,数组需要多少长度才能放入呢?那要这个hash值又是为了什么?
    首先第一个问题,数组的长度肯定是2的N次幂,这个不容置疑,因为put进入第一个元素的时候默认就是16长度。
    第二个问题,这个hash值是为了让put进table的元素在table中尽可能的均匀分布,减少链表与红黑树的使用,虽然提供了链表和
红黑树的数据结构,但是大量元素放入会大大降低table的存取效率,与HashMap存在的意义相悖,不可取。
    
    那么又出现了一个新的问题,K的hash计算出来后,在HashMap中是如何使用的呢?怎么就把K的hash值转换成了元素的index呢?
   ** HashMap中处理是四步走:1.hash  2.hash右移163.两者异或  4.[(lenth - 1)& 异或结果 ] 按位与运算**
    
以下我的伪代码,理解四步走:
    String one = "one";
    String three = "three";
    //110182
    System.out.println("one的hashCode:"+one.hashCode());
    //110339486
    System.out.println("three的hashCode:"+three.hashCode());
    Integer oneHash = one.hashCode();
    // 转换为二进制  右移16位
    Integer _oneHash = oneHash >>>  16;
    System.out.println(oneHash+"转换二进制:"+Integer.toBinaryString(oneHash));
    System.out.println(oneHash+"右移16:"+Integer.toBinaryString(_oneHash));

    Integer threeHash = three.hashCode();
    // 转换为二进制  右移16位
    Integer _threeHash = threeHash >>>  16;
    System.out.println(threeHash+"转换二进制:"+Integer.toBinaryString(threeHash));
    System.out.println(threeHash+"右移16:"+Integer.toBinaryString(_threeHash));
    //1 1010 1110 0110 0110  : 110182 的 二进制
    //1                      : 110182 的 右移16

    //11010010011 1010 0101 1001 1110  : 110339486 的 二进制
    //11010010011                      : 110339486 的 右移16

    System.out.println("oneHash异或:"+(oneHash ^ _oneHash)+",_oneHash十进制:"+_oneHash);
    System.out.println("threeHash异或:"+(threeHash ^ _threeHash)+",_threeHash十进制:"+_threeHash);

    //计算下标 hashMap初始化size = 16 反正是2的N次幂
    System.out.println("one异或后hash:"+ ( 110182 ^ (110182 >>> 16)));
    int oneHash_ = ( 110182 ^ (110182 >>> 16));
    System.out.println("threeHash异或后hash:"+ ( 110339486 ^ (110182 >>> 16)));
    int threeHash_ = ( 110339486 ^ (110182 >>> 16));

    System.out.println("================计算下标=====16size==========");
    int size = 16;

    //将hash的二进制 与 size-1的二进制 进行&运算
    int oneIndex = (size - 1) & oneHash_;
    int threeIndex = (size - 1) & threeHash_;
    //   11010111001100111
    //  &
    //   00000000000000111
    //   00000000000000111   = 7
    System.out.println(Integer.toBinaryString(oneHash)+"与"+Integer.toBinaryString((size - 1))+"进行&运算");
    //   1111
    //  &
    //   1111
    //   1111   = 15
    System.out.println(Integer.toBinaryString(threeIndex)+"与"+Integer.toBinaryString((size - 1))+"进行&运算");
    System.out.println("one在HashMap中的下标-oneIndex:"+oneIndex);
    System.out.println("three在HashMap中的下标-threeIndex:"+threeIndex);
    
    //以上如果理解了,其实关于HashMap的长度为什么是2的N次幂基本上就很清晰了。