天天看點

Java學習筆記一Set集合

Set集合與Collection基本相同,沒有提供任何額外的方法,實際上Set就是Collection,隻是不允許包含相同的元素。

HashSet類

HashSet是Set接口的典型實作,按Hash算法來存儲集合中的元素,具有很好的存取和查找性能。

HashSet具有以下特點:不能保證元素的排列順序,順序可能與添加順序不同,順序也有可能發生變化。HashSet不是同步的,如果多個線程同時通路一個HashSet,假如有兩個或兩個以上線程同時修改了HashSet集合時,則必須通過代碼來保證其同步。集合元素值可以是null。

當向HashSet集合中存入一個元素時,HashSet會調用該對象的hashCode()方法來得到該對象的hashCode值,然後根據hashCode值決定該對象在HashSet中的存儲位置。如果有兩個元素通過equals方法比較傳回true,但它們的hashCode方法傳回值不相等,HashSet将會把它們存儲在不同的位置,依然可以添加成功。HashSet集合判斷兩個元素相等的标準是兩個對象通過equals方法比較相等,并且兩個對象的hashCode方法傳回值也相等。

注意點:當把一個對象放入HashSet中時,如果需要重寫該對象對應類的equals方法,則也應該重寫其hashCode方法。規則是,如果兩個對象通過equals方法比較傳回true,這兩個對象的hashCode值也應該相同。如果兩個對象通過equals方法比較傳回true,但這兩個對象的hashCode方法傳回不同的hashCode值時,這将導緻HashSet會把這兩個對象儲存在Hash表的不同位置,進而使兩個對象都可以添加成功,這就與Set集合的規則沖突了。如果兩個對象的hashCode方法傳回的hashCode值相同,但它們通過equals方法比較傳回fasle時将更麻煩,因為兩個對象的hashCode值相同,HashSet将試圖把它們儲存在同一位置,但又不行(否則将隻剩下一個對象),是以實際上會在這個位置用鍊式結構來儲存多個對象;而HashSet通路集合元素時也是根據元素的hashCode值來快速定位的,如果HashSet中兩個以上的元素具有相同的hashCode值,将會導緻性能下降。

HashSet中每個能存儲元素的槽位slot通常稱為桶bucket,如果有多個元素的hashCode值相同,但它們通過equals()方法比較傳回false,就需要在一個桶裡放多個元素,這樣會導緻性能下降。

重寫hashCode方法的一般步驟:

  1. 把對象内每個有意義的執行個體變量(即每個參與equals方法比較标準的執行個體變量)計算出一個int類型的hashCode值。

    | 執行個體變量 | 計算方式 |

    | boolean | hashCode=(f?0:1); |

    | 整數類型(byte、short、char、int) | hashCode=(int)f; |

    | long | hashCode=(int)(f^(f>>32)); |

    | float | hashCode=Float.floatToIntBits(f); |

    | double | long l = Double.doubleToLongBits(f); hashCode=(int)(l^(l>>32)); |

    | 引用類型 | hashCode=f.hashCode(); |

  2. 用第1步計算出來的多個hashCode值組合計算出一個hashCode值傳回。

    例如​​

    ​return f1.hashCode() + (int)f2;​

    ​​ 為了避免直接相加産生偶然相等(兩個對象的f1、f2執行個體變量并不相等,但它們的hashCode的和恰好相等),可通過為各執行個體變量的hashCode乘以任意一個質數後再相加​

    ​return f1.hashCode()*19 + (int)f2*31;​

class R
{
  int count;
  public R(int count) { this.count = count; }
  public String toString() { return "R[count:"+count+"]"; }
  public boolean equals(Object obj)
  {
    if(this == obj)  return true;
    if(obj != null && obj.getClass() == R.class){
      R r = (R)obj;
      return this.count == r.count;
    }
    return false;
  }
  public int hashCode() { return this.count; }
}
public class HashSetTest2
{
  public static void main(String[] args)
  {
    HashSet hs = new HashSet();
    hs.add(new R(5));
    hs.add(new R(-3));
    hs.add(new R(9));
    hs.add(new R(-2));
    // 列印HashSet集合,集合元素沒有重複
    System.out.println(hs);
    // 取出第一個元素
    Iterator it = hs.iterator();
    R first = (R)it.next();
    // 為第一個元素的count執行個體變量指派
    first.count = -3;
    // 再次輸出HashSet集合,集合元素有重複元素
    System.out.println(hs);
    // 删除count為-3的R對象
    hs.remove(new R(-3));
    // 可以看到被删除一個R元素
    System.out.println(hs);
    System.out.println("hs是否包含count為-3的R對象?"+hs.contains(new R(-3)));  // 輸出false
    System.out.println("hs是否包含count為-2的R對象?"+hs.contains(new R(-2)));  // 輸出false
  }
}      
Java學習筆記一Set集合

LinkedHahSet類

LinkedHashSet集合根據元素的hashCode值來決定元素的存儲位置,但它同時使用連結清單維護元素的次序,這樣使得元素看起來是以插入的順序儲存的。也就是說,當周遊LinkedHasdSet集合裡的元素時,LinkedHashSet将會按元素的添加順序來通路集合裡的元素。

public class LinkedHashSetTest
{
  public static void main(String[] args){
    LinkedHashSet books = new LinkedHashSet();
    books.add("瘋狂JAVA講義");
    books.add("輕量級Java EE企業應用實戰");
    System.out.println(books);
    // 删除 瘋狂Java講義
    books.remove("瘋狂JAVA講義");
    // 添加 瘋狂Java講義
    books.add("瘋狂JAVA講義");
    System.out.println(books);
  }
}      
Java學習筆記一Set集合

雖然LinkedHashSet使用了連結清單記錄集合元素的添加順序,但LinkedHashSet依然是HashSet,是以它依然不允許集合元素重複。

TreeSet類

TreeSet是SortedSet接口的實作類,可以確定集合元素處于排序狀态,根據元素實際值的大小來進行排序的。TreeSet采用紅黑樹的資料結構來存儲集合元素,并且支援兩種排序方法:自然排序和定制排序。

TreeSet nums = new TreeSet();
// 向TreeSet中添加四個Integer對象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
// 輸出集合元素,看到集合元素已經處于排序狀态
System.out.println(nums);
// 輸出集合中第一個元素
System.out.println(nums.first());
// 輸出集合中最後一個元素
System.out.println(nums.last());
// 傳回小于4的子集,不包括4
System.out.println(nums.headSet(4));
// 傳回大于5的子集,如果Set中包含5,子集中還包含5
System.out.println(nums.tailSet(5));
// 傳回大于等于-3、小于4的子集
System.out.println(nums.subSet(-3,4));      
Java學習筆記一Set集合

自然排序

TreeSet會調用集合元素的compareTo(Object obj)方法來比較元素之間的大小關系,然後将集合元素按升序排列。Java提供了一個Comparable接口,該接口裡定義了一個compareTo(Object obj)方法,該方法傳回一個整數值,實作該接口的類必須實作該方法,實作了該接口的類對象就可以比較大小。當一個對象調用該方法與另一個對象進行比較時,例如obj1.compareTo(obj2),如果該方法傳回0,則表明這兩個對象相等;如果該方法傳回一個正整數,則表明obj1大于obj2;如果該方法傳回一個負整數,則表明obj1小于obj2。

Java 9改進了TreeSet實作,如果采用自然排序的Set集合的元素沒有實作Comparable接口,程式就會立即引發ClassCastException異常。大部分類在實作compareTo(Object obj)方法時,都需要将被比較對象obj強制類型轉換成相同類型,是以隻有相同類的兩個執行個體才會比較大小。當試圖把一個對象添加到TreeSet集合時,TreeSet會調用該對象的compareTo(Object obj)方法與集合中的其他元素進行比較-這就要求集合中的其他元素與該元素是同一個類的執行個體。也就是說,向TreeSet中添加的應該是同一個類的對象,否則會引發ClassCastException異常。如果向TreeSet中添加的對象是程式員自定義類的對象,則可以向TreeSet中添加多種類型的對象,前提是使用者自定義類實作了Comparable接口,且實作compareTo(Object obj)方法沒有進行強制類型轉換。但當試圖取出TreeSet裡的集合元素時,不同類型的元素依然會發生ClassCastException異常。

當需要把一個對象放入TreeSet中,重寫該對象對應類的equals()方法時,應保證該方法與compareTo(Object obj)方法有一緻的結果,其規則是:如果兩個對象通過equals方法比較傳回true時,這兩個對象通過compareTo(Object obj)方法比較應傳回0。

class Z implements Comparable
{
    int age;
    public Z(int age) { this.age = age; }
    //重寫equals方法,總是傳回true
    public boolean equals(Object obj) { return true; }
    //重寫compareTo方法,總是傳回1
    public int compareTo(Object obj) { return 1; }
}      
TreeSet set = new TreeSet();
Z z1 = new Z(6);
set.add(z1);
//第二次添加同一個對象,輸出true,表示添加成功
//TreeSet依靠對象的compareTo與容器中其他對象比較大小,而自定義的Z一直傳回正數,表明不相等,可以添加
System.out.println(set.add(z1));
// 可見有兩個對象,其實是兩個引用,指向同一個對象
System.out.println(set);
((Z)(set.first())).age = 9;
System.out.println(((Z)(set.last())).age);      
Java學習筆記一Set集合

向TreeSet中添加一個可變對象後,通過修改該可變對象的執行個體變量,将導緻它與其他對象的大小順序發生改變,但TreeSet不會再次調整它們的順序。一旦改變了TreeSet集合裡可變元素的執行個體變量,當再試圖删除該對象時,TreeSet會删除失敗(甚至集合中原有的、執行個體變量沒被修改但與修改後元素相等的元素也無法删除),但是可以删除沒有被修改執行個體變量、且不與其他被修改執行個體變量的對象重複的對象,删除後TreeSet會對集合中的元素重新索引(不是重新排序),接下來就可以删除TreeSet中的所有元素了,包括那些被修改過執行個體變量的元素。與HashSet類似,如果TreeSet中包含了可變對象,當可變對象的執行個體變量被修改時,TreeSet在處理這些對象時将非常複雜,而且容易出錯。

import java.util.TreeSet;
class R implements Comparable
{
    int count;
    public R(int count) { this.count = count; }
    public String toString() { return "R[count:"+count+"]"; }
    // 重寫equals方法,根據count來判斷是否相等
    public boolean equals(Object obj)
    {
        if(this == obj){
            return true;
        }
        if(obj != null && obj.getClass() == R.class)
        {
            R r = (R)obj;
            return r.count == this.count;
        }
        return false;
    }
    // 重寫compareTo方法,根據count來比較大小
    public int compareTo(Object obj){
        R r = (R)obj;
        return count > r.count ? 1 : count < r.count ? -1 : 0;
    }
}
public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet ts = new TreeSet();
        ts.add(new R(5));
        ts.add(new R(-3));
        ts.add(new R(9));
        ts.add(new R(-2));
        // 列印TreeSet集合,集合元素是有序排列的
        System.out.println(ts);
        // 取出第一個元素
        R first = (R)ts.first();
        // 對第一個元素的count指派
        first.count = 20;
        // 取出最後一個元素
        R last = (R)ts.last();
        // 對最後一個元素的count指派,與第二個元素的count相同
        last.count = -2;
        // 再次輸出将看到TreeSet裡的元素處于無序狀态,且有重複元素
        System.out.println(ts);
        // 删除執行個體變量被改變的元素,删除失敗
        System.out.println(ts.remove(new R(-2)));
        System.out.println(ts);
        // 删除執行個體變量沒有被改變的元素,删除成功
        System.out.println(ts.remove(new R(5)));
        System.out.println(ts);
    }
}      
Java學習筆記一Set集合

定制排序

如果需要實作定制排序,則需要在建立TreeSet集合對象時,提供一個Comparator對象與該TreeSet集合關聯,由該Comparator對象負責集合元素的排序邏輯。由于Comparator是一個函數式接口,是以可使用Lambda表達式來代替Comparator對象。

當通過Comparator對象(或Lambda表達式)來實作TreeSet的定制排序時,依然不可以向TreeSet中添加類型不同的對象,否則會引發ClassCastException異常。使用定制排序時,TreeSet對集合元素排序不管集合元素本身大小,而是由Comparator對象(或Lambda表達式)負責集合元素的排序規則。TreeSet判斷兩個集合元素相等的标準是:通過Comparator(或Lambda表達式)比較兩個元素傳回了0,這樣TreeSet不會把第二個元素添加到集合中。

import java.util.TreeSet;
class M
{
    int age;
    public M(int age) { this.age = age; }
    public String toString() { return "M[age:"+age+"]"; }
}
public class TreeSetTest1 {
    public static void main(String[] args) {
        // Lambda表達式的目标類型是Comparator
        TreeSet ts = new TreeSet((o1, o2) -> {
            M m1 = (M)o1;
            M m2 = (M)o2;
            // 根據M對象的age屬性來決定大小,age越大,M對象反而越小
            return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0;
        });
        ts.add(new M(5));
        ts.add(new M(-3));
        ts.add(new M(9));
        ts.add(new M(-2));
        System.out.println(ts);
    }
}      
Java學習筆記一Set集合

EnumSet類

import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
enum Season
{
    SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest {
    public static void main(String[] args) {
        // 建立一個EnumSet集合,集合元素就是Season枚舉類的全部枚舉值
        EnumSet es1 = EnumSet.allOf(Season.class);
        System.out.println(es1);
        // 建立一個EnumSet空集合,指定其集合元素是Season類的枚舉值
        EnumSet es2 = EnumSet.noneOf(Season.class);
        System.out.println(es2);
        // 手動添加兩個元素
        es2.add(Season.WINTER);
        es2.add(Season.SPRING);
        System.out.println(es2);
        // 以指定枚舉值建立EnumSet集合
        EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
        System.out.println(es3);
        EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
        System.out.println(es4);
        // 新建立的EnumSet集合元素和es4集合元素有相同的類型
        // es5集合元素 + es4集合元素 = Season枚舉類的全部枚舉值
        EnumSet es5 = EnumSet.complementOf(es4);
        System.out.println(es5);

        Collection c = new HashSet();
        c.clear();
        c.add(Season.FALL);
        c.add(Season.SPRING);
        // 複制Collection集合中的所有元素來建立EnumSet集合
        EnumSet enumSet = EnumSet.copyOf(c);
        System.out.println(enumSet);
    }
}