本篇部落格主要講解Set介面的三個實現類HashSet、LinkedHashSet、TreeSet的使用方法以及三者之間的區別。
注意:本文中程式碼使用的JDK版本為1.8.0_191
1. HashSet使用
HashSet是Set介面最常用的實現類,底層資料結構是雜湊表,HashSet不保證元素的順序但保證元素必須唯一。
private transient HashMap<E,Object> map;
HashSet類的程式碼宣告如下所示:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
......
}
1.1 新增元素
使用HashSet新增元素的使用方法如下所示:
HashSet<String> platformSet = new HashSet<>();
// 新增元素
System.out.println(platformSet.add("部落格園"));
System.out.println(platformSet.add("掘金"));
System.out.println(platformSet.add("微信公眾號"));
// 新增重複元素,不會新增成功,因為Set不允許重複元素
// 不過程式碼不會報錯,而是返回false,即新增失敗
System.out.println(platformSet.add("部落格園"));
System.out.println(platformSet.add("掘金"));
以上程式碼執行的輸出結果是:
true
true
true
false
false
除錯程式碼也會發現platformSet只有3個元素:
值得注意的是,platformSet.add(3, "個人部落格");
這句程式碼會出現編譯錯誤,因為Set集合新增元素只有1個方法,並不像上篇部落格中講解的List介面一樣提供了2個過載。
1.2 獲取元素
和List介面不一樣的是,Set類介面並沒有獲取元素的方法。
1.3 獲取集合元素個數
獲取HashSet元素個數的使用方法如下所示:
System.out.println("platformSet的元素個數為:" + platformSet.size());
1.4 刪除元素
值得注意的是,使用HashSet刪除元素也只有1個方法,並不像使用ArrayList刪除元素有2個過載:
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
使用方法如下所示:
// 刪除不存在的元素"個人部落格",返回false
System.out.println(platformSet.remove("個人部落格"));
// 刪除存在的元素 "微信公眾號",返回true
System.out.println(platformSet.remove("微信公眾號"));
1.5 修改元素
和List介面不一樣的是,Set類介面並沒有修改元素的方法。
1.6 判斷集合是否為空
判斷HashSet是否為空的使用方法如下所示:
System.out.println("isEmpty:" + platformSet.isEmpty());
1.7 遍歷元素(面試常問)
遍歷HashSet的元素主要有以下2種方式:
- 迭代器遍歷
- foreach迴圈
使用方法如下所示:
System.out.println("使用Iterator遍歷:");
Iterator<String> platformIterator = platformSet.iterator();
while (platformIterator.hasNext()) {
System.out.println(platformIterator.next());
}
System.out.println();
System.out.println("使用foreach遍歷:");
for (String platform : platformSet) {
System.out.println(platform);
}
1.8 清空集合
清空ArrayList中所有元素的使用方法如下所示:
platformSet.clear();
1.9 完整示例程式碼
上面講解的幾點,完整程式碼如下所示:
package collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SetTest {
public static void main(String[] args) {
Set<String> platformSet = new HashSet<>();
// 新增元素
System.out.println(platformSet.add("部落格園"));
System.out.println(platformSet.add("掘金"));
System.out.println(platformSet.add("微信公眾號"));
// 新增重複元素,不會新增成功,因為Set不允許重複元素
// 不過程式碼不會報錯,而是返回false,即新增失敗
System.out.println(platformSet.add("部落格園"));
System.out.println(platformSet.add("掘金"));
System.out.println("platformSet的元素個數為:" + platformSet.size());
// 刪除不存在的元素"個人部落格",返回false
System.out.println(platformSet.remove("個人部落格"));
// 刪除存在的元素 "微信公眾號",返回true
System.out.println(platformSet.remove("微信公眾號"));
System.out.println("platformSet的元素個數為:" + platformSet.size());
System.out.println("isEmpty:" + platformSet.isEmpty());
System.out.println("使用Iterator遍歷:");
Iterator<String> platformIterator = platformSet.iterator();
while (platformIterator.hasNext()) {
System.out.println(platformIterator.next());
}
System.out.println();
System.out.println("使用foreach遍歷:");
for (String platform : platformSet) {
System.out.println(platform);
}
System.out.println();
platformSet.clear();
System.out.println("isEmpty:" + platformSet.isEmpty());
}
}
輸出結果為:
true
true
true
false
false
platformSet的元素個數為:3
false
true
platformSet的元素個數為:2
isEmpty:false
使用Iterator遍歷:
部落格園
掘金
使用foreach遍歷:
部落格園
掘金
isEmpty:true
2. LinkedHashSet使用
LinkedHashSet也是Set介面的實現類,底層資料結構是連結串列和雜湊表,雜湊表用來保證元素唯一,連結串列用來保證元素的插入順序,即FIFO(First Input First Output 先進先出)。
LinkedHashSet類的程式碼宣告如下所示:
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
{
}
從以上程式碼也能看出,LinkedHashSet類繼承了HashSet類。
LinkedHashSet類的使用方法和HashSet基本一樣,只需修改下宣告處的程式碼即可:
Set<String> platformSet = new LinkedHashSet<>();
3. TreeSet使用
TreeSet也是Set介面的實現類,底層資料結構是紅黑樹,TreeSet不僅保證元素的唯一性,也保證元素的順序。
TreeSet類的程式碼宣告如下所示:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
}
TreeSet類的使用方法和HashSet基本一樣,只需修改下宣告處的程式碼即可:
Set<String> platformSet = new TreeSet<>();
4. HashSet、LinkedHashSet、TreeSet的區別(面試常問)
HashSet、LinkedHashSet、TreeSet是實現Set介面的3個實現類,其中:
HashSet只是通用的儲存資料的集合,
LinkedHashSet的主要功能用於保證FIFO(先進先出)即有序的集合,
TreeSet的主要功能用於排序(自然排序或者比較器排序)
4.1 相同點
1)HashSet、LinkedHashSet、TreeSet都實現了Set介面
2)三者都保證了元素的唯一性,即不允許元素重複
3)三者都不是執行緒安全的
可以使用Collections.synchronizedSet()方法來保證執行緒安全
4.2 不同點
4.2.1 排序
HashSet不保證元素的順序
LinkHashSet保證FIFO即按插入順序排序
TreeSet保證元素的順序,支援自定義排序規則
空口無憑,上程式碼看效果:
HashSet<String> hashSet = new HashSet<>();
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
TreeSet<String> treeSet = new TreeSet<>();
String[] letterArray = new String[]{"B", "A", "D", "C", "E"};
for (String letter : letterArray) {
hashSet.add(letter);
linkedHashSet.add(letter);
treeSet.add(letter);
}
System.out.println("HashSet(我不保證順序):" + hashSet);
System.out.println("LinkedHashSet(我保證元素插入時的順序):" + linkedHashSet);
System.out.println("TreeSet(我按排序規則保證元素的順序):" + treeSet);
上面程式碼的輸出結果為:
HashSet(我不保證順序):[A, B, C, D, E]
LinkedHashSet(我保證元素插入時的順序):[B, A, D, C, E]
TreeSet(我按排序規則保證元素的順序):[A, B, C, D, E]
4.2.2 null值
HashSet,LinkedHashSet允許新增null值,TreeSet不允許新增null值,新增null時會丟擲java.lang.NullPointerException
異常。
Set<String> platformSet = new TreeSet<>();
platformSet.add(null);
執行上面的程式碼,報錯資訊如下所示:
4.2.3 效能
理論情況下,新增相同數量的元素, HashSet最快,其次是LinkedHashSet,TreeSet最慢(因為內部要排序)。
然後我們通過一個示例來驗證下,首先新建Employee類,自定義排序規則:
package collection;
public class Employee implements Comparable<Employee> {
private Integer employeeNo;
public Employee(Integer employeeNo) {
this.employeeNo = employeeNo;
}
public Integer getEmployeeNo() {
return employeeNo;
}
public void setEmployeeNo(Integer employeeNo) {
this.employeeNo = employeeNo;
}
@Override
public int compareTo(Employee o) {
return this.employeeNo - o.employeeNo;
}
}
然後新增如下驗證程式碼,分別往HashSet,LinkedHashSet,TreeSet中新增10000個元素:
Random random = new Random();
HashSet<Employee> hashSet = new HashSet<>();
LinkedHashSet<Employee> linkedHashSet = new LinkedHashSet<>();
TreeSet<Employee> treeSet = new TreeSet<>();
int maxNo = 10000;
long startTime = System.nanoTime();
for (int i = 0; i < maxNo; i++) {
int randomNo = random.nextInt(maxNo - 10) + 10;
hashSet.add(new Employee(randomNo));
}
long endTime = System.nanoTime();
long duration = endTime - startTime;
System.out.println("HashSet耗時: " + duration);
startTime = System.nanoTime();
for (int i = 0; i < maxNo; i++) {
int randomNo = random.nextInt(maxNo - 10) + 10;
linkedHashSet.add(new Employee(randomNo));
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("LinkedHashSet:耗時 " + duration);
startTime = System.nanoTime();
for (int i = 0; i < maxNo; i++) {
int randomNo = random.nextInt(maxNo - 10) + 10;
treeSet.add(new Employee(randomNo));
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("TreeSet耗時: " + duration);
第1次執行,輸出結果:
HashSet耗時: 6203357
LinkedHashSet:耗時 5246129
TreeSet耗時: 7813460
第2次執行,輸出結果:
HashSet耗時: 9726115
LinkedHashSet:耗時 5521640
TreeSet耗時: 6884474
第3次執行,輸出結果:
HashSet耗時: 7263940
LinkedHashSet:耗時 6156487
TreeSet耗時: 8554666
第4次執行,輸出結果:
HashSet耗時: 6140263
LinkedHashSet:耗時 4643429
TreeSet耗時: 7804146
第5次執行,輸出結果:
HashSet耗時: 7913810
LinkedHashSet:耗時 5847025
TreeSet耗時: 8511402
從5次執行的耗時可以看出,TreeSet是最耗時的,不過LinkedHashSet的耗時每次都比HashSet少,
這就和上面說的HashSet最快矛盾了,所以這裡留個疑問:HashSet和LinkedHashSet哪個更快?
大家怎麼看待這個問題,歡迎留言。
5. TreeSet的兩種排序方式(面試常問)
先回顧下上面使用TreeSet排序的程式碼:
TreeSet<String> treeSet = new TreeSet<>();
String[] letterArray = new String[]{"B", "A", "D", "C", "E"};
for (String letter : letterArray) {
treeSet.add(letter);
}
System.out.println("TreeSet(我按排序規則保證元素的順序):" + treeSet);
我們插入元素的順序是"B", "A", "D", "C", "E"
,但是輸出元素的順序是"A", "B", "C", "D", "E"
,證明TreeSet已經按照內部規則排過序了。
那如果TreeSet中放入的元素型別是我們自定義的引用型別,它的排序規則是什麼樣的呢?
帶著這個疑問,我們新建個Student類如下:
package collection;
public class Student {
private String name;
private int age;
public Student(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;
}
}
然後新增如下驗證程式碼:
TreeSet<Student> studentTreeSet = new TreeSet<>();
Student student1 = new Student("zhangsan", 20);
Student student2 = new Student("lisi", 22);
Student student3 = new Student("wangwu", 24);
Student student4 = new Student("zhaoliu", 26);
Student student5 = new Student("zhangsan", 22);
studentTreeSet.add(student1);
studentTreeSet.add(student2);
studentTreeSet.add(student3);
studentTreeSet.add(student4);
studentTreeSet.add(student5);
for (Student student : studentTreeSet) {
System.out.println("name:" + student.getName() + ",age:" + student.getAge());
}
滿心歡喜的執行程式碼想看下效果,結果卻發現報如下錯誤:
為什麼會這樣呢?
這是因為我們並沒有給Student類定義任何排序規則,TreeSet說我也不知道咋排序,還是甩鍋丟擲異常吧,哈哈。
怎麼解決呢?有以下兩種方式:
- 自然排序
- 比較器排序
5.1 自然排序
自然排序的實現方式是讓Student類實現介面Comparable,並重寫該介面的方法compareTo,該方法會定義排序規則。
使用IDEA的快捷鍵生成的compareTo方法預設是這樣的:
@Override
public int compareTo(Student o) {
return 0;
}
這個方法會在執行add()方法新增元素時執行,以便確定元素的位置。
如果返回0,代表兩個元素相同,只會保留第一個元素
如果返回值大於0,代表這個元素要排在引數中指定元素o的後面
如果返回值小於0,代表這個元素要排在引數中指定元素o的前面
因此如果對compareTo()方法不做任何修改,直接執行之前的驗證程式碼,會發現集合中只有1個元素:
name:zhangsan,age:20
然後修改下compareTo()方法的邏輯為:
@Override
public int compareTo(Student o) {
// 排序規則描述如下
// 按照姓名的長度排序,長度短的排在前面,長度長的排在後面
// 如果姓名的長度相同,按字典順序比較String
// 如果姓名完全相同,按年齡排序,年齡小的排在前面,年齡大的排在後面
int orderByNameLength = this.name.length() - o.name.length();
int orderByName = orderByNameLength == 0 ? this.name.compareTo(o.name) : orderByNameLength;
int orderByAge = orderByName == 0 ? this.age - o.age : orderByName;
return orderByAge;
}
再次執行之前的驗證程式碼,輸出結果如下所示:
name:lisi,age:22
name:wangwu,age:24
name:zhaoliu,age:26
name:zhangsan,age:20
name:zhangsan,age:22
5.2 比較器排序
比較器排序的實現方式是新建一個比較器類,繼承介面Comparator,重寫介面中的Compare()方法。
注意:使用此種方式Student類不需要實現介面Comparable,更不需要重寫該介面的方法compareTo。
package collection;
import java.util.Comparator;
public class StudentComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
// 排序規則描述如下
// 按照姓名的長度排序,長度短的排在前面,長度長的排在後面
// 如果姓名的長度相同,按字典順序比較String
// 如果姓名完全相同,按年齡排序,年齡小的排在前面,年齡大的排在後面
int orderByNameLength = o1.getName().length() - o2.getName().length();
int orderByName = orderByNameLength == 0 ? o1.getName().compareTo(o2.getName()) : orderByNameLength;
int orderByAge = orderByName == 0 ? o1.getAge() - o2.getAge() : orderByName;
return orderByAge;
}
}
然後修改下驗證程式碼中宣告studentTreeSet的程式碼即可:
TreeSet<Student> studentTreeSet = new TreeSet<>(new StudentComparator());
輸出結果和使用自然排序的輸出結果完全一樣。
6. 原始碼及參考
Java集合中List,Set以及Map等集合體系詳解(史上最全)
7. 最後
打個小廣告,歡迎掃碼關注微信公眾號:「申城異鄉人」,定期分享Java技術乾貨,讓我們一起進步。