1. 程式人生 > 其它 >Java學習筆記127——集合—Set集合—HashSet類、LinkedHashSet類、TreeSet類及其自然排序

Java學習筆記127——集合—Set集合—HashSet類、LinkedHashSet類、TreeSet類及其自然排序

Set集合

HashSet類

import java.util.HashSet;

/*
        Set集合:元素唯一且元素無序(儲存和取出順序不一致)的集合
        
        HashSet類概述
             不保證 set 的迭代順序
             特別是它不保證該順序恆久不變。
        HashSet如何保證元素唯一性
             底層資料結構是雜湊表(元素是連結串列的陣列)
             雜湊表依賴於雜湊值儲存
        新增功能底層依賴兩個方法:
             int hashCode()
             boolean equals(Object obj)

        Set集合中的元素為什麼不會重複?看新增add()方法的原始碼
*/
public class SetDemo1 {
    public static void main(String[] args) {
      //用Set的子類去例項化
        HashSet<String> arr = new HashSet<>();

        //新增元素到集合
        arr.add("hello");
        arr.add("world");
        arr.add("java");
        arr.add("bigdata");
        arr.add("hadoop");
        arr.add("hello");
        arr.add("hello");
        arr.add("java");
        arr.add("spark");
        arr.add("flink");
        arr.add("world");
        arr.add("hadoop");

      //增強for迴圈遍歷
        for(String s : arr){
            System.out.println(s);
        }
    }
}

//----------HashSet<>()中add()的原始碼分析-------

public interface Set<E> extends Collection<E>{

}

public class HashSet<E> extends AbstractSet<E> implements Set<E>{
    private static final Object PRESENT = new Object();

    private transient HashMap<E,Object> map;

    public boolean add(E e) {  
        //E -- String  
        //e -- "hello"
        return map.put(e, PRESENT)==null;
    }
}

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{

    public V put(K key, V value) {
        //key -- "hello"
        //value -- new Object()
        return putVal(hash(key), key, value, false, true);
    }

    //簡單理解為呼叫元素類的hashCode()方法計算雜湊值
    static final int hash(Object key) { //"hello"
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        //理解為雜湊表儲存的是一個一個的結點陣列
        Node<K,V>[] tab; 
        Node<K,V> p; 
        int n, i;

        //判斷雜湊表是否已經初始化完畢,如果沒有初始化,就在這裡初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //根據元素物件計算好的雜湊值再進行一次與運算,計算出的是該元素儲存在雜湊表中的位置
        //如果該元素的位置是null,說明該位置沒有元素,可以進行儲存
        //就建立新的結點,儲存元素
        //分析到這一步我們得出第一個結論,儲存的位置與元素類中的hashCode()有關。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {

            //如果該元素的位置不是null,說明這個位置上已經有元素了,可以確定是雜湊值是一樣的
            //但是呢,我們並不能確定兩個值是不是一樣的
            Node<K,V> e; K k;
            
            //先將存入元素的雜湊值與該位置中元素的雜湊值做比較
            //如果雜湊值都不一樣,繼續走判斷instanceof()
            //如果雜湊值都一樣,會呼叫元素的equals(k)方法進行比較
            //如果equals(k)方法進行比較結果是false,繼續向下執行,最終會將元素插入到集合中或者不插入
            //如果equals(k)方法進行比較結果是true,表示元素的雜湊值和內容都一樣,表示元素重複了
            //就覆蓋,從現象上來看,其實就是不賦值
            //說到這裡我們已經知道了add()方法和hashCode()以及equals()方法有關
            //會不會去重取決於元素型別有沒有重寫hashCode()以及equals()方法
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
}
import java.util.Objects;

public class Student2 {
    private String name;
    private int age;

    public Student2() {
    }

    public Student2(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

  //重寫equals()和hashCode()方法
    @Override
    public String toString() {
        return "Student2{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

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

    @Override
    public int hashCode() {

        return Objects.hash(name, age);
    }
}

import java.util.HashSet;

/*
        儲存自定義物件並遍歷
*/
public class HashSetDemo2 {
    public static void main(String[] args) {
        //建立集合物件
        HashSet<Student2> hashSet = new HashSet<>();

        //建立學生物件
        Student2 s1 = new Student2("xiaowang", 18);
        Student2 s2 = new Student2("xiaowang", 18);
        Student2 s3 = new Student2("xiaoli", 19);
        Student2 s4 = new Student2("xiaoliu", 20);

        hashSet.add(s1);
        hashSet.add(s2);
        hashSet.add(s3);
        hashSet.add(s4);

        for(Student2 s:hashSet){
            System.out.println(s);
        }
      //輸出結果發現沒有去重,這是為什麼呢?
      //根據剛才add()的原始碼分析,如果沒有在Student2類中重寫equals()和hashCode()
      //比較的就是物件的地址值,而s1到s4的地址值都不一樣,所以都新增。沒有達到去重的目的。
      //想要去重,就需要在Student2類中重寫equals()和hashCode()方法
    }
}

LinkedHashSet類

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.TreeSet;

/*
        public class HashSet<E> implements Set<E>{}
        
        public class LinkedHashSet<E> extends HashSet<E>{}

        LinkedHashSet:底層資料結構是雜湊表和雙向連結串列
           雜湊表保證了元素唯一
           連結串列保證了元素的有序(儲存和取出順序一致)
*/
public class LinkedHashSetDemo {
    public static void main(String[] args) {
      //建立LinkedHashSet集合物件
        LinkedHashSet<String> arr = new LinkedHashSet<>();

        //新增元素到集合
        arr.add("hello");
        arr.add("world");
        arr.add("java");
        arr.add("bigdata");
        arr.add("hadoop");
        arr.add("hello");
        arr.add("hello");
        arr.add("java");
        arr.add("spark");
        arr.add("flink");
        arr.add("world");
        arr.add("hadoop");

        for (String s : arr){
            System.out.println(s);
        }
    }
}

TreeSet類及其自然排序

import java.util.TreeSet;

/*
    TreeSet:元素唯一,元素的順序可以按照某種規則進行排序
    兩種排序方式:
        自然排序 :無參構造--實現comparable介面
        比較器排序 :有參構造--實現Comparator介面

    A NavigableSet實現基於TreeMap 。
    的元件使用其有序natural ordering ,或由Comparator集合建立時提供,這取決於所使用的構造方法。

    要想知道如何去重以及排序,就去看原始碼。
    因為Integer類實現了comparable介面,所以它可以做自然排序

*/

public class TreeSetDemo1 {
    public static void main(String[] args) {
        //建立一個集合物件
        TreeSet<Integer> ts = new TreeSet<>();

        //新增元素到集合中
        ts.add(20);
        ts.add(18);
        ts.add(23);
        ts.add(24);
        ts.add(66);
        ts.add(12);
        ts.add(18);
        ts.add(20);
        ts.add(23);
        ts.add(2);

        //遍歷
        for(Integer i : ts){
            System.out.println(i);
        }
    }
}
//---------------TreeSet集合的add()方法的原始碼--------------
    
---------------------------------------
interface Collection {
    ...
}

interface Set extends Collection {
    ...
}
---------------------------------------
class TreeSet implements Set {
    ...
    private static final Object PRESENT = new Object();
    private transient NavigableMap<E,Object> m;
    
    public TreeSet() {
         this(new TreeMap<E,Object>());
    }

    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    ...
}
---------------------------------------
class TreeMap implements NavigableMap {
    ...
    public V put(K key, V value) {
       Entry<K,V> t = root; // 先造根,TreeSet集合底層資料結構是紅黑樹(是一個自平衡的二叉樹)
       if (t == null) {
           compare(key, key); // type (and possibly null) check

           root = new Entry<>(key, value, null);
           size = 1;
           modCount++;
           return null;
       }
       
       int cmp;
       Entry<K,V> parent;
       // split comparator and comparable paths
       Comparator<? super K> cpr = comparator; // 因為用的是TreeSet的無參構造方法,是自然排序,沒有用到comparator比較器
       if (cpr != null) {                      // 所以此時的comparator = null,則程式執行else裡面的程式碼
           do {
               parent = t;
               cmp = cpr.compare(key, t.key);
               if (cmp < 0)
                   t = t.left;
               else if (cmp > 0)
                   t = t.right;
               else
                   return t.setValue(value);
           } while (t != null);
       } else {
           if (key == null)
               throw new NullPointerException();
           Comparable<? super K> k = (Comparable<? super K>) key; // 此介面Comparable強行對實現它的每個類的物件進行整體排序。這種排序被稱為類的自然排序,類的 compareTo 方法被稱為它的自然比較方法。。
           do {                                                   // 舉例中我們使用的是包裝類Intrger,而Integer類實現了Comparable介面。此例子是向上轉型。
               parent = t;
               cmp = k.compareTo(t.key); // 類的 compareTo 方法被稱為它的自然比較方法。
               if (cmp < 0)              // int compareTo(T o) 比較此物件與指定物件的順序。如果該物件小於、等於或大於指定物件,則分別返回負整數、零或正整數。 
                   t = t.left;
               else if (cmp > 0)
                   t = t.right;
               else
                   return t.setValue(value);
           } while (t != null);
       }
       
       Entry<K,V> e = new Entry<>(key, value, parent);
       if (cmp < 0)
           parent.left = e;
       else
           parent.right = e;
       fixAfterInsertion(e);
       size++;
       modCount++;
       return null;
   }
   ...
}
---------------------------------------
    由上可知:真正的比較是依賴於元素的compareTo()方法,而這個方法compareTo()是定義在 Comparable接口裡面的(抽象方法)。
    所以,你要想重寫該方法,就必須是先實現 Comparable介面。這個介面表示的就是自然排序。
public class Student3 implements Comparable<Student3> {//為了解決這個問題,讓Student3實現Comparable介面
    private String name;           //尖括號中放當前元素型別--Student3,並且重寫Comparable介面中的抽象方法
    private int age;

    public Student3() {
    }

    public Student3(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

    @Override
    public int compareTo(Student3 o) {
//        return 0;--只插入第一個元素
//        return 1;--永遠都可以插入,沒有去重
//        return -1;--也不行,也沒去重 

        //這裡返回什麼,其實應該根據我們的規則來排序
        //比如我想在去重的前提下,按照年齡進行排序
//        return this.age - o.age;

        //年齡一樣,姓名不一定一樣
        //主要條件(題目要求的條件)
        int i = this.age - o.age;
        //隱含條件(需要自己挖掘)
        int i2 = i == 0 ? this.name.compareTo(o.name) : i;
        return i2;
    }
}

import java.util.TreeSet;

/*
        TreeSet儲存學生物件並遍歷

        按照正常的寫法,我們一執行就報錯了
        java.lang.ClassCastException:型別轉換異常
        由於我這裡建立TreeSet物件呼叫的是無參構造方法,所以走的是自然排序
        而底層原始碼有一步向下轉型
        Comparable<? super K> k = (Comparable<? super K>) key;
        報錯了
        原因是我們Student3類沒有實現Comparable介面,無法向下轉型,所以報錯了。

*/
public class TreeSetDemo2 {
    public static void main(String[] args) {
        //建立集合物件
        TreeSet<Student3> ts = new TreeSet<>();

        //建立學生物件
        Student3 s1 = new Student3("周姐",24);
        Student3 s2 = new Student3("李元浩",25);
        Student3 s3 = new Student3("李湘赫",22);
        Student3 s4 = new Student3("漢子哥",26);
        Student3 s5 = new Student3("硬幣哥",21);
        Student3 s6 = new Student3("烏茲",20);
        Student3 s7 = new Student3("李元浩",25);
        Student3 s8 = new Student3("廠長",25);

        //將學生物件插入到集合中
        ts.add(s1);
        ts.add(s2);
        ts.add(s3);
        ts.add(s4);
        ts.add(s5);
        ts.add(s6);
        ts.add(s7);
        ts.add(s8);

        for (Student3 s:ts){
            System.out.println(s);
        }
    }
}