大佬也Hashcode方法上翻船了,不小心秀了一把!

上一篇 / 下一篇  2021-03-26 15:57:47

  大佬的疑惑
  大佬在項目中寫了類似這樣的一段代碼:
  List<ProjectId> list = new ArrayList<>(); 
  // 省略add數據操作 
  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模式,發現奇怪的是實體類的equals方法都沒進。
  大佬解決問題思路值得我們先學習一波,在大佬決定最終放棄的前,給我發消息了,問有興趣看一看沒。有這么奇怪的現象,怎能不研究一下呢?
  解決思路
  根據大佬發的部分代碼和實現思路,把整個模擬的測試程序補充完整,創建了兩個實體類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); 
   
      } 
  } 
  先構建了一組數據,然后讓device1與device2的no屬性一樣,重寫了equals方法,理論上它們應該是相等的,device3對象用來做對照。
  執行上面的程序,控制臺打印如下:
  [1, 1, 2] 
  的確還原了大佬的bug,也奇怪為什么會這樣。但既然bug已重現,解決就是比較簡單的事了。
  此時,大佬又發來另外一個線索,說通過for循環形式沒事:
  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); 
  這種實現形式恰好又可以用來做對照。
  問題排查
  進行問題排查時首先也想到了debug,但是同樣出現并未走equals方法的情況。
  仔細看了一下代碼,發現在Stream處理的過程中用到了map操作。而在之前的文章中也提到,Map中判斷一個對象是否已經存在是先通過key的hash值定位到對應的數組下標,如果該位置上的Entry沒有值,則直接保存;如果已經有存在的值,再通過equals方法比較值是否一樣。
  那么,是不是因為重寫了equals方法,而沒有重寫hashcode方法導致的呢?于是,在DeviceModel類中新增了hashcode方法:
  @Override 
  public int hashCode() { 
      // JDK7新增的Objects工具類 
      return Objects.hash(no); 
  } 
  再次執行,測試方法,發現可以成功去重了。很顯然,大佬的失誤是在重寫equals方法時違背了一條原則:如果一個類的equals方法相等,那么它們的hashcode方法必須相等。由于沒有重寫hashcode方法導致違背這一原則。因此,在隱式使用Map時就出現了莫名其妙的問題。
  后續
  經過這一番周折,問題終于解決。想必大家更也更加明白了為什么重寫equals方法一定要重寫hashcode方法了。后面大佬又考問我一個問題:為什么list.contains方法不會出現這個問題呢?
  因為List的底層結構是數組,不像Map那樣為了提升效率先對Key進行hash處理比較。簡單看一下ArrayList中contains方法的核心實現:
  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時,還是循環調用的equals方法來處理的。
  小結
  通過本篇文章講了一個幫大佬定位問題的故事,感謝大佬給我一個很好的寫作素材,這期間有很多值得學習和借鑒的內容。從側面也證明,有些面試題的確有它的價值,如果你以為只是在造飛機,真有可能是在實踐中沒遇跳到坑里到而已。

TAG: 軟件開發

 

評分:0

我來說兩句

顯示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

日歷

« 2021-05-11  
      1
2345678
9101112131415
16171819202122
23242526272829
3031     

數據統計

  • 訪問量: 21737
  • 日志數: 156
  • 建立時間: 2020-08-11
  • 更新時間: 2021-05-11

RSS訂閱

Open Toolbar
农村里的风流韵事