大佬也Hashcode方法上翻船了,不小心秀了一把!
本文轉載自微信公眾號「程序新視界 」,作者二師兄。轉載本文請聯(lián)系程序新視界 公眾號。
前些天寫了幾篇面試題的文章,其中包括《重寫equals方法為什么通常會重寫hashcode方法?》,有朋友可能會說,這類面試題都是“面試造火箭,工作擰螺絲”。不可否認,有些面試題的確如此。
但就在今天,因為懂了這篇文章中的知識竟然在大佬面前秀了一把,幫大佬解決了疑問,還換來了一個趕明兒請吃飯的“口頭支票”,哈哈~~
下面就來聊聊大佬遇到的奇怪問題以及排查解決過程。
大佬的疑惑
大佬在項目中寫了類似這樣的一段代碼:
- List<ProjectId> list = new ArrayList<>();
- // 省略add數(shù)據(jù)操作
- List<DeviceModel> models = list.stream().map(ProjectId::getDeviceModel).distinct().collect(Collectors.toList());
- System.out.println(models);
結果呢,這段代碼中的distinct()方法并沒有起效,并沒有達到去重的預期。
但大佬并沒有放棄,先是查了該方法的文檔:
Returns a stream consisting of the distinct elements (according to Object.equals(Object)) of this stream.
For ordered streams, the selection of distinct elements is stable (for duplicated elements, the element appearing first in the encounter order is preserved.) For unordered streams, no stability guarantees are made.
This is a stateful intermediate operation.
通過API文檔來看并沒有問題,進而大佬開啟了debug模式,發(fā)現(xiàn)奇怪的是實體類的equals方法都沒進。
大佬解決問題思路值得我們先學習一波,在大佬決定最終放棄的前,給我發(fā)消息了,問有興趣看一看沒。有這么奇怪的現(xiàn)象,怎能不研究一下呢?
解決思路
根據(jù)大佬發(fā)的部分代碼和實現(xiàn)思路,把整個模擬的測試程序補充完整,創(chuàng)建了兩個實體類ProjectId和DeviceModel,并重寫了equals方法(跟大佬溝通,他重寫了equals方法,并且單獨使用是生效的)。
DeviceModel實體類,簡單重寫了equals方法,只比較字段no是否相等。
- @Data
- public class DeviceModel {
- private String no;
- @Override
- public String toString(){
- return no;
- }
- @Override
- public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
- if (other == null || getClass() != other.getClass()) {
- return false;
- }
- return this.toString().equals(other.toString());
- }
- }
ProjectId實體類,重寫了equals方法,
- @Data
- public class ProjectId {
- private int id;
- private DeviceModel deviceModel;
- }
然后,構建了測試類:
- public class Test {
- public static void main(String[] args) {
- List<ProjectId> list = new ArrayList<>();
- DeviceModel device1 = new DeviceModel();
- device1.setNo("1");
- ProjectId projectId1 = new ProjectId();
- projectId1.setDeviceModel(device1);
- projectId1.setId(1);
- list.add(projectId1);
- DeviceModel device2 = new DeviceModel();
- device2.setNo("1");
- ProjectId projectId2 = new ProjectId();
- projectId2.setDeviceModel(device2);
- projectId2.setId(1);
- list.add(projectId2);
- DeviceModel device3 = new DeviceModel();
- device3.setNo("2");
- ProjectId projectId3 = new ProjectId();
- projectId3.setDeviceModel(device3);
- projectId3.setId(2);
- list.add(projectId3);
- List<DeviceModel> models = list.stream().map(ProjectId::getDeviceModel).distinct().collect(Collectors.toList());
- System.out.println(models);
- }
- }
先構建了一組數(shù)據(jù),然后讓device1與device2的no屬性一樣,重寫了equals方法,理論上它們應該是相等的,device3對象用來做對照。
執(zhí)行上面的程序,控制臺打印如下:
- [1, 1, 2]
的確還原了大佬的bug,也奇怪為什么會這樣。但既然bug已重現(xiàn),解決就是比較簡單的事了。
此時,大佬又發(fā)來另外一個線索,說通過for循環(huán)形式沒事:
- List<DeviceModel> results = new ArrayList<>();
- for (DeviceModel deviceModel : list.stream().map(ProjectId::getDeviceModel).collect(Collectors.toList())) {
- if (!results.contains(deviceModel)) {
- results.add(deviceModel);
- }
- }
- System.out.println(results);
這種實現(xiàn)形式恰好又可以用來做對照。
問題排查
進行問題排查時首先也想到了debug,但是同樣出現(xiàn)并未走equals方法的情況。
仔細看了一下代碼,發(fā)現(xiàn)在Stream處理的過程中用到了map操作。而在之前的文章中也提到,Map中判斷一個對象是否已經存在是先通過key的hash值定位到對應的數(shù)組下標,如果該位置上的Entry沒有值,則直接保存;如果已經有存在的值,再通過equals方法比較值是否一樣。
那么,是不是因為重寫了equals方法,而沒有重寫hashcode方法導致的呢?于是,在DeviceModel類中新增了hashcode方法:
- @Override
- public int hashCode() {
- // JDK7新增的Objects工具類
- return Objects.hash(no);
- }
再次執(zhí)行,測試方法,發(fā)現(xiàn)可以成功去重了。很顯然,大佬的失誤是在重寫equals方法時違背了一條原則:如果一個類的equals方法相等,那么它們的hashcode方法必須相等。由于沒有重寫hashcode方法導致違背這一原則。因此,在隱式使用Map時就出現(xiàn)了莫名其妙的問題。
后續(xù)
經過這一番周折,問題終于解決。想必大家更也更加明白了為什么重寫equals方法一定要重寫hashcode方法了。后面大佬又考問我一個問題:為什么list.contains方法不會出現(xiàn)這個問題呢?
因為List的底層結構是數(shù)組,不像Map那樣為了提升效率先對Key進行hash處理比較。簡單看一下ArrayList中contains方法的核心實現(xiàn):
- public int indexOf(Object o) {
- if (o == null) {
- for (int i = 0; i < size; i++)
- if (elementData[i]==null)
- return i;
- } else {
- for (int i = 0; i < size; i++)
- if (o.equals(elementData[i]))
- return i;
- }
- return -1;
- }
可以看出如果對象不為null時,還是循環(huán)調用的equals方法來處理的。
小結
通過本篇文章講了一個幫大佬定位問題的故事,感謝大佬給我一個很好的寫作素材,這期間有很多值得學習和借鑒的內容。從側面也證明,有些面試題的確有它的價值,如果你以為只是在造飛機,真有可能是在實踐中沒遇跳到坑里到而已。
最后,大佬就是因為沒好好看公眾號的上篇文章,才掉坑里的[捂臉][捂臉][捂臉]。所以,間接說明本公眾號的內容對大家還是能提供一些幫助的,感興趣就關注一下。也歡迎直接加微信好友,探討一些有意思的技術問題。