.集合Set,HashSet,TreeSet及其底層實現HashMap和紅黑樹;Collection總結
ONE.Set集合
one.Set集合的特點
無序,唯一
TWO.HashSet集合
1.底層資料結構是雜湊表(是一個元素為連結串列的陣列)
2.那麼HashSet如何來實現元素的唯一性的呢?
通過一HashSet新增字串的案例檢視HashSet中add()的原始碼,看為什麼相同的字串沒有被加入HashSet中
interface Collection { ... } interface Set extends Collection { ... } class HashSet implements Set { private static final Object PRESENT = new Object(); private transient HashMap<E,Object> map; //1.從這一步我們可以看出來,HashSet()其實使用HashMap()實現的 public HashSet() { map = new HashMap<>(); } public boolean add(E e) { //e=hello,world //2.add()方法的內部也是HashMap的例項物件呼叫方法,這裡e是被新增的物件,而PRESENT是一個 private static final Object PRESENT = new Object();物件。 return map.put(e, PRESENT)==null; } } class HashMap implements Map { //3.來到HashMap實現的put方法 public V put(K key, V value) { //key=e=hello,world //4.看雜湊表是否為空,如果空,就開闢空間 if (table == EMPTY_TABLE) { inflateTable(threshold); } //5.判斷物件是否為null if (key == null) return putForNullKey(value); //6_1.呼叫hash()方法,通過檢視這個方法我們知道這個方法的返回值和物件的hashCode()方法相關 int hash = hash(key); //7.在雜湊表中查詢hash值 int i = indexFor(hash, table.length); //8.這裡for迴圈的的初始條件是把table[i]賦值給e,如果在雜湊表中找不到這個hash值得話, 就不會進入for迴圈比較,如果有的話,就會進入比較 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //9.如果hash值一樣(說實話,這一步每太看懂,既然能夠查詢到,說明兩者的hash值必然相等), 並且地址值或者equls一樣的話,不會新增進來 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; //走這裡其實是沒有新增元素 } } modCount++; //9.把元素新增 addEntry(hash, key, value, i); return null; } transient int hashSeed = 0; //6_2.這是HashMap中的hash方法,從這個方法的實現我們可以看出,這個方法的唯一變數是 hashCode() final int hash(Object k) { //k=key=e=hello, int h = hashSeed;//這個值預設是0 if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); //這裡呼叫的是物件的hashCode()方法 // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
HashSet其實是用HashMap()來實現的,HashMap()是Map介面的實現類
呼叫HashSet的add()方法其實就是呼叫HashMap()中的put(),put()中主要涉及到兩個方面
1).物件的hash值,通過呼叫hash()得到,這個方法是由hashCode()經過操作實現的,由hashCode()的值控制
2).建立了雜湊表,雜湊表會將每一個hash值收入
然後比較的方式是
A.先在hash表中查詢看是否有這個hash值(第一次比較,看hash表中是否有當前元素的hash值(這個值有hashcode操作的得到)),如果沒有,直接將這個hash值對應的物件新增到HashSet中,如果有還要進行第二次比較
B.如果hash表中有這個hash值,那麼獲取表中的這個hash對應的物件,如果這兩個物件的地址值(e.key == key)或者key.equal(e.key)。(第二次比較,如果兩個物件的hash值相同,還是不能認為是同一個物件,還要比較兩個物件的地址值,或者是equals(),這裡是一個||,只要有一個滿足相等,就可以認為是同一個元素,不新增
3.現在可以回答開始的問題,為什麼儲存字串的時候,字串相同的時候,只儲存了一個了,原因是String類中重寫了hashCode()和equals()而String類的hashcode值和equals結果都是由字串的內容決定的
我們來看一下String類中的hashCode()和equals()方法
1) public int hashCode()返回此字串的雜湊碼。
String 物件的雜湊碼根據以下公式計算:
s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]
使用 int 演算法,這裡 s[i] 是字串的第 i 個字元,n 是字串的長度,^ 表示求冪。(空字串的雜湊值為 0。)
2)public boolean equals(Object anObject)將此字串與指定的物件比較。
當且僅當該引數不為 null,並且是與此物件表示相同字元序列的 String 物件時,結果才為 true。
所以當物件是String型別的時候,地址值(可能相等(字串常量池知識點)),equals方法必然相等,所以不會加入
4.如果HashSet中收入的是自定義物件,那如何實現唯一呢?
1)通過上面String類的分析,我們知道,HashSet中實現唯一的兩個比較,先是比較hash值(這個值由hashcode值控制),如果這個值相同,那麼再比較地址值或者equals(),所以我們可以通過重寫hashCode()和equals(),來實現對自定義物件的唯一性判斷
2)這裡開始的想法是,其實只要重寫了equals,讓這個方法比較的是物件的內容,那麼就可以排除掉相同的物件
所以讓hashCode()返回一個常數,這樣hash就相等了,然後通過equals判斷
但是這樣會導致每一個新物件都要和老物件進行比較,太麻煩,所以我們可以仿照String類中對hashCode()重寫的方式,同樣的有物件的成員變數來決定這個物件的hashcode值,equals方法也是通過比較物件的成員變數
是否相等。這樣我們就得到了最後的結論,其實eclipse提供了自定義類重寫這兩個方法的最終版本
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Student other = (Student) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
3)開發的時候,程式碼非常的簡單,自動生成即可。
THREE.TreeSet集合
1:底層資料結構是紅黑樹(是一個自平衡的二叉樹)
2:TreeSet底層是如何保證元素的排序方式,和元素的唯一性呢
同樣的我們來看看看TreeSet的原始碼
interface Collection {...}
interface Set extends Collection {...}
interface NavigableMap {
}
class TreeMap implements NavigableMap {
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
}
class TreeSet implements Set {
private transient NavigableMap<E,Object> m;
public TreeSet() {
this(new TreeMap<E,Object>());
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
真正的比較是依賴於元素的compareTo()方法,而這個方法是定義在 Comparable裡面的。
所以,你要想重寫該方法,就必須是先 Comparable介面。這個介面表示的就是自然排序。
根據原始碼,我們知道,TreeSet的底層程式碼可以通過兩種方式來實現其元素的唯一性和有序
a:自然排序(元素具備比較性)
讓元素所屬的類實現Comparable介面,重寫其中的compareTo方法
package cn.itcast_03;
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student() {
super();
// TODO Auto-generated constructor stub
}
public Student(String name, int age) {
super();
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;
}
@Override
public int compareTo(Student s) {
int num = 0;
num = this.age - s.age;
int num2 = num == 0 ? this.name.compareTo(s.name) : num;
return num2;
}
}
b:比較器排序(集合具備比較性)
讓集合構造方法接收Comparator的實現類物件,在實現類中重寫compare()方法
這種方法通過呼叫集合的帶參構造來實現比較
public TreeSet(Comperator comparator)//比較器排序
Comperator 是一個介面,把介面作為引數其實就是需要這個介面的實現類的物件
通過API我們可以得知這個介面中方法的格式,另外,這種需要一個介面的實現類的物件作為引數的情況,開發中我們常常使用匿名內部類的格式
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
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;
}
});
3.需要注意的問題
1)根據對原始碼和底層資料結構紅黑樹的理解,我們知道無論是通過自然排序還是通過比較器排序,資料結構核心是通過將根節點和子節點比較,在紅黑樹的預設方法中,小的為左兒子,大的為右兒子,實現一顆平衡二叉樹,而程式碼的體現是將成員變數進行某種比較得到一個int值,如果值大於1,就放右邊,如果值小於1就放左邊,如果值相等就不放入,通過這個int值來完成元素的收集
2)我們在考慮這個值的時候,對於大於,小於,相等的條件設定常常使用三目運算子來做判斷,熟練使用這一三目運算子
//如果按照正常的二叉排序,那麼會先輸出小的,而我們要求先輸出分高的,所以,可以將全部的s1,s2交換順序
//就可以達到總分大的放左邊(先輸出),然後單科權重是語文——數學——英語
int num = s2.getSum() - s1.getSum();
int num2 = num ==0 ? s2.getChineseScore() - s1.getChineseScore() : num;
int num3 = num2 == 0 ? s2.getMathScore() - s1.getMathScore() : num2;
int num4 = num3 == 0 ? s2.getEnglishScore() - s1.getEnglishScore() : num3;
return num4;
FOUR.Collection集合總結(掌握) Collection
|--List 有序,可重複
|--ArrayList
底層資料結構是陣列,查詢快,增刪慢。
執行緒不安全,效率高
|--Vector
底層資料結構是陣列,查詢快,增刪慢。
執行緒安全,效率低
|--LinkedList
底層資料結構是連結串列,查詢慢,增刪快。
執行緒不安全,效率高
|--Set 無序,唯一
|--HashSet
底層資料結構是雜湊表。
如何保證元素唯一性的呢?
依賴兩個方法:hashCode()和equals()
開發中自動生成這兩個方法即可
|--LinkedHashSet
底層資料結構是連結串列和雜湊表
由連結串列保證元素有序
由雜湊表保證元素唯一
|--TreeSet
底層資料結構是紅黑樹。
如何保證元素排序的呢?
自然排序
比較器排序
如何保證元素唯一性的呢?
根據比較的返回值是否是0來決定
4:針對Collection集合我們到底使用誰呢?(掌握)
唯一嗎?
是:Set
排序嗎?
是:TreeSet
否:HashSet
如果你知道是Set,但是不知道是哪個Set,就用HashSet。
否:List
要安全嗎?
是:Vector
否:ArrayList或者LinkedList
查詢多:ArrayList
增刪多:LinkedList
如果你知道是List,但是不知道是哪個List,就用ArrayList。
如果你知道是Collection集合,但是不知道使用誰,就用ArrayList。
如果你知道用集合,就用ArrayList。
5:在集合中常見的資料結構(掌握)
ArrayXxx:底層資料結構是陣列,查詢快,增刪慢
LinkedXxx:底層資料結構是連結串列,查詢慢,增刪快
HashXxx:底層資料結構是雜湊表。依賴兩個方法:hashCode()和equals()
TreeXxx:底層資料結構是二叉樹。兩種方式排序:自然排序和比較器排序