底層數據結構是哈希表(元素是鏈表的數組)java
關於這個結構,我以爲有必要用一張圖來解釋:數組
哈希表是一個數組,當一個數據(對象)要添加時,先計算對象的hashCode,來肯定它應該對應數組的哪一個位置,若是那個位置沒有數據,則該元素放到對應位置上;若是有數據,則繼續調用該對象的equals方法,與該位置的已有數據對比,若是返回true,則HashSet認爲這兩個數據是同樣的,就不容許添加;若是返回false,則在原位置以鏈表的形式繼續向下鏈接該要添加的數據。數據結構
因此說,哈希表依賴於哈希值存儲,也就是對象的hashCode方法ide
HashSet不保證迭代的插入順序性,特別是不保證每次迭代的順序一致。this
比方說下面這段代碼,咱們作一個String的Set集合,看看每次輸出的結果:spa
import java.util.HashSet; public class Test { public static void main(String[] args) { HashSet<String> set = new HashSet<>(); set.add("嘿嘿"); set.add("呵呵"); set.add("嗯吶"); set.add("哈哈"); set.add("好的"); set.add("呼呼"); System.out.println(set); } } //運行結果 //[嘿嘿, 呵呵, 哈哈, 呼呼, 嗯吶, 好的]
跟add的順序果真不同。。。code
但所謂的迭代順序不肯定,並非幾回試驗就能夠試驗出來的,這取決於對象的hashCode和equals,還有哈希表的內部結構。更深刻的瞭解,戳:「不保證有序」和「保證無序」對象
既然基本數據的包裝類能夠,咱們來試試本身寫的自定義實體類吧:排序
import java.util.HashSet; public class Test { public static void main(String[] args) { HashSet<Person> set = new HashSet<>(); set.add(new Person(1, "辣條")); set.add(new Person(3, "冰棍")); set.add(new Person(4, "麪包")); set.add(new Person(2, "薯片")); set.add(new Person(2, "薯片")); set.add(new Person(2, "薯片")); for (Person person : set) { System.out.println(person); } } } class Person { private int id; private String name; public Person(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + "]"; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
運行以後出現了下圖的異常狀況:接口
出現異常狀況的緣由:
添加功能底層依賴兩個方法:
int hashCode()
boolean equals(Object obj)
那咱們給Person類重寫hashCode方法,存儲Person:
import java.util.HashSet; public class Test { public static void main(String[] args) { HashSet<Person> set = new HashSet<>(); set.add(new Person(1, "辣條")); set.add(new Person(3, "冰棍")); set.add(new Person(4, "麪包")); set.add(new Person(2, "薯片")); set.add(new Person(2, "薯片")); set.add(new Person(2, "薯片")); for (Person person : set) { System.out.println(person); } } } class Person { private int id; private String name; public Person(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + id; return result; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
運行以後:
發現仍然異常,但彷佛咱們有了一個意外收穫:居然以id爲排序規則,升序排序了?
注意:這或許是巧合!但咱們能夠感受到另一個重點:3個id=2在一塊兒了,這也就偏偏跟剛纔的數組+鏈表結構呼應上了!
最後咱們給Person類重寫equals方法,存儲Person:
import java.util.HashSet; public class Test { public static void main(String[] args) { HashSet<Person> set = new HashSet<>(); set.add(new Person(1, "辣條")); set.add(new Person(3, "冰棍")); set.add(new Person(4, "麪包")); set.add(new Person(2, "薯片")); set.add(new Person(2, "薯片")); set.add(new Person(2, "薯片")); for (Person person : set) { System.out.println(person); } } } class Person { private int id; private String name; public Person(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "Person [id=" + id + ", name=" + name + "]"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + id; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Person other = (Person) obj; if (id != other.id) return false; return true; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
運行結果:
徹底正常!這也就說明了爲何自定義的實體類必需要重寫hashCode和equals方法!
當你試圖把對象加入HashSet時,HashSet會使用對象的hashCode來判斷對象加入的位置。
同時也會與其餘已經加入的對象的hashCode進行比較,若是沒有相等的hashCode,HashSet就會假設對象沒有重複出現。
若是元素(對象)的hashCode值相同,並不會當即存入,而是會繼續使用equals進行比較。
若是equals爲true,那麼HashSet認爲新加入的對象重複了,因此加入失敗。
若是equals爲false,那麼HashSet認爲新加入的對象沒有重複,新元素能夠存入。
HashSet與List都有contains方法,List接口的實現所有使用equals,而HashSet先使用hashCode,再使用equals
---------------------------多嘮幾句--------------------------------
若是有興趣去扒源碼,會發現HashSet實際上是利用了HashMap的鍵來作的。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); // 這裏new了一個HashMap }
另外一個能夠解釋的位置在iterator方法裏:
/** * Returns an iterator over the elements in this set. The elements * are returned in no particular order. * * @return an Iterator over the elements in this set * @see ConcurrentModificationException */ public Iterator<E> iterator() { return map.keySet().iterator(); }
返回的居然是map.keySet()的iterator????(黑人問號.jpg)