【Java入門提高篇】Day29 Java容器類詳解(十一)LinkedHashSet詳解


  當當當當當當當,本來打算出去浪來着,想想還是把這個先一起寫完吧,畢竟這篇的主角跟我一樣是一個超級偷懶的角色——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}

  好的,現在已經達到我們想要的效果了。任務完成,午飯加個蛋。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM