您的位置:首页 > 编程语言 > Java开发

JDK8 源码之HashMap(1)

2017-02-24 14:04 246 查看

HashMap是什么

源码出发:

public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {...}


可以看到HashMap实现了Map、Cloneable、Serializable接口,实现了AbstractMap抽象类。

Map(附录有详细介绍)为一个将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。

HashMap在jdk1.8中,实现上有了一个很大的优化,实现方式有原来的数组加链表,变成了数组、链表和红黑树。在性能上有了一个较大的提升,也一定程度上解决了hash值碰撞带来的性能损失。

HashMap-Fields

再继续看看Fields:

//默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//当table容量大于该值,threshold设置成为固定Integer.MAX_VALUE。
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;

//Node节点数组(hash表)
transient Node<K,V>[] table;
//Map.Entry Set对象,用于遍历
transient Set<Map.Entry<K,V>> entrySet;

//存储元素数目
transient int size;
//结构变更次数
transient int modCount;
//下次调整大小的临界值(table大小*负载因子)
int threshold;
//哈希表的负载因子。
final float loadFactor;


可以看到,hashMap存在一个负载因子,当hashMap容量超过threshold(负载因子*数组长度)会发生扩容。链表与红黑树的相互转化各存在一个阈值,同时也使用modCount来记录‘结构变更’用以保证fail-fast。

HashMap-Node

Node节点还是一览无余:

static class Node<K,V> implements Map.Entry<K,V> {
//hash值
final int hash;
final K key;
V value;
//指向下个结点
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey()        { return key; }
public final V getValue()      { return value; }
public final String toString() { return key + "=" + value; }
//获取节点hash值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//设置节点值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断节点是否相同:节点值的key且value调用equals为true
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}


node节点为链表节点,除了记录当前的key、value值及next节点时,也将key的hash值记录来,杜绝重复计算带来的性能损失。

HashMap-TreeNode

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent;  // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;    // needed to unlink next upon deletion
boolean red;
....
}


当链表的长度超过默认阈值时,该链表会转化成红黑树,防止链表过长,造成查找性能缓慢,也是jdk8中一个比较重要的优化。关于TreeNode的实现和操作会在后面单独开一篇博客来说明,有兴趣的可以看看

HashMap构造方法

//默认传参数
public HashMap() {
//负载因子默认0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//指定初始容量
public HashMap(int initialCapacity) {
//负载因子默认为0.75
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//指定初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)

f010
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量最多为MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//计算得到下载扩容容量的临界值(返回值都为2幂次方-1)
//可以看下面的解释,这里threshold的值会实际是table的初始容量
this.threshold = tableSizeFor(initialCapacity);
}

//设置初始值
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//该方法后面再做介绍
putMapEntries(m, false);
}


可以看到的,hashMap初始化未添加元素的时,并未实例化table,只是记录来容量和负载因子。而是在第一次添加的操作的时候,才会实例化table。

而且可以看出设置initialCapacity时,并没有按照你给的initialCapacity取初始化table容量,而是将tableSizeFor(initialCapacity)的返回值newCap赋值给threshold,结合后面的resize方法,初始化时会走‘分支二’,table的容量会被初始化成newCap大小。

HashMap-resize扩容

接下来,看看hashmap怎么扩容的:

final Node<K,V>[] resize() {
//获取当前table对象
Node<K,V>[] oldTab = table;
//获取当前容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取扩容临界值
int oldThr = threshold;
//声明新容量、新负载因子
int newCap, newThr = 0;
if (oldCap > 0) {//“分之一”
//当前容量大于0时
if (oldCap >= MAXIMUM_CAPACITY) {
//当前容量大于于MAXIMUM_CAPACITY
//直接设置threshold的值为Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新容量newCap=当前容量*2
//新临界点threshold=当前临界点threshold*2
newThr = oldThr << 1;
}
else if (oldThr > 0) //“分之二”
//当前容量等于0,但threshold已经指定,则容量=threshold
//例如:HashMap(initCap,0.75)初始化时,oldCap=0,threshold>0
newCap = oldThr;
else {   //“分之三”
//当前容量等于0时(例如第一次添加元素时)
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//走“分之二”,newThr==0
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//记录在新表中位置不变的
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//记录在新table中位置需要变动的
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//这一段的算法应该来说比较巧妙哦,
//已知:新扩容=当前容量*2
//假设当前容量为16(二进制:10000),
//假设元素hash值后5位为(a:10010,b:00010)
//a和b元素位置一致(cap-1)&hash为2位(00010)
//新容量:32(100000)
//新位置 a:第18位(10010),b第2位(00010)
//可以看出,a和b元素的位置正好相差oldCap
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}


resize方法注释已经很详细了,主要有一个点妙传就是扩容时,每个链表中原有元素的位置要么不动,要么移动oldCap个位置。而且插入顺序不变,jdk1.7中则会反序。

其他操作

限于篇幅,可以看后面的博客

附录 Map接口

Map 方法说明

public interface Map<K,V> {
//map大小
int size();
//是否为空
boolean isEmpty();
//map是否含有当前key
boolean containsKey(Object key);
//map是否含有当前value
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);
//批量设置
void putAll(Map<? extends K, ? extends V> m);
void clear();
//获取key Set列表
Set<K> keySet();
//获取value Set列表
Collection<V> values();
//获取遍历对象列表
Set<Map.Entry<K, V>> entrySet();
//遍历节点接口定义
interface Entry<K,V> {....}
boolean equals(Object o);
int hashCode();
}


可以看到Map接口定义了一个map必须实现的操作,增加、插入、修改、遍历等基本方法,同时定义了内部遍历节点Entry接口。不过在jdk8之后,可以看到Map接口中多了很多default方法,虽然都不复杂,但是可以借鉴一下这种思想,还是很有好处的。

Map default方法

大概浏览下,默认方法如:getOrDefault、forEach等方法实现都比较简单,就不描述细节了,只是大概说一下是什么功能了。

//通过键值key获取值为null时,则返回defaultValue
default V getOrDefault(Object key, V defaultValue){...}

//遍历键值对,执行指定动作
default void forEach(BiConsumer<? super K, ? super V> action){...}

//遍历键值对,对每个键值对执行指定动作后的返回值覆盖当前值
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function){...}

//当前键值key获取值为null时,则设置值为value
default V putIfAbsent(K key, V value) {}

//删除键值为key且值为value的键值对
//map已经存在键值key
default boolean remove(Object key, Object value){}

//覆盖键值对(键值为key且值为oldValue)的值为newValue,否则不覆盖
//map已经存在键值key
default boolean replace(K key, V oldValue, V newValue){}

//设置map中键值为key的的值为value
//map已经存在键值key
default V replace(K key, V value){}

//当get(key)为空时,则mappingFunction.apply(key)计算得到的新值newValue不为空时,则put(key,newValue)并返回newValue,否则返回null
default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {}

//与computeIfAbsent基本相同,新增了一个当get(key)为空时,remove(key)
default V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {}

//mappingFunction.apply(key)计算得到的新值newValue
//不为空时,put(key,newValue) 返回newValue
//为空时,remove(key) 返回null
default V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {}

//1.mappingFunction.apply(key)计算得到的新值newValue
//2.get(key)为null时,put(key,value),否则 put(key,newValue)
//3.remove(key)
default V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction){}


可以看到接口中的默认方法还是很丰富,而且都比较简单,但阅读过程中,这些接口的定义,方法的抽象,对于一个程序猿来说还是能学到很多东西。

Map-Entry default方法

interface Entry<K,V> {
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}

public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jdk 源码 hashmap