博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java 8 HashMap键与Comparable接口
阅读量:2218 次
发布时间:2019-05-08

本文共 2811 字,大约阅读时间需要 9 分钟。

这篇文章主要介绍了 Java 8 在 HashMap 哈希冲突处理方面的新特性。

相对之前的版本,Java 8 在许多方面有了提升。其中有很多类被更新了,HashMap 作为最常使用的集合类之一也不例外。这篇文章将介绍 Java 8 中的 HashMap 在处理哈希冲突时的新特性。

让我们从头开始。最容易使 HashMap 发生哈希冲突的方法是什么呢?我们可以创建一个类,让它的哈希函数返回一个最糟糕的结果 —— 比如一个常数。这也是我在面试的时候经常问面试者的问题:哈希方法返回常数会造成什么结果?有很多次面试者会回答说 map 集合里会有且仅有一个元素,因为 map 中的旧元素总会被新的覆盖。这个回答当然是错误的。哈希冲突并不会导致 HashMap 覆盖一个已经存在于集合中的元素,这种情况只会在使用者试图向集合中放入两个元素,并且它们的键对于 equal() 方法是相等的时候才会发生。键不相等但又会产生哈希冲突的不同元素最终会以某种数据结构存储在 HashMap 的同一个桶中(注意,在这种情况下,因为插入和查找的操作都要耗费更长的时间,所以整体的性能就会受到影响)。

首先,我们用一个小程序来模拟哈希冲突。下面的写法可能比较夸张,因为它造成的冲突比现实中多得多,但这个程序对于证实哈希冲突的条件还是很重要的。

我们使用一个 Person 对象作为 map 的键,以字符串作为值。下面是 Person 的具体实现,有一个 firstName 字段,一个 lastName 字段和一个 ID 属性,其中 ID 属性以 UUID 对象表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public
class
Person {
    
private
String firstName;
    
private
String lastName;
    
private
UUID id;
    
public
Person(String firstName, String lastName, UUID id) {
        
this
.firstName = firstName;
        
this
.lastName = lastName;
        
this
.id = id;
    
}
    
@Override
    
public
int
hashCode() {
        
return
5
;
    
}
    
@Override
    
public
boolean
equals(Object obj) {
        
// ... pertty good equals here taking into account the id field...
    
}
    
// ...
}

现在我们可以开始制造一些冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private
static
final
int
LIMIT = 500_000;
private
void
fillAndSearch() {
     
Person person =
null
;
     
Map<Person, String> map =
new
HashMap<>();
 
     
for
(
int
i=
0
;i<LIMIT;i++) {
        
UUID randomUUID = UUID.randomUUID();
        
person =
new
Person(
"fn"
,
"ln"
, randomUUID);
        
map.put(person,
"comment"
+ i);
     
}
     
long
start = System.currentTimeMillis();
     
map.get(person);
     
long
stop = System.currentTimeMillis();
     
System.out.println(stop-start+
" millis"
);
}

上面的代码在一台高性能计算机上运行了两个半小时。其中,最后的查找操作耗费了大约 40 毫秒。现在我们对 Person 类进行修改:使它实现 Comparable 接口,并且添加了下面的方法:

1
2
3
4
@Override
public
int
compareTo(Person person) {
    
return
this
.id.compareTo(person.id);
}

再一次运行之前的程序,这一次在我的机器上它耗费的时间少于 1 分钟。其中,最终的查找操作耗费的时间接近为 0 毫秒 —— 比之前提高了 150 倍!

就像前面说的,Java 8 做了很多优化,其中也包括HashMap 类。在 Java 7 中,两个不同的元素,如果它们的键产生了冲突,那么会被存储在同一个链表中。而从 Java 8 开始,如果发生冲突的频率大于某一个阈值(8),并且 map 的容量超过了另一个阈值(64),整个链表就会被转换成一个二叉树。

原来如此!所以,对于没有实现 Comparable 的键,最终的树是不平衡的;而对于实现了 Comparable 的键,其二叉树就会是高度平衡的。事实是这样吗?不是。HashMap 内部是红黑树,也就是说它总是平衡的。我通过反射机制,查看了最终的树结构。对于拥有 50000 个元素(不敢让数字更大了)的 HashMap 来说,两种不同的情况下(实现或是不实现 Comparable 接口)树的高度都是 19 。

那么,为什么之前的实验结果会有那么大的差别呢?原因在于,当 HashMap 想要为一个键找到对应的位置时,它会首先检查新键和当前检索到的键之间是否可以比较(也就是实现了 Comparable 接口)。如果不能比较,它就会通过调用 tieBreakOrder(Object a,Object b) 方法来对它们进行比较。这个方法首先会比较两个键对象的类名,如果相等再调用 System.identityHashCode 方法进行比较。这整个过程对于我们要插入的 500000 个元素来说是很耗时的。另一种情况是,如果键对象是可比较的,整个流程就会简化很多。因为键对象自身定义了如何与其它键对象进行比较,就没有必要再调用其他的方法,所以整个插入或查找的过程就会快很多。值得一提的是,在两个可比的键相等时(compareTo 方法返回 0)的情况下,仍然会调用 tieBreakOrder 方法。

总而言之,在 Java 8 的 HashMap 里,如果一个桶里存放了大量的元素,它在达到阈值时就会被转换为一棵红黑树,对于实现了 Comparable 接口的键来说,插入或删除的操作会比没有实现 Comparable 接口的键更简单。通常,如果一个桶不会发生那么多次冲突的话,这整个机制不会带来多大的性能提升,但起码现在我们对 HashMap 的内部原理有了更多了解。

转载地址:http://ttyfb.baihongyu.com/

你可能感兴趣的文章
Jmeter之参数化
查看>>
Shell 和Python的区别。
查看>>
【JMeter】1.9上考试jmeter测试调试
查看>>
【虫师】【selenium】参数化
查看>>
【Python练习】文件引用用户名密码登录系统
查看>>
学习网站汇总
查看>>
【Loadrunner】性能测试报告实战
查看>>
【自动化测试】自动化测试需要了解的的一些事情。
查看>>
【selenium】selenium ide的安装过程
查看>>
【手机自动化测试】monkey测试
查看>>
【英语】软件开发常用英语词汇
查看>>
Fiddler 抓包工具总结
查看>>
【雅思】雅思需要购买和准备的学习资料
查看>>
【雅思】雅思写作作业(1)
查看>>
【雅思】【大作文】【审题作业】关于同不同意的审题作业(重点)
查看>>
【Loadrunner】通过loadrunner录制时候有事件但是白页无法出来登录页怎么办?
查看>>
【English】【托业】【四六级】写译高频词汇
查看>>
【托业】【新东方全真模拟】01~02-----P5~6
查看>>
【托业】【新东方全真模拟】03~04-----P5~6
查看>>
【托业】【新东方托业全真模拟】TEST05~06-----P5~6
查看>>