Java集合系列(三):HashSet、LinkedHashSet、TreeSet的使用方法及區別

本篇博客主要講解Set接口的三個實現類HashSet、LinkedHashSet、TreeSet的使用方法以及三者之間的區別。java

注意:本文中代碼使用的JDK版本爲1.8.0_191面試

1. HashSet使用

HashSet是Set接口最經常使用的實現類,底層數據結構是哈希表,HashSet不保證元素的順序但保證元素必須惟一。安全

private transient HashMap<E,Object> map;
複製代碼

HashSet類的代碼聲明以下所示:bash

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("掘金"));
複製代碼

以上代碼運行的輸出結果是:數據結構

truedom

trueide

true性能

falseui

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種方式:

  1. 迭代器遍歷
  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 清空集合

清空HashSet中全部元素的使用方法以下所示:

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說我也不知道咋排序,仍是甩鍋拋出異常吧,哈哈。

怎麼解決呢?有如下兩種方式:

  1. 天然排序
  2. 比較器排序

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技術乾貨,讓咱們一塊兒進步。

相關文章
相關標籤/搜索