1、hashmap简介
HashMap基于哈希表的Map接口实现。是以key-value存储形式存在。线程不安全。key和value都可以为null,无序。
JDK1.8之前由数组+链表组成,数组是HashMap主体,链表则主要是为了解决哈希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(“拉链法”解决冲突)
JDK1.8之后,当链表长度大于阈值(或者红黑树的边界值,默认为8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。如果链表长度小于等于6时,红黑树会再次转换为链表,因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
补充:为了提高效率,将链表转换为红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树,而是选择进行数组扩容。
2、hashmap底层的数据结构
JDK1.8之前,数组+链表
JDK1.8之后,数组+链表+红黑树
//在jdk1.8之前创建该对象,会创建一个长度为16的Entry[] table用来存储键值对数据。
jdk1.8之后不是在构造方法创建了,而是在第一次调用put方法时才进行创建,创建Node[] table
Map< String , String > map = new HashMap<> (); map . put( " 1 " , " 1 " );
问题?
1、哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出hash值
底层采用的key的hashCode方法的值结合数组长度进行无符号右移(>>>)、按位异或(^)计算hash值,按位与(&)计算出索引
static final int hash( Object key) {
int h;
return (key == null ) ? 0 : (h = key . hashCode()) ^ (h >>> 16 );
}//其中n为数组长度
(n - 1 ) & hash
还可以采用:平方取中法,取余数、伪随机数法
2、当两个对象的hashCode相等时会怎么样?
会产生哈希碰撞,若key值内容相同则替换旧的value,不然就连接到链表后面,链表长度超过阈值8转为红黑树
说明:
1、size表示HashMap中KV的实时数量,不是数组的长度
2、threshold(临界值)=capacity(容量)*loadFactor(加载因子)。这个值是当前已占用数组长度的最大值。size超过这个临界值就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍
3、hashmap集合类的成员
成员变量
1、序列化版本号
private static final long serialVersionUID = 362498820763181265L ;
2、集合的初始化容量(必须是2的n次幂)
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; // aka 16
问题?
为什么大小必须是2的n次幂
存储高效,尽量减少碰撞,在(n-1)&hash求索引的时候更均匀
如果传入的容量默认不是2的幂
//对传入容量进行右移位运算后进行或运算//一共进行5次或运算,可以将当前数字中二进制最高位1的右边全部变成1,最后+1后返回
static final int tableSizeFor( int cap) {
//这里-1的目的是使得找到的目标值大于或等于原值
int n = cap - 1 ;
n |= n >>> 1 ;
n |= n >>> 2 ;
n |= n >>> 4 ;
n |= n >>> 8 ;
n |= n >>> 16 ;
return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY ) ? MAXIMUM_CAPACITY : n + 1 ;
}
3、默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f ;
4、集合最大容量
static final int MAXIMUM_CAPACITY = 1 << 30 ;
5、链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8 ;
为什么是8?
TreeNode占用空间是普通Node的两倍,空间和时间的权衡,同时如果为8,log(8)=3小于链表的平均8/2=4
6、红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6 ;
7、链表转红黑树时数组的大小的阈值,即数组大小大于这个数字时,链表长度大于8才会转为红黑树
static final int MIN_TREEIFY_CAPACITY = 64 ;
8、table用来初始化数组(大小是2的n次幂)
transient Node< K , V > [] table;
9、用来存放缓存(遍历的时候使用)
transient Set< Map .Entry< K , V > > entrySet;
10、HashMap中存放元素的个数(重点)
transient int size;
11、记录HashMap的修改次数
transient int modCount;
12、临界值(如果存放元素大小大于该值,则进行扩容)
int threshold;
13、哈希表的加载因子(重点)
final float loadFactor
说明:
loadFactor加载因子,可以表示HashMap的舒米程度,影响hash操作到同一个数组位置的概率,默认0.75,不建议修改构造方法
构造方法
1、构造一个空的HashMap,默认初始容量(16)和默认负载因子(0.75)
public HashMap() {
this . loadFactor = DEFAULT_LOAD_FACTOR ; // all other fields defaulted
}
2、构造一个具有指定的出是容来那个和默认负载因子(0.75)的HashMap
public HashMap( int initialCapacity) {
this (initialCapacity, DEFAULT_LOAD_FACTOR );
}
3、构造一个具有指定初始容量和负载因子的HashMap
public HashMap( int initialCapacity, float loadFactor) {
if (initialCapacity < 0 )
throw new IllegalArgumentException ( " Illegal initial capacity: " +initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY )
initialCapacity = MAXIMUM_CAPACITY ;
if (loadFactor <= 0 || Float . isNaN(loadFactor))
throw new IllegalArgumentException ( " Illegal load factor: " + loadFactor);
this . loadFactor = loadFactor;
//根据初始值返回一个2的n次数字,赋给阈值,在put方法中会对此值进行重新运算
this . threshold = tableSizeFor(initialCapacity);
}
4、包含另一个Map的构造函数
public HashMap( Map<? extends K , ? extends V > m) {
this . loadFactor = DEFAULT_LOAD_FACTOR ;
putMapEntries(m, false );
}
final void putMapEntries( Map<? extends K , ? extends V > m, boolean evict) {
int s = m . size();
if (s > 0 ) {
if (table == null ) { // pre-size
/ / +1的目的是获取更大的容量,减少数组的扩容次数
float ft = (( float )s / loadFactor) + 1.0F ;
int t = ((ft< ( float ) MAXIMUM_CAPACITY ) ? ( int )ft :MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for ( Map . Entry<? extends K , ? extends V > e : m . entrySet()) {
K key = e . getKey();
V value = e . getValue();
putVal(hash(key), key, value, false , evict);
}
}
}
成员方法增加方法(put)
1)先判断数组是否未初始化,如果没有初始化,则进行一次初始化操作(扩容),同时将数组大小赋给n
2)找到具体的桶,并判断此位置是否有元素,如果没有元素,则创建一个Node直接插入
3)如果出现冲突
(1)如果为红黑树节点,调用红黑树方法插入数据
(2)如果为普通节点,插入链表末尾,并且长度达到临界值时,将链表转为红黑树
(3)如果桶中存在重复的键,将该键替换新值value
(4)size大于阈值threshold,进行扩容
扩容方法(resize)
数组初始化以及数组元素个数大于阈值时进行扩容操作,一部分索引会增加原数组长度大小的长度(用到了高位1),一部分仍保持原索引(高位为0)
删除方法(remove)
查找方法(get)
HashMap的初始化设计
为了尽可能的避免hashmap的扩容操作,提高性能,如果明确知道存储的数据量大小I时,初始化值如下
Map< String , String > map = new HashMap<> (initialCapacity);
initialCapacity = (需要存储的元素个数/负载因子) + 1
4、遍历HashMap集合的几种方式
1、分别遍历Key和Values
for ( String key : map . keySet()){
System . out . println(key);
}
for ( Object value : map . values()){
System . out . println(value);
}
2、迭代器(增强for循环)
Iterator< Map .Entry< String , Integer > > iterator = map . entrySet() . iterator();
while (iterator . hasNext()){
Map . Entry< String , Integer > next = iterator . next();
System . out . println(next . getKey() + " : " + next . getValue());
}
3、通过get方式(不建议使用)
Set< String > keySet = map . keySet();
for ( String str : keySet){
System . out . println(str + " === " + map . get(str))
}
4、jdk8以后采用Map接口的默认方法forEach
map . forEach((k,v) - > {
System . out . println(k + " : " + v);
});