一、Java中的hashCode()和equals()
  1、 hashCode()的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode()是用来在散列存储结构中确定对象的存储地址的;
  2、如果两个对象相同,就是指对象调用equals()方法返回true,那么这两个对象的hashCode()方法的返回值一定要相同;
  3、如果对象的equals()方法被重写,那么对象的hashCode()方法也尽量重写,并且hashCode()方法使用的对象的变量信息,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
  4、 两个对象的hashCode()相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里“。

  再归纳一下就是hashCode()是用于查找使用的,而equals()是用于比较两个对象是否相等的。

二、HashMap类的实现原理

  1、HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  2、在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

  3、HashMap的存取:

    1)利用key的hashCode重新hash计算出当前对象的元素在数组中的下标(索引计算公式:(length-1)& hash);
    2)存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
    3)获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
    4)理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

三、HashMap的扩容机制

  当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

/**
 * jdk版本:1.8.0_111。
* 使用HashMap的空参构造创建一个HashMap集合,初始容量为16;负载因子loadFactor为0.75;
* 当集合的元素个数超过16*0.75=12时集合容量扩大一倍。*/
public class Demo01 {
public static void main(String[] args) throws Exception {
HashMap<String, Object> map = new HashMap<String, Object>();
System.out.println(map.size()); //
System.out.println(getCapacity(map)); // map.put("abc0", 1);
map.put("abc16", 1);
map.put("abc27", 1);
map.put("abc49", 1);
System.out.println(map.size()); //
System.out.println(getCapacity(map)); // String key = null;
for (int i = 0; i < 8; i++) {
key = "a" + i;
map.put(key, key);
}
System.out.println(map.size()); //
System.out.println(getCapacity(map)); //
Object obj = map; map.put("a8", "a8");
System.out.println(map.size()); //
System.out.println(getCapacity(map)); //
System.out.println(obj == map); // true,扩容后引用地址没变
} public static Integer getCapacity(Map<?, ?> map) throws Exception {
Method method;
method = map.getClass().getDeclaredMethod("capacity");
method.setAccessible(true);
return (int) method.invoke(map);
}
}

  再看下面的一个例子:

/**
* jdk版本:jdk1.7.0_60。
* 创建一个HashMap集合,初始容量为4,加载因子loadFactor为0.75;
* 我们理应认为:当集合的元素个数超过4*0.75=3时集合容量扩大一倍。
* 但是当使用jdk1.7.0_60时,结果并非如此。
*/
public class Demo2 {
public static void main(String[] args) throws Exception {
HashMap<Integer, Object> map = new HashMap<>(4, 0.75f);
System.out.println(map.size()); //
System.out.println(getCapacity(map)); // map.put(0, 1);
map.put(4, 1);
map.put(8, 1);
System.out.println(map.size()); //
System.out.println(getCapacity(map)); //
map.put(1, 1);
System.out.println(map.size()); //
System.out.println(getCapacity(map)); //
map.put(2, 1);
System.out.println(map.size()); //
System.out.println(getCapacity(map)); //
map.put(3, 1);
System.out.println(map.size()); //
System.out.println(getCapacity(map)); //
map.put(5, 1);
System.out.println(map.size()); //
System.out.println(getCapacity(map)); // } public static Integer getCapacity(Map<?, ?> map) throws Exception {
Method method;
method = map.getClass().getDeclaredMethod("capacity");
method.setAccessible(true);
return (int) method.invoke(map);
}
}

  查看jdk1.7.0_60HashMap源码:问题的关键在于标红的代码。

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);
}

四、为什么HashMap容量为2的次幂?

  看了一些资料,以下是自己的理解:

  HashMap底层实现为数组+单链表,元素的存取是根据数组索引来操作的。那么HashMap是如何计算元素在数组中的索引呢?

  首先,HashMap的元素是包含key、value的键值对,HashMap是根据key值,加上一些算法计算得到该元素在数组中的索引。具体算法如下:

// 传入key,得到对应的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} /**
* @param key 键值对的key
* @param capicity HashMap集合的容量
* @return
*/
static int getIndex(Object key, int capicity) {
int hash = hash(key); // 传入key,得到对应的hash值
int index = (capicity - 1) & hash; // 得到元素在数组中的索引
return index;
}

  

  综上可知,HashMap集合是根据 (capicity - 1) & key的hash值 来计算元素在数组中的索引。

  假设HashMap集合的容量capacity=15,则元素在数组中的索引 index = (capacity-1) & hash = 14 & hash = 0b1110 & hash,则index只有8种结果:0、2、4、6、8、10、12、14,意思就是数组的某些位置不能存值,浪费空间,也加大了hash冲突的可能。

  如果HashMap集合的容量capacity=2的n次方,则元素在数组中的索引 index = (2n-1) & hash = 0b11...11(n个1) & hash,则index有2n种结果,即是0、1、2 ... 2n-1,所以数组的每个位置都会存值,基本上均匀地分布,有效利用空间,也减少了hash冲突,提高HashMap集合的性能。

  事实上,只有当HashMap集合的容量capacity=2n时,(capacity-1) & hash 与 hash % capacity 的结果是等效的。

  写个小案例体会一下:

public class Demo1 {
public static void main(String[] args) {
String key = null; int capicity = 15; // 容量
int[] array = new int[capicity]; for (int i = 0; i < 10000; i++) {
key = "abc1" + i;
int index = getIndex(key, capicity);
if (index < capicity) {
array[index] += 1;
}
}
//capicity=16: [642, 643, 600, 590, 556, 538, 572, 554, 609, 607, 655, 665, 692, 711, 674, 692]
//capicity=8: [1251, 1250, 1255, 1255, 1248, 1249, 1246, 1246]
//capicity=4: [2499, 2499, 2501, 2501] //capicity=15: [1285, 0, 1190, 0, 1094, 0, 1126, 0, 1216, 0, 1320, 0, 1403, 0, 1366]
//capicity=14: [1242, 1233, 0, 0, 1128, 1092, 0, 0, 1264, 1272, 0, 0, 1366, 1403]
System.out.println(Arrays.toString(array));
} // 传入key,得到对应的hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} /**
* 得到元素在数组中的索引
* @param key 键值对的key
* @param capicity HashMap集合的容量
* @return
*/
static int getIndex(Object key, int capicity) {
int hash = hash(key); // 传入key,得到对应的hash值
int index = (capicity - 1) & hash; // 得到元素在数组中的索引
return index;
}
}

参考资料:

  1、Java中HashMap的实现原理

  2、HashMap中的为什么hash的长度为2的幂而&位必须为奇数

  3、Hashmap为什么容量是2的幂次,什么是负载因子

  4、HashMap容量为2次幂的原因(碰撞冲突拉链法解决)

  5、HashMap:为什么容量总是为2的次幂(推荐)

谈一谈HashMap类的更多相关文章

  1. 谈一谈HashMap类2

    1.由一个小案例引出本博文的讨论 public class Demo1 { public static void main(String[] args) throws Exception { Stud ...

  2. 谈一谈Java8的函数式编程(二) --Java8中的流

    流与集合    众所周知,日常开发与操作中涉及到集合的操作相当频繁,而java中对于集合的操作又是相当麻烦.这里你可能就有疑问了,我感觉平常开发的时候操作集合时不麻烦呀?那下面我们从一个例子说起. 计 ...

  3. 谈一谈泛型(Generic)

    谈一谈泛型 首先,泛型是C#2出现的.这也是C#2一个重要的新特性.泛型的好处之一就是在编译时执行更多的检查. 泛型类型和类型参数 ​ 泛型的两种形式:泛型类型( 包括类.接口.委托和结构 没有泛型枚 ...

  4. 谈一谈深度学习之semantic Segmentation

    上一次发博客已经是9月份的事了....这段时间公司的事实在是多,有写博客的时间都拿去看paper了..正好春节回来写点东西,也正好对这段时间做一个总结. 首先当然还是好好说点这段时间的主要工作:语义分 ...

  5. 谈一谈并查集QAQ(上)

    最近几日理了理学过的很多oi知识...发现不知不觉就有很多的知识忘记了... 在聊聊并查集的时候顺便当作巩固吧.... 什么是并查集呢? ( Union Find Set ) 是一种用于处理分离集合的 ...

  6. 谈一谈C#的事件

    谈一谈C#的事件 C#中事件基于委托,要理解事件要先理解委托,如果觉得自己关于委托不是很了解可以看看我前面写委托的文章 事件基于委托,是一种功能受限的委托,为委托提供了一种发布/订阅机制 使用委托时, ...

  7. Hashtable和HashMap类的区别

    Hashtable和HashMap类有三个重要的不同之处.第一个不同主要是历史原因.Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现. ...

  8. Java API —— HashMap类 & LinkedHashMap类

    1.HashMap类 1)HashMap类概述         键是哈希表结构,可以保证键的唯一性 2)HashMap案例         HashMap<String,String>   ...

  9. JAVA中的数据结构——集合类(线性表:Vector、Stack、LinkedList、set接口;键值对:Hashtable、Map接口<HashMap类、TreeMap类>)

    Java的集合可以分为两种,第一种是以数组为代表的线性表,基类是Collection:第二种是以Hashtable为代表的键值对. ... 线性表,基类是Collection: 数组类: person ...

随机推荐

  1. C# DateTime判断时间

    两种情况: 1 DateTime? dtTemp = null; if(dtTime != null) { //wawawa } 刚刚学会的,可空值类型,可判断是否赋值 2 DateTime dtTe ...

  2. P3412 仓鼠找sugar II

    思路 挺神的概率期望.. 好吧是我太弱了,完全没有往那里想 注意期望是具有线性性的,一条路径的期望可以变成每条边的期望求和 概率是某件事发生的可能性,期望是某件事确定发生的代价 首先没有终点的条件并不 ...

  3. UVA11270 Tiling Dominoes(轮廓线动态规划)

    轮廓线动态规划是一种基于状态压缩解决和连通性相关的问题的动态规划方法 这道题是轮廓线动态规划的模板 讲解可以看lrj的蓝书 代码 #include <cstdio> #include &l ...

  4. Redis 应用:缓存

    使用Redis做预定库存缓存功能 缓存是在业务层做的,准确讲应该是在MVC模型中Model的ORM里面 PHP项目的缓存从以前的APC缓存逐渐切换到Redis中,并且根据Redis所支持的数据结构做了 ...

  5. Java 基础功底

    Java 基础语法特性: 首先了解并做好Java Web 开发环境配置(包含 JDK 的配置)是非常必要的.其中 CLASSPATH 的值开始必须包含 ".",否则用 javac ...

  6. sublime text3 license

    —– BEGIN LICENSE —– Michael Barnes Single User License EA7E-821385 8A353C41 872A0D5C DF9B2950 AFF6F6 ...

  7. ERR! registry error parsing json

    报错日志: ERR! registry error parsing json ERR! registry error parsing json 解决过程: 从github上克隆一个项目,在npm i的 ...

  8. P1547 Out of Hay

    传送门     练习 只是一个最小生成树的水题,拿来练练模板 AC代码: #include<iostream> #include<cstdio> #include<alg ...

  9. yum节省安装时间

    yum install java-1.8.0-openjdk 安装jdk yum install tomcat 安装tomcat wget http://repo.mysql.com/mysql-co ...

  10. win7下配置Tomcat

    1.下载tomcat 2.添加系统环境变量,我的电脑->属性->高级系统设置->环境变量 (1)变量名: CATALINA_BASE     变量值: D:\Program File ...