1.基础知识

1.数组

数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难

2.链表

链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易

3.哈希表

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表。哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

2.具体实现

由于HashMap使用的是数组+链表的方式来存储数据的。那么我们先研究下每一个元素存放数据的数据结构--HashMap的内部类。

1.基本元素

Entry<K,V>是HashMap的基本元素单位其本身就是一个链表存储方式。

//定义为静态内部类,使用时不需要外部类的对象
static class Entry<K, V> implements Map.Entry<K, V> {
//Key为HashMap定义的key,为保证key的稳定性定义为不可修改的final类型
final K key;
//value为HashMap的value
V value;
//存储的是如果哈希值相同下一个元素的引用。这是一个典型的链表结构
Entry<K, V> next;
//hash为hash(key)%length(hashMap长度默认为16)运算后的结果
int hash; //默认构造方法
Entry(int h, K k, V v, Entry<K, V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//后面是一些重写toString、equals、 hashCode等操作就省略了。
}

上面的代码多一句嘴,在JDK1.8中以上的版本中我们会看到Node和TreeNode的基础元素类型是因为JDK1.8版本的HashMap采用数组+链表+红黑树来实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

2.基础结构

下面我们看看HashMap的基础结构Entry数组。

static final Entry<?,?>[] EMPTY_TABLE = {};

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

Entry数组的长度必须是2的幂。至于为什么是2的幂这个问题不是本文的重点。

可以参考

HashMap实现原理及源码分析|

HashMap剖析

3.存取实现。

1.put

既然HashMap的基础是数组那么为什么能够随机存取。而不是数组那样一个一个add存储呢。

为了解释清楚这个概念。需要了解下HashMap内部的一些属性(成员变量)

1.size 这个属性表示了HashMap中所有KV对的数量,包含挂在链表中的KV对。

2.capacity 这个属性表示HashMap的哈希表的长度,也就是table的长度。

3.loadFactor

这个属性表示装载因子(用来形容是否装满,默认为0.75f),用来当HashMap的哈希表是否需要扩容的最大比例。当前的实装的因子为size/capacity。

4.threshold

这个属性表示HashMap的哈希表是否需要扩容的阈值。一般的来说当size大于这个值时会出发resize()操作(哈希散列表扩容的操作)。一般计算方法为capacity*loadFactor

5.modCount

这个属性表示HashMap表修改次数。给迭代器使用以保证Map迭代的完整性。

在项目第一次put是如果发现table的值为空那么就会启动一个初始化table的方法inflateTable(),这个名称很形象叫充气或者叫可以填充的。

if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//table初始化方法
private void inflateTable(int toSize) {
// 将其扩大值2的幂
int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建了一个大小为最大长度的entry数字
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}

数组建立完成后数组是有下标的。

我们只需要将key的哈希值与数组的最大长度取余。得出的结果作为存储的下标位置存入该数组。

具体实现如下:

//存值过程
int index = key.hashCode() % table.length;
Entry[index] = value;

具体是怎么返回索引的呢,h是key的哈希码 length 是table.length由于是全部填充故table的长度大约等于capacity.

& 与运算:参加运算的两个数据,按二进制位进行“与”运算。 运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;

这里用了一个巧妙的算法。应为之前的约定length必定为2的幂。那么如果将length-1的到的结果一定是全1的二进制数字例如15(1111)、 7(111)、 3(11)、1(1)等。

那么将哈希值与这样的值做与运算得出的结果为h对length取余数。下面我举个栗子说明这一点。



我们用长度为16的length举例 16-1=15用二进制表示为(1111)

依据运算规则 0&1 = 0 1&1=1那么我们只要保证,二进制的数值最后4位为0那么他的余数一定是零。只要后面四位有任意一位是1数值都会被过滤出来。成为余数。

11110000(240)、10000(16)、100000(32)、110000(48)等一定是16的倍数。也就是说无论高于4位的数值是什么对余数结果都没有干扰。

对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。

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

这里会出现一个问题如果2个key的哈希值冲突那么会出现什么结果呢。

这时HashMap的链表就登场了。当时我们在研究哈希表存储结构的时候有一个next属性。作用是指向下一个Entry,那么这两个Entry就以链表的形式存储在了一个哈希值下。

public V put(K key, V value) {
//如果为空即第一次存储执行初始化数组table方法
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null这时就调用putForNullKey来存储value
//这就是hashMap支持null key的原因。
if (key == null)
return putForNullKey(value);
//上文讲到的计算index
int hash = hash(key);
int i = indexFor(hash, table.length);
//遍历链表
//这时一个非常漂亮的递归遍历方式
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果hash 、 key相同则覆盖原值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果key不相同则执行添加(也包含了第一次添加的逻辑)
addEntry(hash, key, value, i);
return null;
}
//如果key是null
private V putForNullKey(V value) {
//如果发现table[0]发现有key等于null的值则覆盖
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//在table[0]添加一个新的KV
addEntry(0, null, value, 0);
return null;
} //添加新元素
void addEntry(int hash, K key, V value, int bucketIndex) {
//在这里调用了是否需要扩容的逻辑
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建新节点
createEntry(hash, key, value, bucketIndex);
}
//这时创建新的ENtry
void createEntry(int hash, K key, V value, int bucketIndex) {
//首先将当前节点的元素存储起来
Entry<K,V> e = table[bucketIndex];
//创建一个新对象存储当前元素,将当原本元素存储到next中
//如果两个元素碰撞那么后来者居上。
table[bucketIndex] = new Entry<>(hash, key, value, e);
//将元素长度增加
size++;
}

说下这个扩容的逻辑,就是这个方法resize 需要传入一个容量大小。每次扩容都是前一次容量的两倍。

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];
//将数据转移到新表
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//修改新的容量阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//将旧哈希表的数据转移到扩容后哈希表中
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;
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;
}
}
}

2.get

取值逻辑

//存值过程
int index = key.hashCode() % table.length;
return Entry[index]

获取的逻辑就没有存储这么复杂了。

public V get(Object key) {
//key为null时单独调用获取null的逻辑
if (key == null)
return getForNullKey();
//获取value值
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//key为null值得取值方法
private V getForNullKey() {
if (size == 0) {
return null;
}
//村吃时候是存在固定位置取时直接从table[0]位置读取
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
//取值逻辑 此方法不能重写。
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
} int hash = (key == null) ? 0 : hash(key);
//循环遍历链表查找到值后返回 如果没有返回null
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;
}

3.remove、clear、containsValue、containsKey

//依据key移除元素
public V remove(Object key) {
//依据key的哈希遍历链表然后移除元素
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
} //clear调用了arrays全填充操作
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
//简单粗暴的遍历全部元素判断是否有该value.效率极其低下
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue(); Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
//判断Key是否存在.很高效
public boolean containsKey(Object key) {
return getEntry(key) != null;
}

4.Iterator

首先我们先看下一个抽象哈希迭代器

private abstract class HashIterator<E> implements Iterator<E> {

    Entry<K,V> next;        // 下一个迭代的元素
int expectedModCount; // 开始迭代修改书
int index; // 当前的标记
Entry<K,V> current; // 当前的实例
//初始化迭代器给next赋值
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null);
}
} public final boolean hasNext() {
return next != null;
}
//读取下一个元素
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException(); if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
//将当前元素赋值给当前元素属性
current = e;
return e;
} public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
}

HashMap提供了3中迭代器遍历方式

1.值遍历(values)

//对外提供的方法
//这里的values是Values这个内部类的实例
public Collection<V> values() {
Collection<V> vs = values;
return (vs != null ? vs : (values = new Values()));
}
//这是一个内部类实现了一个迭代器Collection<V>能接收valus这个实例这是向上造型
//这个实例返回的实际上是一个Map元素的映射因为基于map所以数值是动态变化的
private final class Values extends AbstractCollection<V> {
public Iterator<V> iterator() {
return newValueIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsValue(o);
}
public void clear() {
HashMap.this.clear();
}
}
//返回一个迭代器对象
Iterator<V> newValueIterator() {
return new ValueIterator();
}
//迭代器内部类
//当不断调用next()该方法时 元素就一个接一个呗读取出来了
private final class ValueIterator extends HashIterator<V> {
public V next() {
return nextEntry().value;
}
}

具体在代码中的用法

//第一种
Collection<String> vs = m.values();
System.out.println(vs);
//第二种
Iterator<String> vs2 = m.values().iterator();
while(vs2.hasNext()){
System.out.println(vs2.next());
}

2.键遍历(keySet)

迭代方式和值遍历略有不同本质上还是使用HashIterator来迭代。只不过由取value变成了取key


public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
} private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
} Iterator<K> newKeyIterator() {
return new KeyIterator();
}
private final class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}

实际迭代用法,雷同与值遍历

Set<String> keys = m.keySet();
System.out.println(keys);
Iterator<String> keys2 = m.keySet().iterator();
while (keys2.hasNext()) {
System.out.println(keys2.next());
}

3.键值对遍历(entrySet)

迭代方式相同此处就不在赘述。

public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
} private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
} private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<K,V> e = (Map.Entry<K,V>) o;
Entry<K,V> candidate = getEntry(e.getKey());
return candidate != null && candidate.equals(e);
}
public boolean remove(Object o) {
return removeMapping(o) != null;
}
public int size() {
return size;
}
public void clear() {
HashMap.this.clear();
}
} Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
} private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}

用法和上面两个并无差别

Set<Entry<String, String>> es = m.entrySet();
System.out.println(es);
Iterator<Map.Entry<String, String>> it = m.entrySet().iterator();
while (it.hasNext()) {
System.out.println(it.next());
}

3.HashMap的问题

HashMap的线程安全问题一直为人所诟病,幸好我们有了Hashtable、ConcurrentHashMap等安全的hashmap。

4.总结

  1. 允许以Key为null的形式存储<null,Value>键值对。

  2. HashMap的查找效率非常高,因为它使用Hash表对进行查找,可直接定位到Key值所在的链表中;

  3. 使用HashMap时,要注意HashMap容量和加载因子的关系,这将直接影响到HashMap的性能问题。加载因子过小,会提高HashMap的查找效率,但同时也消耗了大量的内存空间,加载因子过大,节省了空间,但是会导致HashMap的查找效率降低。需要使用接从中权衡利弊。

Java中关于HashMap源码的研究的更多相关文章

  1. Java中的HashMap源码记录以及并发环境的几个问题

    HashMap源码简单分析: 1 一切需要从HashMap属性字段说起: /** The default initial capacity - MUST be a power of two. 初始容量 ...

  2. Java HashSet和HashMap源码剖析

    转自: Java HashSet和HashMap源码剖析 总体介绍 之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也就是说Ha ...

  3. 【转】Java集合:HashMap源码剖析

    Java集合:HashMap源码剖析   一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调 ...

  4. 死磕Java之聊聊HashMap源码(基于JDK1.8)

    死磕Java之聊聊HashMap源码(基于JDK1.8) http://cmsblogs.com/?p=4731 为什么面试要问hashmap 的原理

  5. Java容器之HashMap源码分析

    在java的容器框架中,hashMap是最常用的容器之一,下面我们就来深入了解下它的数据结构和实现原理 先看下HashMap的继承结构图 下面针对各个实现类的特点进行下说明:1)HashMap: 它是 ...

  6. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  7. Java集合:HashMap源码剖析

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

  8. 死磕 java集合之HashMap源码分析

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改 ...

  9. JDK8中的HashMap源码

    背景 很久以前看过源码,但是猛一看总感觉挺难的,很少看下去.当时总感觉是水平不到.工作中也遇到一些想看源码的地方,但是遇到写的复杂些的心里就打退堂鼓了. 最近在接手同事的代码时,有一些很长的pytho ...

随机推荐

  1. PSP(4.6——4.12)以及周记录

    1.PSP 4.6 8:30 10:30 20 100 博客 B Y min 12:00 13:00 5 55 Account A Y min 13:05 13:15 0 10 站立会议 A Y mi ...

  2. USACO 2012 December ZQUOJ 24128 Wifi Setup(动态dp)

    题意:给出在同一条直线上的n个点和两个数A,B,现在要在这条直线上放置若干个信号塔,每个信号塔有一个r值,假设它的位置是x,则它能覆盖的范围是x-r~x+r,放置一个信号塔的花费是A+B*r,问要覆盖 ...

  3. Chrome Ajax 跨域设置

    一.前言 web 开发中 Ajax 是十分常见的技术,但是在前后端使用接口对接的调试过程中不可避免会碰到跨域问题.今天我给大家介绍一个十分简单有效的方法. 跨域经典错误 二.Chrome 跨域设置 首 ...

  4. Windows 10 中的存储空间

    存储空间有助于保护你的数据免受驱动器故障的影响,并随着你向电脑添加驱动器而扩展存储.你可以使用存储空间将两个或多个驱动器一起分组到一个存储池中,然后使用该池的容量来创建称为存储空间的虚拟驱动器.这些存 ...

  5. 初征——NOIP2018游记

    前言 从最初接触oi到今年noip到来,也已经将近有一年了.从对于程序一窍不懂到现在开始学习算法,只是短短的不到一年的时间罢了.这次noip,不仅仅是我oi生涯的第一次noip,更是相当于是对我这一年 ...

  6. Tomcat源码解析-整体流程介绍

    一.架构 下面谈谈我对Tomcat架构的理解 总体架构: 1.面向组件架构 2.基于JMX 3.事件侦听 1)面向组件架构 tomcat代码看似很庞大,但从结构上看却很清晰和简单,它主要由一堆组件组成 ...

  7. Django_博客项目 注册用户引发 ValueError: The given username must be set

    博客项目中 注册功能在ajax 提交数据时 报错 ValueError: The given username must be set 锁定到错误点为 判定为是无法获取到 username 字段 那先 ...

  8. Spoj 8372 Triple Sums

    题意:给你n个数字,对于任意s,s满足\(s=u_i+u_j+u_k,i<j<k\),要求出所有的s和对应满足条件的i,j,k的方案数 Solution: 构造一个函数:\(A(x)=\s ...

  9. 【BZOJ4883】棋盘上的守卫(最小生成树)

    [BZOJ4883]棋盘上的守卫(最小生成树) 题面 BZOJ 题解 首先\(n\)行\(m\)列的棋盘显然把行列拆开考虑,即构成了一个\(n+m\)个点的图.我们把格子看成边,那么点\((x,y)\ ...

  10. Winform Treeview 排序及图标处理

      一.排序 1. 继承 IComparer 2.  treeView1.TreeViewNodeSorter = this; 3. 实现IComparer public int Compare(ob ...