HashMap在线学习

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转为红黑树
put流程
说明:
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);
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值