前言:别人是别人,要有自己的思想。没有自己想法的开发者还不如机器。
结构
继承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本身就是null。
1.计算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右移16位 3.两者异或 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次幂基本上就很清晰了。