java.util.HashMap 是一个很常用的集合类,它用数组来存储数据。
transient Entry[] table;
它会对KEY进行HASH运算,如果哈希值冲突,HashMap采用链表来解决
Entry就是HashMap存储数据所用的类
final K key;
V value;
final int hash;
Entry<K,V> next;
next就是为了哈希冲突而存在的,比如一个新对象通过哈希运算应该在第10位置,可10位置已存放Entry对象,那么把新对象也放到第10位置,将第10个位置的原有Entry赋值给当前新加的 Entry的next属性,数组存储的是链表,链表是为了解决哈希冲突的。
几个关键的属性
transient Entry[] table;
默认容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; 1073741824
默认加载因子,加载因子是一个比例,当HashMap的数据大小>=容量*加载因子时,HashMap会将容量扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
当实际数据大小超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子
int threshold;
加载因子
final float loadFactor;
哈希算法
HashMap并不是直接将对象的hashcode作为哈希值的,而是要把key的hashcode作一些运算以得到最终的哈希值,并且得到的哈希值也不是在数组中的位置
无论是get还是put还是别的方法,计算哈希值都是这一句:
int hash = hash(key.hashCode());
hash函数如下:
static int hash(int h) {
return useNewHash ? newHash(h) : oldHash(h);
}
哈希函数
private static int oldHash(int h) {
h += ~(h << 9);
h ^= (h >>> 14);
h += (h << 4);
h ^= (h >>> 10);
return h;
}
确定数据的位置
int hash = hash(k.hashCode());
int i = indexFor(hash, table.length);
第一行,上面讲过了,是得到哈希值,第二行,则是根据哈希指计算元素在数组中的位置了,位置的计算是将哈希值和数组长度按位与运算。
static int indexFor(int h, int length) {
return h & (length-1);
}
首先我们要确定一下,HashMap的数组长度永远都是偶数,即使你在初始化的时候是这样的new HashMap(15,0.75);因为在构造函数内部,上面也讲过,有这样的一段代码:
while (capacity < initialCapacity)
capacity <<= 1;
所以length-1一定是个奇数,假设现在长度为16,减去1后就是15,对应的二进制是:1111。
假设有两个元素,一个哈希值是8,二进制是1000,一个哈希值是9,二进制是1001。和1111与运算后,分别还是1000和1001,它们被分配在了数组的不同位置,这样,哈希的分布非常均匀。
那么,如果数组长度是奇数,减去1后就是偶数了,偶数对应的二进制最低位一定是0了,例如14二进制1110。对上面两个数子分别与运算,得到1000和1000。看到了吗?都是一样的值,哈希值8和9的元素多被存储在数组同一个位置的链表中。在操作的时候,链表中的元素越多,效率越低,因为要不停的对链表循环比较。所以,一定要哈希均匀分布,尽量减少哈希冲突,减少了哈希冲突,就减少了链表循环,就提高了效率。
put方法到底作了什么?
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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++;
addEntry(hash, key, value, i);
return null;
}
如果key为NULL,则是单独处理的,看看putForNullKey方法:
private V putForNullKey(V value) {
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++;
addEntry(0, null, value, 0);
return null;
}
NULL_KEY的声明:static final Object NULL_KEY = new Object();
这一段代码是处理哈希冲突的,就是说,在数组某个位置的对象可能并不是唯一的,它是一个链表结构,根据哈希值找到链表后,还要对链表遍历,找出key相等的对象,替换它,并且返回旧的值。
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;
}
}
如果遍历完了该位置的链表都没有找到有key相等的,那么将当前对象增加到链表里面去
modCount++;
addEntry(0, null, value, 0);
return null;
且看看addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一个Entry对象,并放在当前位置的Entry链表的头部,看看下面的 Entry构造函数就知道了,注意红色部分
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
何扩容?
当put一个元素时,如果达到了容量限制,HashMap就会扩容,新的容量永远是原来的2倍。
上面的put方法里有这样的一段:
if (size++ >= threshold)
resize(2 * table.length);
这是扩容判断,要注意,并不是数据尺寸达到HashMap的最大容量时才扩容,而是达到 threshold指定的值时就开始扩容, threshold=最大容量*加载因子。 看看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);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
tranfer方法将所有的元素重新哈希,因为新的容量变大,所以每个元素的哈希值和位置都是不一样的。
如何寻找元素
通过上面代码的分析,应该可以很清楚的看到,HashMap的元素查找大致分为三步:
根据key的hasocde()得到哈希值
根据哈希值确定元素在数组中的位置
找到指定位置的链表,循环比较,先“==”比较,如果不等,再“equals”比较,如果有一个比较相等,就说明找到元素了。
所以说到这里,我想大家也明白了,为什么要把一个对象放进HashMap的时候,最好是重写hashcode()方法和equals 方法呢?根据前面的分析,hashcode()可以确定元素在数组中的位置,而equals方法在链表的比较时要用到。
正确的使用HashMap
1:不要在并发场景中使用HashMap
HashMap是线程不安全的,如果被多个线程共享的操作,将会引发不可预知的问题,据sun的说法,在扩容时,会引起链表的闭环,在get元素时,就会无限循环,后果是cpu100%。
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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.equals(k)))
return e.value;
}
return null;
}
2:如果数据大小是固定的,那么最好给HashMap设定一个合理的容量值
根据上面的分析,HashMap的初始默认容量是16,默认加载因子是0.75,也就是说,如果采用HashMap的默认构造函数,当增加数据时,数据实际容量超过10*0.75=12时,HashMap就扩容,扩容带来一系列的运算,新建一个是原来容量2倍的数组,对原有元素全部重新哈希,如果你的数据有几千几万个,而用默认的HashMap构造函数,那结果是非常悲剧的,因为HashMap不断扩容,不断哈希,在使用HashMap的场景里,不会是多个线程共享一个HashMap,除非对HashMap包装并同步,由此产生的内存开销和cpu开销在某些情况下可能是致命的。
分享到:
相关推荐
Java 集合框架(JCF:Java Collections Framework)之概述 Java 集合框架(JCF:Java Collections Framework)是 Java 语言中的一组类库,用于实现集合操作的统一标准。集合是计算机科学中的一种基本概念,来源于...
在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...
深入的洞察到Java集合框架的核心组件:Map, List, 和 Set。首先,深入分析了HashMap的内部结构,包括它的数组+链表+红黑树的数据结构。重要的是理解如何处理并发问题,特别是在Java 8中对HashMap的优化,如高低位...
HashMap是Java集合框架中Map接口的典型实现之一,主要用于存储键值对(Key-Value)数据。其特性如下: - 线程不安全,方法为非同步方法,因此多线程环境下访问可能会出现并发问题。 - 允许使用null作为键(Key)和...
#### 二、Java集合框架结构 Java集合框架的核心部分包括以下几类: - **集合接口**:主要包括`Collection`、`Set`、`List`和`Map`等接口。 - **抽象类**:提供了部分接口的默认实现,如`AbstractCollection`、`...
### Java集合框架详解 #### 一、Java集合框架概述 Java集合框架是Java标准库的重要组成部分,它提供了存储和操作对象的各种数据结构。通过使用集合框架,开发人员可以轻松地管理不同类型的数据集,并且能够利用...
根据提供的文档内容,文件是关于Java集合框架的面试题知识点总结。以下是Java集合框架的知识点详述: Java集合框架主要包括Collection接口和Map接口两大分支。Collection接口主要包括List、Set以及Queue三个子接口...
Java集合框架是Java语言提供的用于存储数据和操作数据的集合类库。在Java集合框架中,Map接口的实现类广泛用于存储键值对数据结构。主要实现类包括HashMap、Hashtable、LinkedHashMap和TreeMap。 1. HashMap:它...
Java 集合框架概述 Java 集合框架是 Java 语言中提供的一种统一的标准体系结构,用于表示和操作集合。集合框架包含三大块内容:对外的接口、接口的实现和对集合运算的算法。 1. 接口:Collection 顶层接口是集合...
Java集合框架是Java编程语言中一个至关重要的组成部分,它提供了数据结构和算法的抽象,使得开发者可以方便地存储和管理各种类型的数据。本篇将详细探讨Java集合框架的基础知识,包括核心接口、类的层级结构以及Java...
Java集合框架是Java编程语言中一个非常重要的组成部分,它提供了一组高效的数据结构和操作这些数据结构的方法。泛型则是Java在J2SE 5.0版本引入的一个特性,极大地提高了代码的类型安全性和可读性。下面我们将深入...
Java集合框架是Java编程语言中的一个核心组成部分,它为数据存储和操作提供了丰富的类库。在Java中,集合框架主要包括接口(如List、Set、Queue)和实现这些接口的类(如ArrayList、HashSet、LinkedList等)。这个...
首先,Java集合框架包括List、Set、Queue等接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类。这些接口和类提供了存储、检索、遍历和修改元素的方法。例如,ArrayList是以动态数组为基础的列表实现,适合...
之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也是说HashSet里面有一个HashMap(适配器模式)。因此本文将重点分析HashMap。 HashMap实现了Map...
Java集合框架是Java编程语言中一个重要的组成部分,它提供了一组高级数据结构,使得程序员能够高效地管理和操作数据。在本篇文章中,我们将深入探讨Java集合框架中的主要接口和类,以及它们之间的区别。 首先,Java...
HashMap是Java集合框架的一部分,它实现了Map接口,用于存储键值对。其核心特点在于基于哈希表的映射机制,能够通过键快速检索对应的值。HashMap支持键和值为null,同时是非线程安全的,即其方法不是同步的。 2. ...
JAVA集合框架学习笔记 JAVA集合框架是Java语言中提供的一种数据结构,用于存储和管理大量数据。它提供了多种接口和实现类,例如Collection、List、Map等,帮助开发者更方便地处理数据。 什么是异常? 在Java中,...
Java集合框架是Java编程语言中处理对象集合的一套接口和类。该框架提供了用于存储和操作集合的标准方法。在Java集合框架中,基本的接口分为两大类:Collection和Map。 Collection接口用于表示一组对象,称为其元素...