Java集合(四)、繼承自Collection接口的Set接口


一、Set接口的特點

  一個不包含重復元素的collection。更確切地講,Set不包含滿足e1.equals(e2)的元素對 e1和e2,並且最多包含一個null元素。

  Set集合由Set接口和Set接口的實現類組成,Set接口繼承了Collection接口,因此包含了Collection接口的所有方法。其主要實現類有HashSet和TreeSet,在HashSet的基礎上又延伸出了LinkedHashSet。

 

HashSet和TreeSet的不同就在於如何判斷兩個數是否相同的方法上。

  1.HashSet判斷兩個對象是否相同的方法時繼承自Object類的equals方法

    (public boolean equals(Object o)方法只可以比較是否相等,相等返回true,反之返回false)。

  2.TreeSet判斷兩個對象是否相同的方法則是Comparable接口中的compareTo()方法

    (public void int compareTo(Object o)方法不僅可以比較是否相等,還可以比較大小,如果相等返回0,調用者大於參數則返回正數,否則返回負數)

所以可以得到添加到TreeSet中的對象必須實現Comparable接口。同時如果使用HashSet則最好重寫equals()方法。

 

二、HashSet

HashSet實現了Set接口,基於HashMap進行存儲。遍歷時不保證順序,並不保證下次遍歷的順序和之前一樣。HashSet中允許null值。

進入到HashSet源碼中我們發現,所有數據存儲在

private transient HashMap<E, Object> map;
private static final Object PRESENT = new Object();

意思就是HashSet的集合其實就是HashMap的key的集合,然后HashMap的val默認都是PRESENT。HashMap的定義即是key不重復的集合。使用HashMap實現,這樣HashSet就不需要再實現一遍。

所以所有的add,remove等操作其實都是HashMap的add、remove操作。遍歷操作其實就是HashMap的keySet的遍歷,舉例如下

...
public Iterator<E> iterator(){
    return map.keySet().iterator();
}

public boolean contains(Object o) {
    return map.containsKey(o);
}

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

public void clear() {
    map.clear();
}
...

1.HashSet底層實際上是一個HashMap,HashMap底層采用了哈希表數據結構

2.哈希表又叫散列表,哈希表底層是一個數組,這個數組中每一個元素是一個單向鏈表,每個單向鏈表都有一個獨一無二的hash值,代表數組的下表。在某個單向鏈表中的每個節點上的hash值是相同的。hash值實際上是key調用hashCode方法,再通過“hash function”轉換成的值。

3.如何向哈希表中添加元素?

  先調用存儲的key的hashCode方法,經過 某個算法得到hash值,如果這個哈希表中不存在這個hash值,則直接加入元素。如果該hash值已經存在,繼續調用key之間的equals方法,如果equals方法返回false,則將該元素添加。如果equals方法返回true,則放棄添加該元素 ,即元素重復。

  HashMap和HashSet的初始化容量是16,默認加載因子是0.75。

代碼舉例

public class Student{
    private Integer id;

    private String name;

    public Student(Integer id, String name){
        this.id = id;
        this.name = name;
    }

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

測試類

import java.util.*;

public class test{
    public static void main(String[] args) {
        Set<Student> set = new HashSet<>();
        Student stu1 = new Student(1001, "leslie");
        Student stu2 = new Student(1001, "leslie");
        set.add(stu1);
        set.add(stu2);

        set.forEach(stu -> System.out.println(stu));
    }
}

運行程序,得到如下結果

看到這個結果應該很驚訝,我們期望的是Set中存放的數據不可重復,而代碼里的兩個Student對象,學號與姓名都相同,插入到HashSet中后應該只保留一個對象。

 這是因為Set集合中所說的不允許重復,這個重復是指對象的重復。何為同一個對象?即在內存中的編號是一致的。內存中的編號是什么?就是哈希碼(哈希碼一般是 類名 和 對象所在內存地址的十六進制數字表示 的組合)。

 

改造我們定義的Student類,為Student類增加hashCode()與equals()方法,即重寫這兩個方法

public class Student{
    private Integer id;

    private String name;

    public Student(Integer id, String name){
        this.id = id;
        this.name = name;
    }

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

    @Override
    public boolean equals(Object o){
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        Student stu = (Student) o;
        return id.equals(stu.id);
    }

    @Override
    public int hashCode(){
        return id.hashCode();
    }
}

再次運行測試程序,結果如下:

此時,HashSet中只有一個對象。

 

另外,HashSet完全繼承了Set或者Collection里的方法實現add、addAll、clear、isEmpty、size、contains、iterator、remove等

簡單示例:

import java.util.*;

public class test{
    public static void main(String[] args) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < 10; i++){
            hashSet.add((int)(Math.random()*100));
        }
        System.out.println(hashSet.size());
        //打印
        Iterator<Integer> it = hashSet.iterator();
        while(it.hasNext()){
            System.out.println(it.next() + " ");
        }
        //判斷是否包含重復元素
        System.out.println(hashSet.contains(8));
    }
}

輸出

 

 

三、TreeSet

TreeSet類型是J2SE中唯一可實現自動排序的類型

        TreeSet是SortedSet接口的唯一實現類,TreeSet可以確保集合元素處於排序狀態。TreeSet支持兩種排序方式,自然排序 和定制排序,其中自然排序為默認的排序方式。向  TreeSet中加入的應該是同一個類的對象。

        TreeSet判斷兩個對象不相等的方式是兩個對象通過equals方法返回false,或者通過CompareTo方法比較沒有返回0

TreeSet集合:可以對Set集合中的元素進行排序。是不同步的。

 

但是TreeSet集合的存儲是有序的,即:存儲到集合中的元素是按自然順序存儲的。

判斷元素唯一性的方式:

  根據比較方法的返回值來判斷。是0(零)就存入集合,不是0就不存。因為Set集合是不能有重復的元素,無序。

 

TreeSet要注意的事項:

1.往TreeSet里面添加元素時候,如果元素本具備自然順序特性,那么就按照元素的自然順序排序存儲.

2.往TreSet里面添加元素時候,如果元素不具備自然順序特性,那么該元素就必須要實現Comparable接口,把元素的比較規則定義在compareTo(T o)方法中

3.如果比較元素的時候,compareTo返回的是0,那么該元素被視為重復元素,不允許添加 (注意:TreeSet與HashCode,equals沒有任何關系)

4.往TreeSet里面添加元素時候,如果元素本身不具備自然自然順序特性,而且元素所屬類也沒有實現Comparable接口,那么我們必須要在創建TreeSet的時候傳入一個比較器.

 

自定義比較器

 自定一個比較器只需要實現接口 Comparator<T>即可,把元素與元素之間的比較規則定義在compare方法內即可

自定義比較器的格式:

class 類名 implements Comparator<T>{
    
}

 

Java中的TreeSet是Set的一個子類,TreeSet集合是用來對象元素進行排序的,同樣他也可以保證元素的唯一。TreeSet如何保證元素唯一呢,他是怎么樣進行排序的呢?

首先來看一段代碼

public class Person{
    private Integer age;

    private String name;

    public Person(Integer age, String name){
        this.age = age;
        this.name = name;
    }
}

import java.util.*;

public class test{
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>();
        ts.add(new Person(23, "張三"));
        ts.add(new Person(13, "李四"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王五"));
        ts.add(new Person(33, "趙六"));

        System.out.println(ts);
    }
}

出錯,會拋出一個異常:java.lang.ClassCastException顯然是出現了類型轉換異常。原因在於我們需要告訴TreeSet如何來進行比較元素,如果不指定,就會拋出這個異常

 

如何解決:

如何指定比較的規則,需要在自定義類(Person)中實現```Comparable```接口,並重寫接口中的compareTo方法 

public class Person implements Comparable<Person>{
    private Integer age;

    private String name;

    public Person(Integer age, String name){
        this.age = age;
        this.name = name;
    }

    public int CompareTo(Person o){
        return 0;     //當compareTo方法返回0的時候集合中只有一個元素
        return 1;    //當compareTo方法返回正數的時候集合會怎么存就怎么取
        return -1;    //當compareTo方法返回負數的時候集合會倒序存儲
    }

為什么返回0,只會存一個元素,返回-1會倒序存儲,返回1會怎么存就怎么取呢?

原因在於TreeSet底層其實是一個二叉樹機構,且每插入一個新元素(第一個除外)都會調用```compareTo()```方法去和上一個插入的元素作比較,並按二叉樹的結構進行排列。

1. 如果將```compareTo()```返回值寫死為0,元素值每次比較,都認為是相同的元素,這時就不再向TreeSet中插入除第一個外的新元素。所以TreeSet中就只存在插入的第一個元素。

2. 如果將```compareTo()```返回值寫死為1,元素值每次比較,都認為新插入的元素比上一個元素大,於是二叉樹存儲時,會存在根的右側,讀取時就是正序排列的。

3. 如果將```compareTo()```返回值寫死為-1,元素值每次比較,都認為新插入的元素比上一個元素小,於是二叉樹存儲時,會存在根的左側,讀取時就是倒序序排列的。

 

(一)、自然排序

實現Comparable接口比較元素

  自然排序使用要排序元素的CompareTo(Object obj)方法來比較元素之間大小關系,然后將元素按照升序排列。

      Java提供了一個Comparable接口,該接口里定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現了該接口的對象就可以比較大小。

      obj1.compareTo(obj2)方法如果返回0,則說明被比較的兩個對象相等,如果返回一個正數,則表明obj1大於obj2,如果是 負數,則表明obj1小於obj2。

      如果我們將兩個對象的equals方法總是返回true,則這兩個對象的compareTo方法返回應該返回0

利用上訴規則,我們就可以按照年齡來排序了。主要代碼:

    public int CompareTo(Person o){
        int num = this.age - o.age;        //年齡是比較的主要天驕
        return num == 0 ? this.name.compareTo(o.name) : num; //姓名是比較的次要條件
    }

完整代碼:

import java.util.*;


class Person implements Comparable<Person>{
    Integer age;
    String name;
    public Person (Integer age, String name){
        this.age = age;
        this.name = name;
    }

    //重寫接口中的方法
    //要重寫比較規則
    @Override
    public int compareTo(Person o){
        int num = this.age - o.age;
        return num == 0 ? this.name.compareTo(o.name) : num;
    }

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


public class test{
    //需求:將字符創安裝長度排序
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>(); 
        
        ts.add(new Person(23, "張三"));
        ts.add(new Person(13, "李四"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王五"));
        ts.add(new Person(33, "趙六"));

        System.out.println(ts);
    }
}

 

按照姓名排序(依據Unicode編碼大小),主要代碼如下:

    public int CompareTo(Person o){
        int num = this.name.compareTo(o.name);        //姓名是比較的主要條件
        retrun num == 0 ? this.age - o.age : num; //年齡是比較的次要條件
    }

完整代碼:

 

import java.util.*;


class Person implements Comparable<Person>{
    Integer age;
    String name;
    public Person (Integer age, String name){
        this.age = age;
        this.name = name;
    }

    //重寫接口中的方法
    //要重寫比較規則
    @Override
    public int compareTo(Person o){
        int num = this.name.compareTo(o.name);
        return num == 0 ? this.age - o.age : num;
    }

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


public class test{
    //需求:將字符創安裝長度排序
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>(); 
        
        ts.add(new Person(23, "張三"));
        ts.add(new Person(13, "李四"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王五"));
        ts.add(new Person(33, "趙六"));

        System.out.println(ts);
    }
}

 

按照姓名長度排序,代碼如下:

    public int CompareTo(Person o){
        int length = this.name.length() - o.name.length(); //比較長度為主要條件
        int num = length == 0? this.name.compareTo(o.name) : length; //比較內容為此要條件
        return num == 0 ? this.age - o.age : num; //比較年齡為次要條件
    }

完整代碼:

 

import java.util.*;


class Person implements Comparable<Person>{
    Integer age;
    String name;
    public Person (Integer age, String name){
        this.age = age;
        this.name = name;
    }

    //重寫接口中的方法
    //要重寫比較規則
    @Override
    public int compareTo(Person o){
        int length = this.name.length() - o.name.length();
        int num = length == 0 ? this.name.compareTo(o.name) : length;
        return num == 0 ? this.age - o.age : num;
    }

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


public class test{
    //需求:將字符創安裝長度排序
    public static void main(String[] args) {
        TreeSet<Person> ts = new TreeSet<>(); 
        
        ts.add(new Person(23, "張三呀"));
        ts.add(new Person(13, "李"));
        ts.add(new Person(13, "周七"));
        ts.add(new Person(43, "王顏王五"));
        ts.add(new Person(33, "趙leslie六"));

        System.out.println(ts);
    }
}

 

以上是TreeSet如何比較自定義對象,接下來我們再來看一下TreeSet如何利用比較器比較元素。 

(二)定制排序

自定義比較器比較元素

自然排序是根據集合元素的大小,以升序排列,如果要定制排序,應該使用Comparator接口,實現 int compare(To1,To2)方法

需求:現在要制定TreeSet中按照String長度比較String。

import java.util.*;

//定義一個類,實現Comparator接口,並重寫compare()方法
class CompareByLen implements Comparator<String>{
    @Override
    public int compare(String s1, String s2){    //按照字符串的長度比較
        int num = s1.length() - s2.length();    //長度為主要條件
        return num == 0 ? s1.compareTo(s2) : num;  //內容為次要條件
    }
}



public class test{
    //需求:將字符創安裝長度排序
    public static void main(String[] args) {
        TreeSet<String> ts = new TreeSet<>(new CompareByLen()); 
        //CompareByLen c = new CompareByLen();

        ts.add("aaaaaaaa");
        ts.add("z");
        ts.add("wc");
        ts.add("nba");
        ts.add("cba");

        System.out.println(ts);
    }
}

 

(三)TreeSet總結:

1.特點

  TreeSet是用來排序的,可以指定一個順序,對象存入之后會按照指定的順序排列

2.使用方式

  (1)自然順序(Comparable)

      TreeSet類的add()方法會把存入的對象提升為Comparable類型

      調用對象的comparaTo()方法和集合中的對象比較

      根據comparaTo()方法返回的結果進行存儲

  (2)比較器順序

      創建TreeSet的時候可以指定一個Comparator

      如果傳入了Comparator的子類對象,那么TreeSet就會按照比較器的順序排序。

      add()方法內部會自動調用Comparator接口中的compare()方法排序

      調用的對象是compare方法的第一個參數,集合中的對象是compare方法的第二個參數

  (3)兩種方式的區別

      TreeSet構造函數什么都不傳,默認按照類中Comparable的順序(沒有就報錯ClassCastException)

      TreeSet如果傳入Comparator,就有先按照Comparator

 

四、LinkedList

(一)、LinkedList概述

  LinkedHashSet集合也是根據元素hashCode值來決定元素存儲位置,但它同時使用鏈表維護元素的次序,這樣使得元素看起來是以插入的順序保存的。也就是說,當遍歷LinkedHashSet集合里元素時,HashSet將會按元素的添加順序來訪問集合里的元素。

  LinkedHashSet需要維護元素的插入順序,因此性能略低於HashSet的性能,但在迭代訪問Set里的全部元素時將有很好的性能,因為它以鏈表來維護內部順序。

(代碼示例)

import java.util.*;

public class test{
    public static void main(String[] args) {
        LinkedHashSet<String> books = new LinkedHashSet<>();
        books.add("java");
        books.add("Linux");
        books.add("C++");
        books.add("算法");
        System.out.println(books);
        //刪除 Java
        books.remove("java");
        //重新添加Java
        books.add("java");
        System.out.println(books);
    }
}

運行結果為:

我們可以看到,“java”所在的位置發生了變化,也證明了插入順序就是遍歷順序。

 

(二)LinkedHashSet結論

輸出LinkedHashSet集合中的元素時,元素的順序總是和添加順序一致。

 


免責聲明!

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



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