當當當當當當當,本來打算出去浪來着,想想還是把這個先一起寫完吧,畢竟這篇的主角跟我一樣是一個超級偷懶的角色——LinkedHashSet,有多偷懶?看完你就知道了。
本篇將從以下幾個方面對LinkedHashSet進行介紹:
1、LinkedHashSet中的特性
2、LinkedHashSet源碼分析
3、LinkedHashSet應用場景
本篇預計需要食用10分鍾,快的話五分鍾也夠了,完全取決於各位看官心情。
LinkedHashSet中的特性
前面已經介紹過了HashSet,本篇要介紹的LinkedHashSet正是它的兒子,作為HashSet的唯一法定繼承人,可以說是繼承了HashSet的全部優點——懶,並且將其發揮到了極致,這一點在之后的源碼分析里可以看到。
LinkedHashSet繼承了HashSet的全部特性,元素不重復,快速查找,快速插入,並且新增了一個重要特性,那就是有序,可以保持元素的插入順序,所以可以應用在對元素順序有要求的場景中。
先來看一個小栗子:
public class LinkedHashSetTest { public static void main(String[] args){ LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(); HashSet<String> hashSet = new HashSet<>(); for (int i = 0; i < 10; i++) { linkedHashSet.add("I" + i); hashSet.add("I" + i); } System.out.println("linkedHashSet遍歷:"); for (String string : linkedHashSet){ System.out.print(string + " "); } System.out.println(); System.out.println("hashSet遍歷:"); for (String string : hashSet){ System.out.print(string + " "); } } }
linkedHashSet遍歷:
I0 I1 I2 I3 I4 I5 I6 I7 I8 I9
hashSet遍歷:
I9 I0 I1 I2 I3 I4 I5 I6 I7 I8
可以看到,在HashSet中存儲的元素遍歷是無序的,而在LinkedHashSet中存儲的元素遍歷是有序的。嗯,它和HashSet就這唯一的區別了。
LinkedHashSet源碼分析
那么問題來了,LinkedHashSet中的元素為什么會是有序的呢?難道也跟LinkedHashMap一樣用了鏈表把元素都拴起來了?別着急,讓我們一起來看看源碼。
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { private static final long serialVersionUID = -2851667679971038690L; /** * 使用指定初始容量和裝載因子構造一個空的LinkedHashSet實例 */ public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } /** * 使用指定的初始容量和默認的裝載因子構造一個空的LinkedHashSet實例 */ public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } /** * 使用默認的初始容量和默認的裝載因子構造一個空的LinkedHashSet實例 */ public LinkedHashSet() { super(16, .75f, true); } /** * 構造一個與指定集合有相同元素的空LinkedHashSet實例,使用默認的裝載因子和能夠容納下指定集合所有元素的合適的容量。 */ public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); } /** * 可分割式迭代器 */ @Override public Spliterator<E> spliterator() { return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); } }
你沒看錯,這應該是所有容器類中最短小精悍的了,這也就是開頭為什么說這家伙懶到家的原因了。
可是,LinkedHashSet中並沒有覆蓋add方法,只是加了幾個構造函數和一個迭代器,其他全部和HashSet一毛一樣,為什么它就能有序呢??
玄機就藏在這個構造函數中,這幾個構造函數其實都是調用了它父類(HashSet)的一個構造函數:
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
嗯,這個構造函數跟其他構造函數唯一的區別就在於,它創建的是一個LinkedHashMap對象,所以元素之所以有序,完全是LinkedHashMap的功勞。該構造函數是默認訪問權限的,所以在HashSet中是不能直接調用的,留給子類去調用或覆蓋(講道理使用protected權限不是更合理嗎)。
LinkedHashSet應用場景
現在假設這樣的場景,現在我有一堆商品,商品有名稱和價格,但是里面有重復商品,我希望把重復的商品(名稱和價格都一樣的)過濾掉,只保留一個,並且希望輸出后的順序跟原來的順序一致。嗯,這時候LinkedHashSet就派上用場了。(廢話,那是你特意給主角加的戲)
商品的結構是這樣的:
public class Commodity { private String name; private Double price; public Commodity(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return "Commodity{" + "name='" + name + '\'' + ", price=" + price + '}'; } }
來用LinkedHashSet解決一下這個需求:
public class CommodityTest { public static void main(String[] args){ //有7個商品,A和E、D和G信息完全一樣,希望能過濾掉,只保留一個,C和F雖然名稱一樣,但是價格不同,希望保留 Commodity commodityA = new Commodity("Iphone6S", 6666.66); Commodity commodityB = new Commodity("Iphone7", 7777.77); Commodity commodityC = new Commodity("Iphone8", 8888.88); Commodity commodityD = new Commodity("IphoneX", 9999.99); Commodity commodityE = new Commodity("Iphone6S", 6666.66); Commodity commodityF = new Commodity("Iphone8", 6666.66); Commodity commodityG = new Commodity("IphoneX", 9999.99); LinkedHashSet<Commodity> commodities = new LinkedHashSet<>(); commodities.add(commodityA); commodities.add(commodityB); commodities.add(commodityC); commodities.add(commodityD); commodities.add(commodityE); commodities.add(commodityF); commodities.add(commodityG); for (Commodity commodity : commodities){ System.out.println(commodity); } } }
輸出如下:
Commodity{name='Iphone6S', price=6666.66} Commodity{name='Iphone7', price=7777.77} Commodity{name='Iphone8', price=8888.88} Commodity{name='IphoneX', price=9999.99} Commodity{name='Iphone6S', price=6666.66} Commodity{name='Iphone8', price=6666.66} Commodity{name='IphoneX', price=9999.99}
翻...翻...翻車了?雖然輸出的順序與插入的順序是一致的最后一個IphoneX和Iphone6S並沒有被去掉,怎么回事呢?說好的可以去重呢?
嗯,別慌,我既然可以讓車翻過來,那就有辦法讓它再翻回去。
想要利用LinkedHashSet自動去重性質,那么我們就要先理解它是怎樣去重的,其實和HashSet是一樣的,往里面添加元素的時候,其實是這樣的:
public boolean add(E e) { return map.put(e, PRESENT)==null; }
所以當該元素在map中存在的時候,map.put方法就會返回舊值,此時add方法會返回false,在查找map中put元素的時候,會先調用hashCode方法得到該元素的hashCode值,然后查看table中是否存在該hashCode值,如果存在則調用equals方法重新確定是否存在該元素,如果存在,則更新value值,否則將新的元素添加到HashMap中,如果沒有覆蓋過hashcode方法,那么就會使用對象默認的hashcode,這個值跟對象成員變量的具體值就沒有直接關聯了,所以我們需要覆蓋hashcode方法和equals方法。
public class Commodity { private String name; private Double price; public Commodity(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; } @Override public String toString() { return "Commodity{" + "name='" + name + '\'' + ", price=" + price + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Commodity commodity = (Commodity) o; return Objects.equals(name, commodity.name) && Objects.equals(price, commodity.price); } @Override public int hashCode() { return Objects.hash(name, price); } }
這里用的equals方法和hashCode方法是很通用的,在其他地方也可以使用類似的寫法,現在再來重新跑一下程序看下:
Commodity{name='Iphone6S', price=6666.66} Commodity{name='Iphone7', price=7777.77} Commodity{name='Iphone8', price=8888.88} Commodity{name='IphoneX', price=9999.99} Commodity{name='Iphone8', price=6666.66}
好的,現在已經達到我們想要的效果了。任務完成,午飯加個蛋。