花了点时间来研究HashMap的数据结构。看了源码之后不得不为设计者感到震惊!
下面讲讲有意思的方面:
一、关于key=null。把null作为key的话,我认为存取速度是最快的!因为在put和get之前都会去判断key是否为null,如果为null则会直接去取key为null的值,而且key=null的话,在容器Entry数组里面是存的0下标,直接可以取出,对象在Entry数组所存储下标是根据一个hash值和数字的长度减一相与而得来的,只要key是null 那么数组下标肯定是0,及时HashMap自己在做长度调整重新转储时也一样!记住快的原因不是因为下标为0哦~而是因为put和get之前都会做判断!不过有一个极端的情况就是Entry[0]这个链非常的长,那么速度也就不那么的快了。
二、HashMap容量变化。当HashMap容量“不够”时,容量会自动增长,增长容量的方式上一次数组长度*2,HashMap容量不手动设置的话初始值是16,并且会有一个loadfactor(默认初始值为0.75f)来决定是否现在进行容量增长,并不是等16用完才增长,是当容量>=loadfactor*1 6的时候就开始增长。至于默认这个因子为什么是0.75f,可能是由实践得出的结论吧,所有默认HashMap在放入12个对象后,下次再放对象则会重新改变容量大小为32,而下一次判断是否扩容时就是用32*loadfactor来决定了。
三、put普通key,普通对象的过程。在我们存放一个key!=null的情况下,对象的存储过程又是怎样呢?我们常常使用String来作为key,是为了方便识别,其实key也可以是一个普通的Object对象,或者是自己初始化的对象、甚至可以是**.class,在jvm看来它依然是一个Object而已,既然是Object那么就会有hashcode,如果你没有覆写hashcode方法,自己实现的对象hashcode会直接返回jvm对 对象在内存中的一个索引值,这也就是hashcode本身存在的意义---对象的快速寻址。
好了,不扯远了!继续put的过程,
先介绍HashMap里面的存储原子Entry,他是Map接口里面的一个内部接口,HashMap实现了它,有四个属性:Key、value、next、hash。key就是我们用的键值,value就是被存储的对象。next其实也是一个Entry对象,你可以认为这是一个链式结构,他指向下一个Entry对象,在Entry的具体对象里面可能存储了一个很长的链式结构,最后一个末端的对象的next是为null的。hash这个东西暂时认为是一个一个用于计算Entry数组下标的依据。你也可以认为是一个简单的索引。
HashMap在put对象之前会先要获得一个hash,这个hash是后来用于做Entry数组下标的一个依据之一,其实另外一个依据是数组的长度!后面再说。
获得hash这个值得过程很纠结,看代码
int hash = hash(key.hashCode());
...
//
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
先去key的hashcode,然后再进行了一番恶心的无符号移位与运算,至于为什么移位是20 、12、7、4 这个就是一个疑问了,就像是String的hashcode为什么移位是31(我的博客里面有讨论)....可能是处于性能方面的考虑,需要大神解释。
好了,这里就计算出了hash这个值!在HashMap存储一个对象之前,因为底层是一个数组,所以他要先计算出该存在哪个数组下标下面呢?于是有了下面的计算:
int i = indexFor(hash, table.length);
...//省略
//计算数组下标
static int indexFor(int h, int length) {
return h & (length-1);
}
有人可能会问,为什么要length-1,其实很简单,为了数组不越界,相与的结果做大也只能是length-1
好了,要存入的数组下标也已经计算出来了,现在就是要放入对象了....
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//这里为什么要比较e.hash==hash呢?看下面解答
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
//新老值替换
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
看看代码可以看出,在放入对象的之后直接定位的table[i],里面的i就是之前indexFor出来的值。直接来这个Entry链来进行比较有木有相同的key,注意注释里面写: e.hash==hash 是为了提升查找速度(我们常常覆写hashcode和equals方法,如果hashcode不相等可以立马判决这不是同一个对象,如果hashcode相等再进行equals对比!普通的没有覆写Object的equals方法的对象,其实是去对比内存地址是否相等的),直接数字的对比要比key的对比快,如果发现hash值不相等,那么就可以立马执行下一次循环对比!找到则进行新老值替换,如果没有找到就立马新加一个Entry对象到该Entry链中,注意下面一个细节:
//这里的i就是之前计算出来的
// int hash = hash(key.hashCode());
//int i = indexFor(hash, table.length);
addEntry(hash, key, value, i);
.....
//
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);//扩容
}
这样,一个对象就被放入了!
四、关于get方法。
public V get(Object key) {
if (key == null)
return getForNullKey();//key为null的情况
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;
}
看看上面的代码,可以知道1个问题:
hash这个值是那么的重要,能够帮HashMap快速定位到具体哪个Entry原子,现在很多公司在使用Mysql的时候,为了解决单表数据量过多导致操作表速度变慢的一个解决方法就是分库分表,其实也是一个道理,分库分表也相当于这里的一个Entry原子而已,hash看起来就像是一个索引,帮我快速定位!
五、关于entrySet方法。
为了方便我们遍历Map,map提供了一个entrySet方法,使其转化为一个Set的子类。由各个子类去自己实现其遍历的方法。对于HashMap而言,遍历从Entry[0]开始,先把Entry[0]这个链表遍历完毕,然后再遍历Entry[1]....,类似遍历二维数组,或者说..类似深度遍历!
其中还有一个keySet方法,也是和entryset方法类似,只是在内部把key单独的从entry剥离出来。
最后、关于putAll方法,即把被put的map里面的内容copy或者覆盖到当前map里面,在之前会进行一些容量计算或者扩容的操作,最后循环调用put方法。
=======================================
好了,看了看都是Java的基础,小弟不才...只是回过头来看看这些设计思想也挺有意思。上面内容有很多属于个人的YY,有不同意见不吝赐教,望高人指点。
分享到:
相关推荐
在IT领域,Java容器是一个非常重要的概念,尤其对于软件开发者来说,它们是理解和构建高效、可扩展的应用程序的关键。本文将深入探讨Java容器,并结合标签“源码”和“工具”,从源码层面和实用工具角度来分析这些...
Java容器详细解析 Java容器是一种基本的数据结构,用于存储和管理对象。Java容器主要分为两大类:Collection和Map。 Collection Collection是一个独立元素的序列,这些元素都服从一条或多条规则。Collection接口...
在Java中,HashMap广泛应用于Set、Map等容器中,用于快速根据Key找到元素。例如,Set的contains方法和Map的get方法都是通过Key去查找的。 然而,HashMap的实现也存在一些问题,例如哈希碰撞问题和equals方法的调用...
### Java容器学习心得详解 在Java编程中,容器(Containers)是存储和操作对象集合的重要工具,主要包括集合(Collections)和映射(Maps)。本文将深入解析Java容器的关键概念、特性以及不同容器类型的应用场景。 ...
这篇博客"JAVA容器对象整理"可能涵盖了关于Java中的不同容器类、接口以及它们的使用方式。在这里,我们将深入探讨一些核心的Java容器知识点。 1. **ArrayList与LinkedList** - `ArrayList`是一个基于数组实现的...
在Java中,最常见的对象容器包括ArrayList、List、Set和HashMap等。这些容器各自具有不同的特性和用途,理解并熟练掌握它们对于提升Java编程能力至关重要。 ArrayList是Java集合框架中的一个动态数组,它允许我们在...
### Java使用WebService读取HashMap里的数值 #### 背景介绍 在Java开发中,`WebService`是一种常用的技术栈,用于实现不同系统间的通信。它允许应用程序之间通过标准的HTTP协议进行数据交换与方法调用,这对于...
通过这些练习,你将巩固对Java容器的理解,提高代码编写效率,并为解决实际问题打下坚实基础。记得在实践中不断挑战自己,尝试不同的场景和数据结构,以便更好地掌握Java容器的精髓。祝你在学习过程中取得优异的成绩...
Java 容器详解 Java 容器是 Java 语言中的一种集合类库,主要包括 Collection 和 Map 两种类型。Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 Collection Collection 是一种集合接口...
Java容器是Java编程中至关重要的一个部分,它们用于存储、管理和操作对象集合。在这个主题下,我们将深入探讨Java中的核心容器类,包括数组、List、Set和Map,以及它们各自的特点和使用场景。 1. **数组**:数组是...
Java容器讲解PPT,Collection Map(HashMap TreeMap LinkedHashMap) List (ArrayList LinkedList Vector) Set (HashSet TreeSet LinkedHashSet)
在Java编程中,容器是用来存储和管理对象的类或接口,它们使得我们可以在程序中方便地组织和操作数据。在Java中,常见的容器主要分为三类:List、Set和Map,这些都是Java集合框架的重要组成部分。 首先,我们来看...
《Java容器HashMap与HashTable详解》 HashMap和HashTable是Java中两种重要的数据结构,它们都是用于存储键值对的数据容器,但两者在设计和使用上有显著的差异。 HashMap是Java集合框架的一部分,它继承自...
在Java编程语言中,容器(Container)是一种用来存储和管理数据结构的重要概念,它提供了组织、存储和操作数据的方式。容器通常指的是集合框架中的各种类,如List、Set、Map等,它们允许开发者以不同的方式存储和...
Java 类容器是 Java 编程中非常重要的一个概念,它主要指的是 Java 集合框架中的各种类,如 ArrayList、LinkedList、HashSet、HashMap 等,这些类用于存储和管理对象。本文将深入探讨这些常用的Java类容器,帮助...
Java容器类是Java编程语言中不可或缺的一部分,它们主要用于存储和管理对象。这些类和接口位于`java.util`包中,为开发者提供了灵活的数据结构和数据操作方式。在Java中,容器类主要分为两大类:Collection和Map。 ...
【JAVA容器试题解析】 一、不定选择题 1. Java 容器框架主要分为 Collection 和 Map 两种。其中,Collection 又分为A、List,B、Set,C、Queue,D、以上都是。答案:D。 2. 以下哪一个是线程安全的:A、Vector,B...
Java容器类是Java集合框架的重要组成部分,它们提供了一种存储、管理和操作对象的方式。在Java中,容器类包括数组、列表、队列、集、映射等数据结构,它们为开发者提供了灵活的数据处理能力。本篇文章将深入探讨Java...
Java容器(集合框架)是Java编程中极其重要的部分,它提供了多种数据结构,如列表、集合和映射,以适应不同场景下的数据存储和处理需求。通过合理选择和使用不同的容器,可以优化代码的性能和可维护性。同时,了解和...