Set
Set不儲存重複的元素(如何判斷元素相同?);如果你試圖将相同對象的多個執行個體添加到Set中,那麼它就阻止這種重複的現象。Set中最常被使用的是測試歸屬性,你可以很容易地詢問某個對象是否在某個Set中。正因如此,查找就成為了Set中最重要的操作,是以你通常都會選擇一個HashSet的實作,它專門對快速查找進行了優化。
總結:無序集合,不允許有重複值,允許有null值,存入與取出的順序有可能不一緻,主要有HashSet和TreeSet兩大實作類。

HashSet
HashSet與數學上的集合概念一模一樣。由一個或多個元素所構成的叫做集合。
HashSet實作Set接口,由哈希表(實際上是一個HashMap執行個體)支援。它不保證set 的疊代順序;特别是它不保證該順序恒久不變,此類允許使用null元素。
在HashSet中,元素都存到HashMap鍵值對的Key上面,而Value時有一個統一的值private static final Object PRESENT = new Object();,(定義一個虛拟的Object對象作為HashMap的value,将此對象定義為static final。)
特點:
- HashSet通過使用一種稱為哈希的機制來存儲元素。
- HashSet不能存放重複元素,例如:集合A={1,a},則a不能等于1,也就是如果你把兩個1放進HashSet會自動變為一個1。
- HashSet允許為空值。
- HashSet類是非同步的(線程不安全)。
- HashSet元素是無序的。因為元素是根據其哈希碼插入的,是以HashSet也不得進行排序操作。
- HashSet友善檢索資料。
- HashSet的初始預設容量為16,而負載因子為0.75。
HashSet的構造方法
構造方法 | 描述 |
---|---|
HashSet() | 用于構造預設的HashSet。 |
HashSet(int capacity) | 用于将HashSet的容量初始化為給定的整數容量。随着将元素添加到HashSet中,容量會自動增長。 |
HashSet(int capacity, float loadFactor) | 用于将HashSet的容量初始化為給定的整數容量和指定的負載因子。 |
HashSet(Collection<? extends E> c) | 用于通過使用集合來初始化HashSet。 |
HashSet的方法
思考:如何保證存儲元素不一緻?
通過hashCode方法和equals方法來保證元素的唯一性,add()傳回的是boolean類型;判斷兩個元素是否相同,先要判斷元素的hashCode值是否一緻,隻有在該值一緻的情況下,才會判斷equals方法,如果存儲在HashSet中的兩個對象hashCode方法的值相同equals方法傳回的結果是true,那麼HashSet認為這兩個元素是相同元素,隻存儲一個(重複元素無法存入)。
注意:HashSet集合在判斷元素是否相同先判斷hashCode方法,如果相同才會判斷equals。如果不相同,是不會調用equals方法的。
案例代碼:
Person類:
//Person類
public class Person
{
private String name;
private int age;
Person(String name,int age)
{
this.name=name;
this.age=age;
}
public int hashCode()
{
System.out.println(this.name+"......hashCode");
return 60;
}
public boolean equals(Object obj)
{
if(!(obj instanceof Person))
return false;
Person p=(Person)obj;
System.out.println(this.name+"....equals...."+p.name);
return this.name.equals(p.name) && this.age == p.age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
}
主函數:
import java.util.HashSet;
import java.util.Iterator;
public class HashSetDemo {
public static void sop(Object obj)
{
System.out.println(obj);
}
public static void main(String[] args) {
HashSet hs=new HashSet<Object>();
hs.add(new Person("a1",11));
hs.add(new Person("a2",12));
hs.add(new Person("a2",12));
hs.add(new Person("a3",13));
Iterator it=hs.iterator();
while(it.hasNext()){
Person p=(Person)it.next();
sop(p.getName()+"::"+p.getAge());
}
}
}
運作結果:
a1......hashCode
a2......hashCode
a2....equals....a1
a2......hashCode
a2....equals....a1
a2....equals....a2
a3......hashCode
a3....equals....a1
a3....equals....a2
a1::11
a2::12
a3::13
由此可見我們程式運作順序是:設定a1的hashcode -> 裝入set -> 設定a2的hashcode -> 與a1對比一下(不同)->裝入set -> 設定a2的hashcode -> 與a1對比一下(不同)-> 與a2對比一下(相同)-> 不裝入set -> 設定a3的hashcode -> 與a1對比一下(不同)-> 與a2對比一下(不同)-> 裝入set
為什麼要單獨設定HashCode呢?
因為如果不給每個Person對象設定唯一的Hashcode的話,第一個new Person(“a2”,12)的hashcode與第二個new Person(“a2”,12)的hashcode必然是不一樣的,然後編譯器就會認為它們是不一樣的對象,但是在我們實際場景中,因為它們的屬性是一樣的,是以這裡我們認為這兩個是一樣的對象,是以我們為了讓編譯器知道我們的判斷标準是根據屬性判斷的,而不要讓他在第一步hashcode就判斷完畢了,而不進行到第二步equals方法中,是以我們必須改寫hashcode方法與equals方法。
補充一個常見的面試題:重寫equals方法為什麼要重寫hashcode方法?
貼一篇清晰明了:
https://blog.csdn.net/We_chuan/article/details/96426273
LinkedHashSet
這篇介紹看到一篇博文很受啟發這裡也貼一下
‘https://yq.aliyun.com/articles/635156
我們先來看一下LinkedHashSet的源碼
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
/**
* Constructs a new, empty linked hash set with the default initial
* capacity (16) and load factor (0.75).
*/
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
發現它的源碼也太精簡了吧,然後每個構造器裡面都用了super()調用父類的構造函數,LinkedHashSet是繼承HashSet的,LinkedHashSet繼承了HashSet的全部特性,元素不重複,快速查找,快速插入,并且新增了一個重要特性,那就是有序,可以保持元素的插入順序,是以可以應用在對元素順序有要求的場景中。
然後我們點進去這個super()看一看調用的是哪個構造函數:
這個父類也就是HashSet的構造器中傳回的是一個LinkedHashMap,然後這個方式是預設也就是default修飾符封裝的,也就是說,它不能給HashSet直接調用:
它隻能給子類調用或者重寫,這裡就是給LinkedHashSet調用了,因為LinkedHashSet跟HashSet在同一個包 java.util下;我們來看一下它的使用場景:
咖啡類:
package Collection;
public class Coffee {
private String name;
private String price;
public Coffee(String name, String price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
}
主函數:
package Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetDemo {
public static void main(String[] args) {
//我們想保證插入元素不重複的同時確定他的插入順序
//此時就用LinkedHashSet()實作類
Set<Coffee> linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Coffee("natie", "15"));
linkedHashSet.add(new Coffee("natie", "15"));
linkedHashSet.add(new Coffee("moka", "12"));
linkedHashSet.add(new Coffee("bigmoka", "18"));
Iterator<Coffee> it = linkedHashSet.iterator();
while (it.hasNext()) {
Coffee coffee = it.next();
System.out.println(coffee.getName());
System.out.println(coffee.getPrice());
}
}
}
這裡我們看一下輸出結果:
天啊翻水水,順序倒是沒錯,但是怎麼兩杯15塊的拿鐵都放進去了Set中了呢,說好了Set中是不帶有重複元素的呢?我們的LinkedHashSet是繼承于HashSet的,而add()方法是Set接口定義的,也就是說,我們這裡用的add()方法也是調用了LinkedHashSet的父類,也就是HashSet()中實作的add(),那就回到我們上面講到的問題了,我們在Set是怎麼幫我們自動去重的呢?就是第一步通過檢驗hashcode再走equals,那我們得先給他配置好,才能正确的去重呢!這時候我們需要重寫hashcode與equals:
修改後的Coffee類:
package Collection;
import java.util.Objects;
public class Coffee {
private String name;
private String price;
public Coffee(String name, String price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
@Override
public int hashCode() {
return 60;
}
@Override
public boolean equals(Object obj) {
if (this == obj){ return true;}
if(!(obj instanceof Coffee)){ return false;}
//if (obj == null || getClass() != obj.getClass()) return false;
Coffee coffee = (Coffee) obj;
return this.name.equals(coffee.name) && this.price.equals(coffee.price);
}
}
運作結果:
補充一點供自己記憶:
情況一:隻修改hashcode不修改equals方法:
放入set的時候第一層驗證hashcode的時候相同,但是到了第二步如果不改寫equals的話,對比的是記憶體位址,都是不一樣的,是以會存入相同對象。
情況二:隻修改equals不修改hashcode:
我們重寫了equals讓它通過比較類的成員變量是否一緻,如果一緻則傳回true,不一緻為false,Demo demo1 = new Demo(“11”,“11”), Demo demo2 = new Demo(“11”,“11”); 這裡demo1.equals(demo2)應該是傳回true,然後我們把他放入hashmap中,因為demo1跟demo2的hashcode方法沒有改寫,是以都可以存入,用equals比較說明對象相同,但是在HashMap中卻以不同的對象存儲(沒有重寫hascode值,兩個hascode值,在他看來就是兩個對象)到底這兩個對象相等不相等????說明必須重寫hashCode()的重要性。
TreeSet
TreeSet實作了NavigableSet接口,NavigableSet接口繼承了SortSet接口,是以推測TreeSet是具有排序功能的Set,我們直接上執行個體:
package Collection;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<Coffee> treeSet = new TreeSet<Coffee>();
TreeSet<String> treeString = new TreeSet<String>();
treeString.add("Benz");
treeString.add("BWM");
treeString.add("Audi");
treeString.add("Mini");
System.out.println(treeString);
}
}
運作結果:
發現他給我們按字母表的順序排好序了诶?那能不能給我們自己定義的類進行排序呢?我們也試下剛剛的咖啡類:
報錯了,說是我們定義的咖啡類不能轉換為Comparable類;那為什麼我們剛剛的String類是可以的呢?請看:
原來String類已經偷偷繼承了Comparable接口了呀,其實這裡我們有兩種方法:
- 實作Comparable接口
- 傳入一個外部比較器 我們先來模仿一下String這種繼承接口的方法:
Java填坑之Set
package Collection;
public class Coffee implements Comparable{
private String name;
private String price;
public Coffee(String name, String price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
@Override
public int hashCode() {
return name.hashCode()+price.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj){ return true;}
if(!(obj instanceof Coffee)){ return false;}
//if (obj == null || getClass() != obj.getClass()) return false;
Coffee coffee = (Coffee) obj;
return this.name.equals(coffee.name) && this.price.equals(coffee.price);
}
@Override
public String toString() {
return "Coffee{" +
"name='" + name + '\'' +
", price='" + price + '\'' +
'}';
}
public int compareTo(Object o) {
if(!(o instanceof Coffee)){ return -1;}
Coffee coffee = (Coffee) o;
return this.price.compareTo(coffee.getPrice());
}
}
主函數:
package Collection;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<String> treeString = new TreeSet<String>();
treeString.add("Benz");
treeString.add("BWM");
treeString.add("Audi");
treeString.add("Mini");
System.out.println(treeString);
TreeSet<Coffee> treeSet = new TreeSet<Coffee>();
treeSet.add(new Coffee("natie", "11"));
treeSet.add(new Coffee("natie", "11"));
treeSet.add(new Coffee("black", "8"));
treeSet.add(new Coffee("arbica", "15"));
treeSet.add(new Coffee("arbica11", "101"));
// for (Coffee coffee : treeSet) {
// System.out.println(coffee.getName() + ":" + coffee.getPrice());
// }
System.out.println(treeSet);
}
}
輸出結果:
天啊它怎麼沒按照價格排???是為什麼呢?因為我們這裡設定的price是String類型,調用的是String實作的compareTo,是按照數字排列順序排序的哦,我們來改為數值類型試下;
我們隻需要稍微做一下類型轉換,系統就會識别到并調用的是Integer實作的compareTo方法啦!
輸出結果:
這裡描述的是第一種,繼承Comparable接口,接下來我們看看第二種:
我們看到TreeSet源碼裡面有這麼一個構造器,參數是一個Comparator類,那我們就想到了,我們可以用一個匿名内部類直接寫這個Comparator是怎麼實作的,給他重新定義了是怎麼比較的,我們把Coffee改回來:
Coffee(沒有實作Comparable接口)
package Collection;
public class Coffee {
private String name;
private String price;
public Coffee(String name, String price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
@Override
public int hashCode() {
return name.hashCode()+price.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj){ return true;}
if(!(obj instanceof Coffee)){ return false;}
//if (obj == null || getClass() != obj.getClass()) return false;
Coffee coffee = (Coffee) obj;
return this.name.equals(coffee.name) && this.price.equals(coffee.price);
}
@Override
public String toString() {
return "Coffee{" +
"name='" + name + '\'' +
", price='" + price + '\'' +
'}';
}
}
主函數:
package Collection;
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
TreeSet<String> treeString = new TreeSet<String>();
treeString.add("Benz");
treeString.add("BWM");
treeString.add("Audi");
treeString.add("Mini");
System.out.println(treeString);
TreeSet<Coffee> treeSet = new TreeSet<Coffee>(new Comparator<Coffee>() {
public int compare(Coffee o1, Coffee o2) {
return Integer.valueOf(o1.getPrice()).compareTo(Integer.valueOf(o2.getPrice()));
}
});
treeSet.add(new Coffee("natie", "11"));
treeSet.add(new Coffee("natie", "11"));
treeSet.add(new Coffee("black", "8"));
treeSet.add(new Coffee("arbica", "15"));
treeSet.add(new Coffee("arbica11", "101"));
// for (Coffee coffee : treeSet) {
// System.out.println(coffee.getName() + ":" + coffee.getPrice());
// }
System.out.println(treeSet);
}
}
比較的代碼邏輯是跟上面一樣的,我們寫好我們想要排序的根據是什麼,這裡就是想要通過price排序,然後寫好之後編譯器就會幫我們搞定啦,看一看結果:
沒毛病~