目錄
- Set集合保證元素不重複的原理&Java中的排序接口
-
- 1. Set體系集合的繼承關系和特點
- 2. HashSet如何保證元素的唯一性
-
- 2.1 HashSet集合的簡介和HashSet#add()方法
- 2.2 重寫hashCode()方法的原則
- 2.3 重寫equals()方法的原則
- 3. TreeSet與排序
-
- 3.1 TreeSet集合的簡介和TreeSet#add()方法
- 3.2 自然排序
- 3.3 比較器排序
- 3.4 小結
Set集合保證元素不重複的原理&Java中的排序接口
- 閱讀本文您可以了解到以下内容:
- 常用集合的繼承體系&常用Set體系集合的特點;
- Set體系集合保證元素唯一性的原因;
- 重寫hashCode()方法的原因和原則; 重寫equals()方法的原因和原則;
- TreeSet集合如何對元素進行排序;
- 自然排序,比較器排序,Comparable接口,Comparator接口;
1. Set體系集合的繼承關系和特點
- Set體系常用集合的特點
- HashSet 的特點:無序、不可重複;
- LinkedHashSet 的特點:有序、不可重複;
- TreeSet 的特點:能夠使元素按照某種規則排序、不可重複;
- HashSet 的特點:無序、不可重複;
- Set體系常用集合的資料結構
- HashSet 的底層資料結構:哈希表。增删改都快,哈希表能保證元素的唯一性。
- LinkedHashSet 的底層資料結構:哈希表+連結清單。哈希表保證元素唯一,連結清單保證元素有序。
- TreeSet 的底層資料結構:紅黑樹。該資料結構能保證元素的唯一和排序。
- HashSet 的底層資料結構:哈希表。增删改都快,哈希表能保證元素的唯一性。
2. HashSet如何保證元素的唯一性
2.1 HashSet集合的簡介和HashSet#add()方法
- HashSet特點
- 元素的存入順序和取出順序不同
- 不能存儲重複元素
-
多次調用add()方法添加同樣的資料, 除第一次外都會添加失敗。
是以保證元素唯一性的源碼肯定在HashSet#add()方法中。
- HashSet底層資料結構是哈希表, 哈希表能保證元素的唯一性。
-
- HashSet#add()方法的源碼的簡要分析
//添加相同元素會失敗,展現出儲存元素唯一性,應該看HashSet#add()方法源碼了解為何添加相同元素将失敗
interface Collection {
//...
}
interface Set extends Collection {
//...
}
class HashSet implements Set {
private static final Object PRESENT = new Object();
private transient HashMap<E, Object> map;
//由HashSet集合的構造可知,HashSet的底層是HashMap
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
//可見HashSet#add()方法底層調用了HashMap#put()方法,參數傳入了一個E和一個Object類型的靜态常量PRESENT
return map.put(e, PRESENT) == null;
}
}
class HashMap implements Map {
public V put(K key, V value) {
hashCode();
equals();
}
}
/*
小結:
HashSet的底層是HashMap,HashSet調用add()方法實際上底層是調用HashMap的put()方法。
put()方法的底層依賴hashCode()方法和equals()方法來保證添加元素的唯一性。
Object#hashCode():傳回對象的哈希碼值,對象的位址值經過計算得到的值。
Java泛型中的E,就是Element的意思,一般代表放入集合中的元素。
*/
- HashSet#add()方法保證添加元素唯一性的細節
- 用add()方法欲添加一個元素,先用hashCode()方法算出該元素的哈希值,和哈希表中的哈希值進行比較。
- 如果哈希表中無此哈希值,那麼說明該元素沒被添加過,就添加這個元素。
- 如果哈希表中有該哈希值,那麼比較欲添加元素和同哈希值元素的位址值并使用equals()方法比較欲添加元素和同哈希值元素的内容是否相同。如果位址值或equals()其中之一不同,就添加這個元素。
- 小結
- 使用HashSet#add()方法添加元素,先比較哈希值,哈希值不同,添加該元素;哈希值相同,比較位址值或equals(),其中之一不同就添加該元素。
- 輔助記憶上述内容
-
哈希表就是字典,哈希值就是頁碼,欲添加的元素就是字典上的字。
字的頁碼是根據字和頁碼算法計算出來的(元素的哈希值是根據元素的位址值和雜湊演算法計算出來的)。
- 如果一個字(元素)的頁碼(哈希值)在字典(哈希表)中找不到,說明這個字(元素)沒有被添加過,那麼就添加這個元素。
- 如果一個字(元素)的頁碼(哈希值)能在字典(哈希表)中找到,但是該頁碼(哈希值)下有很多字(元素) (對應不同java對象可能有相同的hashCode),如果該字(元素)和該頁碼(哈希值)下所有字(元素)的位置(位址值)和筆畫順序(内容)其中之一不同, 說明這個字(元素)沒被添加過, 那麼就添加這個元素。
-
- 綜上所屬,如果想讓HashSet#add()保證添加元素的唯一性, 必須重寫集合中元素對應類的hashCode()和equals()。
2.2 重寫hashCode()方法的原則
-
hashCode()方法是Object類中的方法。
該方法的作用是:傳回對象的哈希碼值。
該方法的源碼是:public native int hashCode();
native 關鍵字代表該方法底層是用c/c++實作的。
- 重寫該方法的原則:使成員變量值相同的對象的哈希值相同。
- 重寫該方法的細節
- 将對象的成員變量值相加得出一個值A即可, 如此一來, 值A相同的對象就是成員變量值相同的對象, 就視為相同的對象;
- 如果成員變量是引用類型, 就加哈希值; 如果成員變量是基本類型, 就加變量值;
- 重寫該方法的示例
// String name; int age;
@Override
public int hashCode() {
return this.name.hashCode() + this.age * 任意數字;
}
/*
* 為什麼要乘以任意數字呢 ?
* A對象:name.hashCode()=40, age=30. 40+30=70;
* B對象:name.hashCode()=50, age=20. 50+20=70;
* 若出現以上情況,盡管成員變量值都不同,但是最後相加後結果都是相同的,
* 是以我們将其中一個成員變量值做算數運算,保證了最後相加值的唯一性。
*/
- 注意
-
如果換一個重寫hashCode()的方式,比如将hashCode()重寫為永遠傳回一個固定的數值的方法(例如将hashCode()的方法體重寫為“return 1;”)也能實作Set體系集合的唯一性。
因為比較完哈希值是否相同,還會再用equals()比較,當equals()傳回false時才添加元素,但是這樣做欲添加的第1000個元素需要和以前添加的999個元素做hashCode()比較和equals()比較才能判斷是否有相同元素, 效率低。是以我們想直接比較哈希值,哈希值不同就添加,相同就不添加,這就需要不同元素哈希值不同, 相同元素哈希值相同, 是以我們選擇成員變量相加的方式重寫hashCode().。
HashSet集合的底層是哈希表結構,哈希表就靠hashCode()和equals()實作元素的唯一性。
-
2.3 重寫equals()方法的原則
-
equals()方法是Object類中的方法。
該方法的作用是:判斷兩個變量是否相同。
Object類中的equals()方法是用"=="實作的。
- equals()和==的差別
- ==比較基本資料類型變量,比較的是棧中該變量的值是否相同。
- ==比較引用資料類型變量,比較的是棧中該對象的引用變量是否相同,即比較的是兩個對象的位址值是否相同。
- equals()不能比較基本資料類型。因為equals()是Object類中的方法,基本資料類型不能調用方法。
- equals()比較引用資料類型,比較的是對象的内容是否相同。
- 由于equals()是Object類中的方法,該方法底層是用“==”比較的。是以我們一般需要重寫該方法。有些類已經幫我們重寫了該方法,例如String類。
- 重寫equals()方法的原則:隻要對象的所有屬性值都不同,就視為不同對象。是以重寫equals()隻要寫一個比較兩對象所有屬性值是否相同的方法即可。
-
我們隻需要知道為什麼要重寫hashCode()方法和equals()方法,不需要手動重寫這兩個方法。
使用IDE工具的快捷鍵能快速重寫這兩個方法,并且重寫的方法的邏輯比我們的缜密。
3. TreeSet與排序
3.1 TreeSet集合的簡介和TreeSet#add()方法
- TreeSet集合的特點
- 不能添加重複元素
- 能夠對元素按照某種規則排序
- 排序方式有兩種,具體用哪一種方式取決于建立TreeSet時使用哪個構造方法。
- 自然排序 :需要使用TreeSet的無參構造。
- 比較器排序(通過建立set時提供的Comparator接口進行排序) :需要使用TreeSet的有參構造。
- 排序方式有兩種,具體用哪一種方式取決于建立TreeSet時使用哪個構造方法。
- TreeSet是如何保證唯一并對元素排序的
-
因為TreeSet資料結構是二叉樹,是以元素能排序并唯一。即底層結構是紅黑樹,此資料結構能保證元素的唯一性和排序。類似分析HashSet,我們也要分析TreeSet的add()方法。
通過觀察TreeSet的add(),我們知道最終要看TreeMap的put()。TreeSet底層是TreeMap()。TreeSet的add()的底層是TressMap的put()。
-
- TreeSet集合的資料結構
3.2 自然排序
- 概述
-
用TreeSet無參構造建立對象時才能用此排序方式。
集合中元素對應的類A必須實作Comparable<T>接口(自然排序接口),在類A中必須重寫int compareTo(T o)方法,方法的内容就寫排序的規則。要充分考慮排序的主要條件和次要條件。
-
- TreeSet集合使用自然排序步驟
- 使用無參構造建立TreeSet集合的對象
- 把要傳入集合的元素對應的類實作Comparable<T>接口,泛型填傳入集合的元素類型
- 在要傳入集合的元素對應的類中實作Comparable<T>接口的int compareTo(T o)方法
- 方法内容就是比較的規則,要全面考慮主要規則和次要規則。
- 自然排序示例01
package cn.king.demo01;
import org.junit.Test;
import java.util.TreeSet;
class User implements Comparable<User> {
private String name;
private int age;
@Override
public int compareTo(User o) {
/*
* 傳回什麼, 要根據我們的排序規則來決定;
* 按照年齡排序-->主要條件;
* 按照年齡排序是主要條件,需分析出次要條件;
* 年齡相同時,判斷姓名是否相同-->次要條件;
* 如果年齡和姓名都不相同時,才是同一個元素;
*/
//比較年齡大小--主要條件
int num = this.age - o.age;
//如果年齡相同,就比較姓名,否則還是比較年齡--次要條件
int num2 = num == 0 ? this.name.compareTo(o.name) : num;
return num2;
}
// getter... setter... toString... 全參構造... 無參構造...
}
/**
* 排序方式:自然排序--按照年齡從小到大排序
* 排序規則:
* 注意:如果類A中的對象要進行自然排序,類必須實作自然排序接口;并在類A中實作compareTo()方法。
*/
public class Demo01 {
@Test
public void test01() {
TreeSet<User> users = new TreeSet<>();
User user01 = new User("張三", 20);
User user02 = new User("張三", 30);
User user03 = new User("王五", 4);
User user04 = new User("李四", 50);
User user05 = new User("張三", 20);
User user06 = new User("趙六", 20);
users.add(user01);
users.add(user02);
users.add(user03);
users.add(user04);
users.add(user05);
users.add(user06);
users.forEach(System.out::println);
/* 輸出:
User{name='王五', age=4}
User{name='張三', age=20}
User{name='趙六', age=20}
User{name='張三', age=30}
User{name='李四', age=50}
*/
}
}
- 自然排序示例02
package cn.king.demo01;
import org.junit.Test;
import java.util.TreeSet;
class User implements Comparable<User> {
private String name;
private int age;
@Override
public int compareTo(User o) {
// 比較姓名長度 --> 主要條件
int num = this.name.length() - o.name.length();
// 姓名長度相同, 比較姓名内容 --> 次要條件
int num2 = num == 0 ? this.name.compareTo(o.name) : num;
// 姓名長度和内容相同, 比較年齡 --> 次要條件
int num3 = num2 == 0 ? this.age - o.age : num2;
return num3;
}
// getter... setter... toString... 全參構造... 無參構造...
}
/**
* 排序方式:自然排序--按照年齡從小到大排序
* 排序規則:
* 注意:如果類A中的對象要進行自然排序,類必須實作自然排序接口;并在類A中實作compareTo()方法。
*/
public class Demo01 {
@Test
public void test01() {
TreeSet<User> users = new TreeSet<>();
User user01 = new User("張三", 20);
User user02 = new User("張三", 30);
User user03 = new User("王五", 4);
User user04 = new User("李四", 50);
User user05 = new User("張三", 20);
User user06 = new User("趙六", 20);
User user07 = new User("張三豐", 40);
users.add(user01);
users.add(user02);
users.add(user03);
users.add(user04);
users.add(user05);
users.add(user06);
users.add(user07);
users.forEach(System.out::println);
/*
輸出:
User{name='張三', age=20}
User{name='張三', age=30}
User{name='李四', age=50}
User{name='王五', age=4}
User{name='趙六', age=20}
User{name='張三豐', age=40}
*/
}
}
3.3 比較器排序
- 概述:用TreeSet<E>的有參構造建立對象時提供的Comparator進行排序。
- TreeSet集合使用比較器排序步驟
- 用能使用比較器的有參構造建立TreeSet集合對象
- 構造方法的參數傳入比較器Comparator<T>接口的實作子類A的對象
- 自定義一個比較器實作子類A,實作比較器接口,泛型填要比較的元素類型
- 在比較器實作子類中實作接口的compare方法
- 方法内容寫自定義的比較規則
- 比較器排序示例01
package cn.king.demo01;
import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;
class Student {
private String name;
private int age;
// getter... setter... toString... 全參構造... 無參構造...
}
// 自定義的比較器
class MyComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
// 姓名長度
int num = s1.getName().length() - s2.getName().length();
// 姓名内容
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
// 年齡
int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
return num3;
}
}
// 測試
public class Demo01 {
@Test
public void test01() {
// 建立集合對象用到的構造: public TreeSet(Comparator comparator)
TreeSet<Student> ts = new TreeSet<>(new MyComparator());
// 建立元素并 添加元素
ts.add(new Student("linqingxia", 27));
ts.add(new Student("zhangguorong", 29));
ts.add(new Student("wanglihong", 23));
ts.add(new Student("linqingxia", 27));
ts.add(new Student("liushishi", 22));
ts.add(new Student("wuqilong", 40));
ts.add(new Student("fengqingy", 22));
ts.add(new Student("linqingxia", 29));
// 周遊
ts.forEach(System.out::println);
}
}
- 比較器排序示例02 – 使用匿名内部類
package cn.king.demo01;
import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;
class Student {
private String name;
private int age;
// getter... setter... toString... 全參構造... 無參構造...
}
public class Demo01 {
@Test
public void test01() {
// 建立集合對象用到的構造: public TreeSet(Comparator comparator)
TreeSet<Student> ts = new TreeSet<>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
// 姓名長度
int num = s1.getName().length() - s2.getName().length();
// 姓名内容
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
// 年齡
return num2 == 0 ? s1.getAge() - s2.getAge() : num2;
}
});
// 建立元素并 添加元素
ts.add(new Student("linqingxia", 27));
ts.add(new Student("zhangguorong", 29));
ts.add(new Student("wanglihong", 23));
ts.add(new Student("linqingxia", 27));
ts.add(new Student("liushishi", 22));
ts.add(new Student("wuqilong", 40));
ts.add(new Student("fengqingy", 22));
ts.add(new Student("linqingxia", 29));
// 周遊
ts.forEach(System.out::println);
}
}
- 比較器排序示例03 – 使用Lambda表達式
package cn.king.demo01;
import org.junit.Test;
import java.util.Comparator;
import java.util.TreeSet;
class Student {
private String name;
private int age;
// getter... setter... toString... 全參構造... 無參構造...
}
public class Demo01 {
@Test
public void test01() {
// 建立集合對象用到的構造: public TreeSet(Comparator comparator)
TreeSet<Student> ts = new TreeSet<>((s1, s2) -> {
// 姓名長度
int num = s1.getName().length() - s2.getName().length();
// 姓名内容
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
// 年齡
return num2 == 0 ? s1.getAge() - s2.getAge() : num2;
});
// 建立元素并 添加元素
ts.add(new Student("linqingxia", 27));
ts.add(new Student("zhangguorong", 29));
ts.add(new Student("wanglihong", 23));
ts.add(new Student("linqingxia", 27));
ts.add(new Student("liushishi", 22));
ts.add(new Student("wuqilong", 40));
ts.add(new Student("fengqingy", 22));
ts.add(new Student("linqingxia", 29));
// 周遊
ts.forEach(System.out::println);
}
}
- 比較器排序示例04 – 我們可以在條件允許的情況下使用方法引用來簡化Lambda的書寫
public class Demo01 {
// 原來的匿名内部類
@Test
public void fun1() {
Comparator<Integer> comparator = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
TreeSet<Integer> treeSet = new TreeSet<>(comparator);
}
// lambda表達式.
@Test
public void fun2() {
Comparator<Integer> comparator = (o1, o2) -> Integer.compare(o1, o2);
TreeSet<Integer> treeSet = new TreeSet<>(comparator);
}
// 方法引用
@Test
public void fun3() {
Comparator<Integer> comparator = Integer::compare;
TreeSet<Integer> treeSet = new TreeSet<>(comparator);
}
// 上述的fun1 fun2 fun3 功能相同.
}
關于Lambda表達式和方法引用的使用,詳見我得另一篇部落格: Lambda表達式入門
3.4 小結
- 總結自然排序和比較器排序
- 自然排序(元素具備比較性):讓元素所屬的類實作自然排序接口 Comparable
- 比較器排序(集合具備比較性):讓集合的構造方法接收一個比較器接口的子類對象 Comparator
- 自然排序和比較器排序都能達到同樣的目的,若隻使用一次,建議使用比較器器排序,因為比較器排序能用匿名内部類或Lambda,更靈活。