JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable

HashMap

HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时候,会保证给定的初始容量为2次幂,如下:

// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;

每一次扩展都为2的倍数,这样子的好处在于,计算HashCode之后,计算bucket index时就不需要进行求余运算,直接进行&运算即可(如2^3 - 1 = 0x0111,进行&运算就保证了在该值范围内,而不是2的次幂的话,就不行了,只能求余)

static int indexFor(int h, int length) {
return h & (length-1);
}

具体实现

put

put操作核心就在于先计算Hash找出对应的bucket,然后再看里面是否有对应的entry,如果没有,则新增,有则替换掉原来的值。在新增的过程中,如果判断出容量达到阈值,则要进行上述所讲的扩容。

public V put(K key, V value) {
if (key == null)
// 特殊处理,key为null的,放在bucket 0上
return putForNullKey(value);
// 计算hash code
int hash = hash(key);
// 计算bucket index
int i = indexFor(hash, table.length);
// 查找是否有值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
} // 没有则结构变更,modCount加1
modCount++;
// 添加entry
addEntry(hash, key, value, i);
return null;
} void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
// 达到阈值,并且当前有冲突才会进行rehashing
// 2倍的速度增长
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
} // 添加新的entry
createEntry(hash, key, value, bucketIndex);
} void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}

resize

重新哈希(rehashing),条件是(size >= threshold) && (null != table[bucketIndex]),达到阈值并且产生冲突。如果新容量大于设定的Hash算法切换阈值,有可能会切换Hash算法,这时Hash Code就要进行重新计算。

void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
// 这里当虚拟机启动后,并且新的容量大于设定切换Hash的阈值,则切换Hash
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
/* 这里的rehash指示是否需要重新计算HashCode,只有当Hash算法进行切换的时候才需要重新计算,
如果已经切换过来了,则不需要了 */
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer

将entry转移到新的table表里面

void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
// 这里就是指示是否需要重新计算HashCode
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

get

get相对比较简单,计算Hash,定位到对应的bucket,然后再从bucket的链表中查找。

```java`

public V get(Object key) {

if (key == null)

return getForNullKey();

Entry<K,V> entry = getEntry(key);

return null == entry ? null : entry.getValue();

}

final Entry<K,V> getEntry(Object key) {

// 计算Hash code

int hash = (key == null) ? 0 : hash(key);

// 定位到对应的bucket

for (Entry<K,V> e = table[indexFor(hash, table.length)];

e != null;

e = e.next) {

// 循环,依次查找

Object k;

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

return e;

}

return null;

}


## LinkedHashMap
LinkedHashMap,一般用于在使用HashMap的时候,需要记录每个entry的插入顺序或者访问顺序(如LRU,FIFO)。在HashMap的基础上实现还是比较简单的,只需要自定义一个entry,继承HashMap的entry,在里面添加链表的特性,然后再覆盖对应的addEntry,使用自定义的entry即可。
关键字段:
accessOrder:用于控制节点排序为访问顺序还是插入的顺序,accessOrder=false可以实现FIFO的淘汰策略,accessOrder=true实现LRU,默认为false。 ### 具体实现 #### put
put操作,通过对addEntry以及createEntry的方法来进行追加链表特性。
```java
// put操作当需要添加entry时,会调用此addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) { // 调用父类的实现,不作更改
super.addEntry(hash, key, value, bucketIndex); // 当有必要的时候,会移除一些键值(通过覆盖removeEldestEntry方法来实现)
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) {
/* 移除最老的键值对,这里最老的键值对判断方式由子类实现,
但是顺序由accessOrder实现,可以为LRU或者FIFO */
removeEntryForKey(eldest.key);
}
} // 父类的entry会调用到此方法(因为覆盖了)
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
// 覆盖父类的createEntry是为了使用当前类定义的entry,具有链表的特性
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}

get

Get操作也是覆盖了父类的实现,方便访问后调用子类的recordAccess。

public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
// 访问回调
e.recordAccess(this);
return e.value;
} private static class Entry<K,V> extends HashMap.Entry<K,V> {
Entry<K,V> before, after; void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
// 这里accessOrder为false则不调整顺序
if (lm.accessOrder) {
// true则将当前访问的节点移动到的链表头
lm.modCount++;
remove();
addBefore(lm.header);
}
}
}

Hashtable

Hashtable和HashMap的区别:

  1. Hashtable的基本操作采用同步实现,加了synchronized关键字
  2. 扩容策略,可以指定初始大小,后续以这个基础进行扩容,每次均为当前数值两倍(不一定是2次幂,采用求余运算)
  3. 取所有的key或者values时,返回的是Enumeration(也有keySet操作)

JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable的更多相关文章

  1. 【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)

    一.前言 在遍历HashMap与LinkedHashMap时,我们通常都会使用到迭代器,而HashMap的迭代器与LinkedHashMap迭代器是如何工作的呢?下面我们来一起分析分析. 二.迭代器继 ...

  2. JDK源码分析之hashmap就这么简单理解

    一.HashMap概述 HashMap是基于哈希表的Map接口实现,此实现提供所有可选的映射操作,并允许使用null值和null键.HashMap与HashTable的作用大致相同,但是它不是线程安全 ...

  3. JDK源码分析(三)——HashMap 下(基于JDK8)

    目录 概述 内部字段及构造方法 哈希值与索引计算 存储元素 扩容 删除元素 查找元素 总结 概述   在上文我们基于JDK7分析了HashMap的实现源码,介绍了HashMap的加载因子loadFac ...

  4. JDK源码分析(三)——HashMap 上(基于JDK7)

    目录 HashMap概述 内部字段及构造方法 存储元素 扩容 取出元素 删除元素 判断 总结 HashMap概述   前面我们分析了基于数组实现的ArrayList和基于双向链表实现的LinkedLi ...

  5. 源码分析四(HashMap与HashTable的区别 )

    这一节看一下HashMap与HashTable这两个类的区别,工作一段时间的程序员都知道, hashmap是非线程安全的,而且key值和value值允许为null,而hashtable是非线程安全的, ...

  6. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  7. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  8. 【JDK】JDK源码分析-HashMap(2)

    前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理.但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题. 这里再贴一下 HashMap ...

  9. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

随机推荐

  1. 最近在线笔试的一些感想和总结,阿里巴巴,腾讯,百度,360。c++研发,机器学习等岗位

    持续更新中... 1.编程界牛人太多了,还是要好好a题,好好弄清楚基础算法,并且用代码实现 2.c/c++方向其实来回来去那么几道题,做好了记到脑子里. 下面就是我打算把不会的,不清楚的都贴上来然后好 ...

  2. 《java入门第一季》之UDP协议下的网络编程详解

    首先看一下UDP协议的图解: 可以看到,分为发送端和接收端程序. 直接上代码: 发送端程序: import java.io.IOException; import java.net.DatagramP ...

  3. mxgraph进阶(二)mxgraph的初步介绍与开发入门

    mxgraph的初步介绍与开发入门 前言 由于小论文实验需求,需要实现根据用户日志提取出行为序列,然后根据行为序列生成有向图的形式,并且连接相邻动作的弧上标有执行此次相邻动作的频次.为此,在大师兄徐凯 ...

  4. H5学习之旅-H5的基本标签(2)

    H5的标签和html的标签没什么区别,主要介绍H5的基本标签 1.基础标签header和body,header的<title>元素主要是显示在标签页面里面,以及设置使用的语言和编码格式.b ...

  5. React native开发中常见的错误

    react native环境搭建请移步:react native环境搭建 这里说说react native创建完成之后,运行中出现的常见问题, 问题1: java.lang.RuntimeExcept ...

  6. shell重定向(大于号,小于号,左右,2>&1,&)

    本文的例子部分是引用网络上的一篇文章. Linux的IO输入输出有三类 Standard Input 代码 0 Standard Output 代码 1 Standard Error 代码 2 举个例 ...

  7. Java-HttpServletResponse-HttpServletResponseWrapper

    //继承ServletResponse,发送回复信息,servlet容器创建一个HttpServletResponse对象,将它作为service函数的参数 public interface Http ...

  8. MTK 软件设置路径

    1. uboot路径 mediatek\custom\common\uboot\logo\hvga\hvga_kernel.bmp mediatek\custom\common\uboot\logo\ ...

  9. ZooKeeper客户端事件串行化处理

    为了提升系统的性能,进一步提高系统的吞吐能力,最近公司很多系统都在进行异步化改造.在异步化改造的过程中,肯定会比以前碰到更多的多线程问题,上周就碰到ZooKeeper客户端异步化过程中的一个死锁问题, ...

  10. 遗传算法解决TSP问题实现以及与最小生成树的对比

    摘要: 本实验采用遗传算法实现了旅行商问题的模拟求解,并在同等规模问题上用最小生成树算法做了一定的对比工作.遗传算法在计算时间和占用内存上,都远远优于最小生成树算法. 程序采用Microsoft vi ...