文章目录
一、Map接口
哈希表就是一种以键-值(key-indexed) 存储数据的结构,只要输入待查找的值即key,即可查找到其对应的值。
哈希的思路很简单,如果所有的键hashCode都是整数,那么就可以使用一个简单数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。
简单的计算方法hashcode%数组长度=【0,数组的长度-1】
它提供了一组键值的映射。其中存储的每个数据对象都有一个相应的键key,键决定了值对象在Map中的存储位置。键应该是唯一的,不允许重复,每个key只能映射一个value。
Map接口的定义
Map为真正的顶级接口
public interface Map<K, V> {}
定义map对象时需要指定key和value对应的类型,必须是复杂类型,不能使用int
map接口中有一个内部接口为Entry:interface Entry<K,V>封装所存储的key-value对数据
interface Entry<K, V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>>
.......
}
每个Entry对象中封装了一个key和value,Entry接口中通过静态方法提供了一组比较器的默认实现
map中所存放的数据将给封装为一个一个的Entry,一个key/value对对应一个Entry对象
Map接口中常见方法
- Object put(Object key,Object value):用来存放一个键-值对Map中 ,如果出现key值冲突则后盖前。允许key值和value值为null,但是key值为null只能有一个,value值为null没有个数限制
- size():int用于获取集合中的元素个数
- Object remove(Object key):根据key(键),移除键-值对,并将值返回
- Object get(Object key) :根据key(键)取得对应的值,如果key值不存在则返回为null
- boolean containsKey(Object key) :判断Map中是否存在某键key
- void clear() :清空当前Map中的元素
- boolean containsValue(Object value):判断Map中是否存在某值value
另外Map提供了3种视图,分别是key所组成的set、value所组成的collection、key-value的Entry集合Set
- public Set keySet() :返回所有的键key,并使用Set容器存放,获取key值后就可以通过get方法获取key对应的值value
- public Collection values() :返回所有的值Value,并使用Collection存放
- public Set entrySet() :返回一个实现 Map.Entry 接口的元素 Set
Map实现类
HashMap、TreeMap、LinkedHashMap、Hashtable等
二、HashMap
类定义
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {}
重要的常量值
//默认的初始化容积:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
//最大容积:2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转换为红黑树的最小长度
static final int TREEIFY_THRESHOLD = 8;
//红黑树退化为链表的最大长度
static final int UNTREEIFY_THRESHOLD = 6;
//元素树化的最小个数
static final int MIN_TREEIFY_CAPACITY = 64;
具体的内部数据存储方式
//存放元素的数组
transient Node<K,V>[] table;
//具体的存储对象
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
静态内部类用于实现Entry,HahMap中存放的key/value对就被封装为Node对象。其中key就是存放的键值,用于决定具体的存放位置;value是具体存放的数据,hash就是当前Node对象的hash值,next用于指向下一个Node节点(单向链表),具体存储数据的实现采用的是单向链
Java中采用拉链法实现了Hash表结构
重要的阈值
- static final int TREEIFY_THRESHOLD = 8;//树化阈值:即链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树
- static final int UNTREEIFY_THRESHOLD = 6;//桶的链表还原阈值:即红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
构造器
无参构造,采用默认的负载因子,负载因子就是用于控制hash表中所允许存储的元素个数占总容积的百分比。值越大hash碰撞的概率越高,但是越节约空间;值越小hash碰撞的概率越低,但是越浪费空间;
控制容器中允许存放的元素个数上限为 [容积*负载因子]
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
有参构造,传入初始化容积,会调用全参构造,负载因子仍是默认值
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
全参构造,自定义初始化容积和负载因子
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;
this.threshold = tableSizeFor(initialCapacity);
}
数组容积,并非传入多少就是多少,而是计算一个2**n值>=设定的容积值.例如初始化容积参数值为7则实际创建容积值为8
tableSizeFor方法用于获取数组的初始化大小
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
获取的值为获取2n<初始化容积<2(n+1)时返回2(n+1),返回一个大于等于初始化容积值的2n值
但是在构造器中并没有创建任何用于存储数据的集合—延迟加载,会在第一次存储数据时才进行空间分配
使用2的n次方的原因:
- 追加元素需要计算允许存储元素个数上限的问题
- 求余的实现方法:
2%8=2 2 & (8-1) 0010 & 0111 = 0010
9%8=1 9 & (8-1) 1001 & 0111 = 0001
15%8=7 15 & (8-1) 1111 & 0111=0111
HashMap的存储结构
HashMap采用的是拉链法实现数据的存储,其中有一个数组Node[],每个元素上存储一个链表Node。每个Node[]数组中的元素被称一个桶bucket,一个桶对应一个hash映射的值,例如0,1等,可能会出现不同的key,但是hash映射的位置相同,例如16、32等,这采用单向链表结构存储hash映射值相同的所有数据(JDK8+在单个链表长度大于阈值8时自动转换为红黑树,删除节点使某单个树节点数小于阈值6时会自动从红黑树退化为链表结构)
相关参数:
- capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
- loadFactor:负载因子,取值在(0,1)之间,默认为 0.75
- threshold:扩容的阈值,等于 capacity * loadFactor
HashMap底层采用的是Entry数组和链表实现。
Map 主要用于存储键key值value对,根据键得到值,因此键不允许重复,但允许值重复。
键类型要求:重新hashCode()与equals()
HashMap的put方法的具体流程
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
TREEIFY_THRESHOLD为8,如果新插入的值是链表中的第 9 个会触发下面的 treeifyBin(树化操作,就是将单向链转换为红黑树),也就是将链表转换为红黑树。
JDK8+插入数据到链表的最后面,Java7 是插入到链表的最前面
HashMap的扩容操作
如果HashMap中存储数据的桶数组table长度为0或者为null,则按照初始化配置参数进行数组的创建;如果长度非空,则数组扩容一倍,并重新计算所有元素的存储新位置,rehash计算
HashMap是怎么解决哈希冲突的
在Java中,保存数据有两种比较简单的数据结构:数组和链表。
- 数组的特点是:寻址容易,插入和删除困难;
- 链表的特点是:寻址困难,但插入和删除容易;
所以将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突这样就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)
数组长度要保证为2的幂次方的原因
只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;如果length为2的次幂则length-1转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为 14,对应的二进制为1110,在于h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费