1. 程式人生 > >【JDK1.8】JDK1.8集合原始碼閱讀——Set彙總

【JDK1.8】JDK1.8集合原始碼閱讀——Set彙總

一、前言

這一篇裡,我將對HashSet、LinkedHashSet、TreeSet進行彙總分析,並不打算一一進行詳細介紹,因為JDK對Set的實現進行了取巧。我們都知道Set不允許出現相同的物件,而Map也同樣不允許有兩個相同的Key(出現相同的時候,就執行更新操作)。所以Set裡的實現實際上是呼叫了對應的Map,將Set的存放的物件作為Map的Key。

二、原始碼分析

這裡筆者就以最常用的HashSet為例進行分析,其餘的TreeSet、LinkedHashSet類似,就不贅述了。

2.1 結構概覽

HashSetStruct

關係也很簡單,實現了Set的介面,繼承了AbstractSet抽象類,抽象類裡面定義了集合的常見操作,與我們之前分析過的ArrayList之類的相似。

2.2 成員變數

// HashMap就是HashSet裡實現具體操作的物件
private transient HashMap<E,Object> map;
// 將物件作為Value存進去
private static final Object PRESENT = new Object();

由於使用Map進行操作,把E作為Key,要定義一個PRESENT物件作為Value,每個存入的物件都使用它來作為Value。

2.3 構造方法

2.3.1 HashSet()

public HashSet() {
  map = new HashMap<>();
}

看了這個,相信園友們應該就知道它是怎麼實現的了,我們平時構建HashSet的時候,其實它是在裡面new了一個HashMap。

2.3.2 HashSet(Collection<? extends E> c)

public HashSet(Collection<? extends E> c) {
  map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
  addAll(c);
}

呼叫傳集合的構造方法則是使用了HashMap裡指定初始化容量的構造方法,然後再呼叫addAll()。

public boolean addAll(Collection<? extends E> c) {
  boolean modified = false;
  for (E e : c)
    if (add(e))
      modified = true;
  return modified;
}

addAll方法很簡單,其實就是遍歷集合c,然後呼叫add方法,實現插入HashMap。

public boolean add(E e) {
  return map.put(e, PRESENT)==null;
}

add方法則是呼叫了map的put()方法,將物件作為Key,之前域裡的PRESENT作為Value,插入到HashMap中。

2.3.3 HashSet(int initialCapacity, float loadFactor, boolean dummy)

最後值得一提的是這個構造方法:

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
  map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

注意它是包訪問許可權的,而不是public,因為這個構造方法是提供給LinkedHashSet使用的,所以裡面初始化的也是LinkedHashMap,有興趣的園友們也可以去LinkedHashSet裡看一下它的構造方法。

三、Set的使用注意事項

筆者前端時間恰好碰到了因為HashSet的底層事項導致的一個bug,在此跟大家分享一下:

/**
 * @author joemsu 2018-02-04 上午10:33
 */
public class UserInfo {
  private Long id;
  private String name;
  private Integer age;

  public UserInfo(Long id, String name, Integer age) {
    this.id = id;
    this.name = name;
    this.age = age;
  }

  public void setName(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return "UserInfo{" +
      "id=" + id +
      ", name='" + name + '\'' +
      ", age=" + age +
      '}';
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    UserInfo info = (UserInfo) o;
    return Objects.equals(id, info.id) &&
      Objects.equals(name, info.name) &&
      Objects.equals(age, info.age);
  }

  @Override
  public int hashCode() {
    return Objects.hash(id, name, age);
  }
  
}

UserInfo為當時筆者要處理的一個pojo物件,由第三方提供,重寫了equals和hashCode方法(當時沒有發現)。而筆者當時要獲取所有的UserInfo物件,放入集合當中進行復雜的邏輯處理,出於可能出現重複物件的考慮(使用遞迴遍歷不同部門下的人員資訊,可能存在一個人在多個部門),選擇使用HashSet。然後在遍歷HashSet集合a的時候,會將每個遍歷到的物件加入另一個集合b作記錄,事後會將a,b做一個差集,取出其中沒有訪問到的元素。這個過程中可能會涉及到更新某個物件,具體過程簡化如下:

public static void main(String[] args) {
  // 將物件加入set
  UserInfo info1 = new UserInfo(1L, "zhangsan", 22);
  UserInfo info2 = new UserInfo(2L, "lisi", 23);
  UserInfo info3 = new UserInfo(3L, "wangwu", 24);
  Set<UserInfo> userInfoSet = new HashSet<>();
  userInfoSet.add(info1);
  userInfoSet.add(info2);
  userInfoSet.add(info3);
  
  // 對訪問到的元素加入集合
  List<UserInfo> visited = new ArrayList<>();
  visited.add(info1);
  visited.add(info2);
  visited.add(info3);
  // 假設對其中一個物件進行修改
  info1.setName("liliu");
  // 去掉訪問過的元素
  userInfoSet.removeAll(visited);
  userInfoSet.stream().forEach(System.out::println);
}

最後的輸出結果:

UserInfo{id=1, name='liliu', age=22}

是的,所有修改過的元素都無法移除。當筆者通過debug發現這一現象後立刻就意識到,可能就是hashCode導致被修改過的元素無法訪問到,為什麼呢,我們可以回顧一下HashMap的remove操作:

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
  Node<K,V>[] tab; Node<K,V> p; int n, index;
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (p = tab[index = (n - 1) & hash]) != null) {
    // 省略
  }
  return null;
}

在HashMap中,是通過Key的hash值來定位桶的位置,當筆者修改了物件的name屬性之後,由於重寫的hashCode方法裡包括了name這一欄位,所以,hash值也發生了改變,導致在對應的桶裡找不到該物件,也就無法實現remove操作(雖然兩個是同一個引用)。

四、總結

Set的各種底層實現都是對應的Map,熟悉了Map裡的各種方法,相信對於Set的瞭解也會更加深入。最後謝謝各位園友觀看,如果有描述不對的地方歡迎指正,與大家共同進步!

相關推薦

JDK1.8JDK1.8集合原始碼閱讀——Set彙總

一、前言 這一篇裡,我將對HashSet、LinkedHashSet、TreeSet進行彙總分析,並不打算一一進行詳細介紹,因為JDK對Set的實現進行了取巧。我們都知道Set不允許出現相同的物件,而Map也同樣不允許有兩個相同的Key(出現相同的時候,就執行更新操作)。所以Set裡的實現實際上是呼叫了對應的

JDK1.8JDK1.8集合原始碼閱讀——TreeMap(一)

一、前言 在前面兩篇隨筆中,我們提到過,當HashMap的桶過大的時候,會自動將連結串列轉化成紅黑樹結構,當時一筆帶過,因為我們將留在本章中,針對TreeMap進行詳細的瞭解。 二、TreeMap的繼承關係 下面先讓我們來看一下Tre

JDK1.8JDK1.8集合源碼閱讀——Set匯總

都是 arr initial 復雜 定義 bst als ati bool 一、前言 這一篇裏,我將對HashSet、LinkedHashSet、TreeSet進行匯總分析,並不打算一一進行詳細介紹,因為JDK對Set的實現進行了取巧。我們都知道Set不允許出現相同的對象,

jdk1.8 J.U.C併發原始碼閱讀------AQS之conditionObject內部類分析

一、繼承關係 public class ConditionObject implements Condition, java.io.Serializable 實現了Condition介面和Serializable介面,是AbstractQueuedSynchronize

electron教程8如何壓縮electron原始碼使其不可見

先說背景,使用electron開發過的同學都知道,採用electron-packager打包後的程式,原始碼是暴露在 outputpath/resources/app目錄下的,對於剛才c++轉過來的同學,可能非常不適應。 再說結論,本文所述方法只能講原始碼壓縮,無法像dll

Debian 8.8Java 8 安裝以及環境變量配置

ubuntu lan pri 教程 target /usr tle 步驟 tor 事實上可以分為簡單的三個步驟: 下載 JDK 壓縮包 解壓壓縮包 配置環境變量 需要註意的是: 所有命令默認在 root 權限下進行! 演示環境是 Debian 8.8

python練習題程序8

AS display span 乘法 range spa 習題 題目 輸出 #題目:輸出 9*9 乘法口訣表。 for i in range(1,10): k = ‘‘ for j in range(1,i+1): k += ‘%s *

精選文章YUM 8分鐘部署LAMP架構

list httpd配置 php.ini 分享圖片 服務器軟件 yum ini install create 什麽是LAMP? LAMP指的Linux(操作系統)、ApacheHTTP 服務器,MySQL(有時也指MariaDB,數據庫軟件) 和PHP(有時也是指Perl

JDK8HashMap集合 原始碼閱讀

    JDK8的HashMap資料結構上覆雜了很多,因此讀取效率得以大大提升,關於原始碼中紅黑樹的增刪改查,博主沒有細讀,會在下一篇博文中使用Java實現紅黑樹的增刪改查。     下面是類的結構圖:      程

JDKArrayList集合 原始碼閱讀

       這是博主第二次讀ArrayList 原始碼,第一次是在很久之前了,當時讀起來有些費勁,記得那時候HashMap的原始碼還是雜湊表+連結串列的資料結構。        時隔多年,再次閱讀起來ArrayLi

比賽報告2018.8.7集訓 NOIP練習賽卷十

A.函式 題目連結 問題描述 對於一個整數,定義 f(x)為他的每個數位的階乘的乘積。例如 f(135)=1! * 3! * 5! = 720。給出一個數 a(可以包含字首零), a 滿足他的至少一個數位大於 1。我們要求出最 大 的整數 x,其中 x 不含 0

spring系列8:屬性賦值

一:使用@Value賦值     基本數值     可以寫SpEL; #{}     可以寫${};取出配置檔案【properties】中的值(在執行環境變數裡面的值) demo:set,get省略 @Value("張三") private String na

數字影象C++8位和24位BMP點陣圖的平滑、銳化、二值化處理,以及24位真彩圖的灰度化

BMP標頭檔案: #ifndef BMP_H//前處理器 #define BMP_H typedef unsigned char BYTE; typedef unsigned short WORD; typedef unsigned int DWORD; typedef

資料結構考研 8種排序演算法視覺化解讀

排序(Sorting) 排序(Sorting)是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整為“有序”的記錄序列。分內部排序和外部排序,若整個排序過程不需要訪問外存便能完成,則稱此類排序問題為內部排序。反之,若參加排序的記錄數量很大,整個序列的排序過程不

FPGAVerilog基礎模組3-8譯碼器

使用移位實現:module decoder(out ,in); output [7:0 ] out ; input [2:0] in; assign out = 1'b1 << in; endmodule 使用case實現:module decod

9.0對於java集合的叠代器的底層分析

trac print post turn pan 很難 分享 對象 nal 前言:如果對java的集合的遍歷(主要是HashMap中的keySet() 和 entrySet()是如何取值並且可以實現遍歷的)不是很明白的話,有興趣深入了解的小夥伴,本文可以作為一個參考,由於時

Java7、8中HashMap和ConcurrentHashMap原始碼閱讀

首先來看下HashMap的類繼承結構: public class HashMap extends AbstractMap<K,V> impement Map<K,V>,Coloneable,Serializable{ } 可以看出HashMap實現了Map介面。其裡面的方法都是

區塊鏈比特幣原始碼學習

比特幣原始碼學習 - 1 - 交易 參考部落格:here and here 一、交易概念 1、 交易形式 比特幣交易中的基礎構建單元是交易輸出。在比特幣的世界裡既沒有賬戶,也沒有餘額,只有分散到區塊鏈裡的UTXO[未花費的交易輸出]。 例如,你有20比特幣

區塊鏈比特幣原始碼

比特幣原始碼 - 2 - 金鑰和地址 一、基本概念 這裡摘抄一下《精通比特幣》裡面的描述: 比特幣的所有權是通過數字金鑰、比特幣地址和數字簽名來確立的。數字金鑰實際上並不是儲存在網路中,而是由使用者生成並存儲在一個檔案或簡單的資料庫中,稱為錢包。 每筆比特幣交

獨家分享資源大集合!週末大放送!Ja粉超級福利!

本週Ja強為Ja粉們帶來了兩波架構師資源 【高階架構師四十八個階段高階課】 【Java高階網際網路架構師課程】 廣大Ja粉們反應火熱 掀起了下載狂潮 。  。  。 Ja強看到如此景象,深感欣慰 不枉小Ja幾經周折地獲取到資源 值得一提的