天天看點

Java填坑之Set

Set

Set不儲存重複的元素(如何判斷元素相同?);如果你試圖将相同對象的多個執行個體添加到Set中,那麼它就阻止這種重複的現象。Set中最常被使用的是測試歸屬性,你可以很容易地詢問某個對象是否在某個Set中。正因如此,查找就成為了Set中最重要的操作,是以你通常都會選擇一個HashSet的實作,它專門對快速查找進行了優化。

總結:無序集合,不允許有重複值,允許有null值,存入與取出的順序有可能不一緻,主要有HashSet和TreeSet兩大實作類。

Java填坑之Set

HashSet

HashSet與數學上的集合概念一模一樣。由一個或多個元素所構成的叫做集合。

HashSet實作Set接口,由哈希表(實際上是一個HashMap執行個體)支援。它不保證set 的疊代順序;特别是它不保證該順序恒久不變,此類允許使用null元素。

在HashSet中,元素都存到HashMap鍵值對的Key上面,而Value時有一個統一的值private static final Object PRESENT = new Object();,(定義一個虛拟的Object對象作為HashMap的value,将此對象定義為static final。)

特點:

  • HashSet通過使用一種稱為哈希的機制來存儲元素。
  • HashSet不能存放重複元素,例如:集合A={1,a},則a不能等于1,也就是如果你把兩個1放進HashSet會自動變為一個1。
  • HashSet允許為空值。
  • HashSet類是非同步的(線程不安全)。
  • HashSet元素是無序的。因為元素是根據其哈希碼插入的,是以HashSet也不得進行排序操作。
  • HashSet友善檢索資料。
  • HashSet的初始預設容量為16,而負載因子為0.75。

HashSet的構造方法

構造方法 描述
HashSet() 用于構造預設的HashSet。
HashSet(int capacity) 用于将HashSet的容量初始化為給定的整數容量。随着将元素添加到HashSet中,容量會自動增長。
HashSet(int capacity, float loadFactor) 用于将HashSet的容量初始化為給定的整數容量和指定的負載因子。
HashSet(Collection<? extends E> c) 用于通過使用集合來初始化HashSet。

HashSet的方法

Java填坑之Set

思考:如何保證存儲元素不一緻?

通過hashCode方法和equals方法來保證元素的唯一性,add()傳回的是boolean類型;判斷兩個元素是否相同,先要判斷元素的hashCode值是否一緻,隻有在該值一緻的情況下,才會判斷equals方法,如果存儲在HashSet中的兩個對象hashCode方法的值相同equals方法傳回的結果是true,那麼HashSet認為這兩個元素是相同元素,隻存儲一個(重複元素無法存入)。

注意:HashSet集合在判斷元素是否相同先判斷hashCode方法,如果相同才會判斷equals。如果不相同,是不會調用equals方法的。

案例代碼:

Person類:

//Person類
public class Person
{
    private String name;
    private int age;
    Person(String name,int age)
    {
        this.name=name;
        this.age=age;
    }
    public int hashCode()
    {
        System.out.println(this.name+"......hashCode");
        return 60;
    }

    public boolean equals(Object obj)
    {

        if(!(obj instanceof Person))
            return false;
        Person p=(Person)obj;
        System.out.println(this.name+"....equals...."+p.name);
        return this.name.equals(p.name) && this.age == p.age;
    }
    public String getName()
    {
        return name;
    }
    public int getAge()
    {
        return age;
    }
}

           

主函數:

import java.util.HashSet;
import java.util.Iterator;

public class HashSetDemo {

    public static void sop(Object obj)
    {
        System.out.println(obj);
    }
    public static void main(String[] args) {
        HashSet hs=new HashSet<Object>();

        hs.add(new Person("a1",11));
        hs.add(new Person("a2",12));
        hs.add(new Person("a2",12));
        hs.add(new Person("a3",13));

        Iterator it=hs.iterator();
        while(it.hasNext()){
            Person p=(Person)it.next();
            sop(p.getName()+"::"+p.getAge());
        }
    }

}
           

運作結果:

a1......hashCode
a2......hashCode
a2....equals....a1
a2......hashCode
a2....equals....a1
a2....equals....a2
a3......hashCode
a3....equals....a1
a3....equals....a2
a1::11
a2::12
a3::13

           

由此可見我們程式運作順序是:設定a1的hashcode -> 裝入set -> 設定a2的hashcode -> 與a1對比一下(不同)->裝入set -> 設定a2的hashcode -> 與a1對比一下(不同)-> 與a2對比一下(相同)-> 不裝入set -> 設定a3的hashcode -> 與a1對比一下(不同)-> 與a2對比一下(不同)-> 裝入set

為什麼要單獨設定HashCode呢?

因為如果不給每個Person對象設定唯一的Hashcode的話,第一個new Person(“a2”,12)的hashcode與第二個new Person(“a2”,12)的hashcode必然是不一樣的,然後編譯器就會認為它們是不一樣的對象,但是在我們實際場景中,因為它們的屬性是一樣的,是以這裡我們認為這兩個是一樣的對象,是以我們為了讓編譯器知道我們的判斷标準是根據屬性判斷的,而不要讓他在第一步hashcode就判斷完畢了,而不進行到第二步equals方法中,是以我們必須改寫hashcode方法與equals方法。

補充一個常見的面試題:重寫equals方法為什麼要重寫hashcode方法?

貼一篇清晰明了:

https://blog.csdn.net/We_chuan/article/details/96426273

LinkedHashSet

這篇介紹看到一篇博文很受啟發這裡也貼一下

‘https://yq.aliyun.com/articles/635156

我們先來看一下LinkedHashSet的源碼

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2851667679971038690L;
    
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }

    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }

    /**
     * Constructs a new, empty linked hash set with the default initial
     * capacity (16) and load factor (0.75).
     */
    public LinkedHashSet() {
        super(16, .75f, true);
    }
    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);
    }
}

           

發現它的源碼也太精簡了吧,然後每個構造器裡面都用了super()調用父類的構造函數,LinkedHashSet是繼承HashSet的,LinkedHashSet繼承了HashSet的全部特性,元素不重複,快速查找,快速插入,并且新增了一個重要特性,那就是有序,可以保持元素的插入順序,是以可以應用在對元素順序有要求的場景中。

然後我們點進去這個super()看一看調用的是哪個構造函數:

Java填坑之Set

這個父類也就是HashSet的構造器中傳回的是一個LinkedHashMap,然後這個方式是預設也就是default修飾符封裝的,也就是說,它不能給HashSet直接調用:

Java填坑之Set

它隻能給子類調用或者重寫,這裡就是給LinkedHashSet調用了,因為LinkedHashSet跟HashSet在同一個包 java.util下;我們來看一下它的使用場景:

咖啡類:

package Collection;

public class Coffee {

    private String name;
    private String price;

    public Coffee(String name, String price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
    }
}

           

主函數:

package Collection;

import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

public class LinkedHashSetDemo {

    public static void main(String[] args) {
		//我們想保證插入元素不重複的同時確定他的插入順序
		//此時就用LinkedHashSet()實作類
        Set<Coffee> linkedHashSet =  new LinkedHashSet();
        linkedHashSet.add(new Coffee("natie", "15"));
        linkedHashSet.add(new Coffee("natie", "15"));
        linkedHashSet.add(new Coffee("moka", "12"));
        linkedHashSet.add(new Coffee("bigmoka", "18"));


       Iterator<Coffee>  it = linkedHashSet.iterator();
        while (it.hasNext()) {
            Coffee coffee = it.next();
            System.out.println(coffee.getName());
            System.out.println(coffee.getPrice());
        }

    }
}

           

這裡我們看一下輸出結果:

Java填坑之Set

天啊翻水水,順序倒是沒錯,但是怎麼兩杯15塊的拿鐵都放進去了Set中了呢,說好了Set中是不帶有重複元素的呢?我們的LinkedHashSet是繼承于HashSet的,而add()方法是Set接口定義的,也就是說,我們這裡用的add()方法也是調用了LinkedHashSet的父類,也就是HashSet()中實作的add(),那就回到我們上面講到的問題了,我們在Set是怎麼幫我們自動去重的呢?就是第一步通過檢驗hashcode再走equals,那我們得先給他配置好,才能正确的去重呢!這時候我們需要重寫hashcode與equals:

修改後的Coffee類:

package Collection;

import java.util.Objects;

public class Coffee {

    private String name;
    private String price;

    public Coffee(String name, String price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
    }

    @Override
    public int hashCode() {
        return 60;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj){ return true;}

        if(!(obj instanceof Coffee)){ return false;}
        //if (obj == null || getClass() != obj.getClass()) return false;
        Coffee coffee = (Coffee) obj;

        return this.name.equals(coffee.name) && this.price.equals(coffee.price);
    }
}

           

運作結果:

Java填坑之Set

補充一點供自己記憶:

情況一:隻修改hashcode不修改equals方法:

放入set的時候第一層驗證hashcode的時候相同,但是到了第二步如果不改寫equals的話,對比的是記憶體位址,都是不一樣的,是以會存入相同對象。

情況二:隻修改equals不修改hashcode:

我們重寫了equals讓它通過比較類的成員變量是否一緻,如果一緻則傳回true,不一緻為false,Demo demo1 = new Demo(“11”,“11”), Demo demo2 = new Demo(“11”,“11”); 這裡demo1.equals(demo2)應該是傳回true,然後我們把他放入hashmap中,因為demo1跟demo2的hashcode方法沒有改寫,是以都可以存入,用equals比較說明對象相同,但是在HashMap中卻以不同的對象存儲(沒有重寫hascode值,兩個hascode值,在他看來就是兩個對象)到底這兩個對象相等不相等????說明必須重寫hashCode()的重要性。

TreeSet

Java填坑之Set

TreeSet實作了NavigableSet接口,NavigableSet接口繼承了SortSet接口,是以推測TreeSet是具有排序功能的Set,我們直接上執行個體:

package Collection;

import java.util.TreeSet;

public class TreeSetDemo {

    public static void main(String[] args) {
        
        TreeSet<Coffee> treeSet = new TreeSet<Coffee>();
        TreeSet<String> treeString = new TreeSet<String>();
        treeString.add("Benz");
        treeString.add("BWM");
        treeString.add("Audi");
        treeString.add("Mini");
        System.out.println(treeString);
        
    }
}

           

運作結果:

Java填坑之Set

發現他給我們按字母表的順序排好序了诶?那能不能給我們自己定義的類進行排序呢?我們也試下剛剛的咖啡類:

Java填坑之Set

報錯了,說是我們定義的咖啡類不能轉換為Comparable類;那為什麼我們剛剛的String類是可以的呢?請看:

Java填坑之Set

原來String類已經偷偷繼承了Comparable接口了呀,其實這裡我們有兩種方法:

  • 實作Comparable接口
  • 傳入一個外部比較器
    Java填坑之Set
    我們先來模仿一下String這種繼承接口的方法:
package Collection;


public class Coffee implements Comparable{

    private String name;
    private String price;

    public Coffee(String name, String price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
    }

    @Override
    public int hashCode() {
        return name.hashCode()+price.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj){ return true;}

        if(!(obj instanceof Coffee)){ return false;}
        //if (obj == null || getClass() != obj.getClass()) return false;
        Coffee coffee = (Coffee) obj;

        return this.name.equals(coffee.name) && this.price.equals(coffee.price);
    }

    @Override
    public String toString() {
        return "Coffee{" +
                "name='" + name + '\'' +
                ", price='" + price + '\'' +
                '}';
    }

    public int compareTo(Object o) {
        if(!(o instanceof Coffee)){ return -1;}
        Coffee coffee = (Coffee) o;
        return this.price.compareTo(coffee.getPrice());

    }
}


           

主函數:

package Collection;

import java.util.TreeSet;

public class TreeSetDemo {

    public static void main(String[] args) {

        TreeSet<String> treeString = new TreeSet<String>();
        treeString.add("Benz");
        treeString.add("BWM");
        treeString.add("Audi");
        treeString.add("Mini");
        System.out.println(treeString);

        TreeSet<Coffee> treeSet = new TreeSet<Coffee>();
        treeSet.add(new Coffee("natie", "11"));
        treeSet.add(new Coffee("natie", "11"));
        treeSet.add(new Coffee("black", "8"));
        treeSet.add(new Coffee("arbica", "15"));
        treeSet.add(new Coffee("arbica11", "101"));
//        for (Coffee coffee : treeSet) {
//            System.out.println(coffee.getName() + ":" + coffee.getPrice());
//        }
        System.out.println(treeSet);
    }
}

           

輸出結果:

Java填坑之Set

天啊它怎麼沒按照價格排???是為什麼呢?因為我們這裡設定的price是String類型,調用的是String實作的compareTo,是按照數字排列順序排序的哦,我們來改為數值類型試下;

Java填坑之Set

我們隻需要稍微做一下類型轉換,系統就會識别到并調用的是Integer實作的compareTo方法啦!

輸出結果:

Java填坑之Set

這裡描述的是第一種,繼承Comparable接口,接下來我們看看第二種:

Java填坑之Set

我們看到TreeSet源碼裡面有這麼一個構造器,參數是一個Comparator類,那我們就想到了,我們可以用一個匿名内部類直接寫這個Comparator是怎麼實作的,給他重新定義了是怎麼比較的,我們把Coffee改回來:

Coffee(沒有實作Comparable接口)

package Collection;


public class Coffee {

    private String name;
    private String price;

    public Coffee(String name, String price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
    }

    @Override
    public int hashCode() {
        return name.hashCode()+price.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj){ return true;}

        if(!(obj instanceof Coffee)){ return false;}
        //if (obj == null || getClass() != obj.getClass()) return false;
        Coffee coffee = (Coffee) obj;

        return this.name.equals(coffee.name) && this.price.equals(coffee.price);
    }

    @Override
    public String toString() {
        return "Coffee{" +
                "name='" + name + '\'' +
                ", price='" + price + '\'' +
                '}';
    }
    
}

           

主函數:

package Collection;

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSetDemo {

    public static void main(String[] args) {

        TreeSet<String> treeString = new TreeSet<String>();
        treeString.add("Benz");
        treeString.add("BWM");
        treeString.add("Audi");
        treeString.add("Mini");
        System.out.println(treeString);

        TreeSet<Coffee> treeSet = new TreeSet<Coffee>(new Comparator<Coffee>() {
            public int compare(Coffee o1, Coffee o2) {
                return Integer.valueOf(o1.getPrice()).compareTo(Integer.valueOf(o2.getPrice()));
            }
        });
        
        treeSet.add(new Coffee("natie", "11"));
        treeSet.add(new Coffee("natie", "11"));
        treeSet.add(new Coffee("black", "8"));
        treeSet.add(new Coffee("arbica", "15"));
        treeSet.add(new Coffee("arbica11", "101"));
//        for (Coffee coffee : treeSet) {
//            System.out.println(coffee.getName() + ":" + coffee.getPrice());
//        }
        System.out.println(treeSet);
    }
}

           

比較的代碼邏輯是跟上面一樣的,我們寫好我們想要排序的根據是什麼,這裡就是想要通過price排序,然後寫好之後編譯器就會幫我們搞定啦,看一看結果:

Java填坑之Set

沒毛病~