原來 Set 集合也可以排序


Java 集合類主要由兩個接口派生而出: Collection 和 Map。在 Collection 集合中,我們經常用到的是 List 集合和 Map 集合,而 Set 集合出場的機會就相對比較的少了。在書本上學習的時候就只知道 Set 集合是無序並且是不可重復的,所以也就對 Set 集合排序的問題沒有怎么好好考慮,知其然而不知其所以然。但,最近在項目中就遇到一個關於 Set 集合排序的問題,所以我又拿起了書本,仔細閱讀關於集合方面的資料,並且瀏覽網上相關的教程以及論壇中的帖子等。收集和總結了一些 Set 集合方面的知識,供大家參考。

說起 Set 集合想到的就是 Set 集合是無序並且不重復的集合。當試圖把兩個相同的對象加入一個 Set 中時,對象會調用 equals 方法比較兩個對象元素是否相同,相同則不會加入。

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
Set集合有幾個派生類:HashSet LinkedHashSet TreeSet 等

HashSet
/**
* 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<>();
}
private transient HashMap<E,Object> map;
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
通過查看 JDK 原碼我們可以很清晰的看出 HashSet 的底層就是一個 HashMap,所以 HashSet 的很多特性都是 HashMap 的延續。

HashSet的特點有:

元素的排列順序是隨機的,不保證排列順序
HashSet 非線程安全的,所以速度快
允許元素值為 null
HashSet 集合判斷兩個元素是否相等的標准是兩個對象(元素)通過 equals 方法比較返回 true,並且兩個對象的 HashCode() 方法返回值也要相等。因為 HashCode 集合中存入一個元素時,HashSet 會調用該對象的的 HashCode 方法得到對象的HashCode 值,決定該對象在 HashCode 中的存儲位置。如果加入的兩個對象通過equals() 的返回值相等,但是 HashCode() 的返回值不相等,HashSet 依然會認為兩個元素是不相等的,可以添加成功。所以,當把某個類的對象作為元素存儲到HashSet 中,重寫這個類的 equals 方法和 HashCode 方法時,盡量保證兩個對象通過 equals() 方法返回 true 時,他們的 HashCode() 方法的返回值也是相等的。
/**
* Compares the specified object with this set for equality. Returns
* <tt>true</tt> if the given object is also a set, the two sets have
* the same size, and every member of the given set is contained in
* this set. This ensures that the <tt>equals</tt> method works
* properly across different implementations of the <tt>Set</tt>
* interface.<p>
*
* This implementation first checks if the specified object is this
* set; if so it returns <tt>true</tt>. Then, it checks if the
* specified object is a set whose size is identical to the size of
* this set; if not, it returns false. If so, it returns
* <tt>containsAll((Collection) o)</tt>.
*
* @param o object to be compared for equality with this set
* @return <tt>true</tt> if the specified object is equal to this set
*/
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection c = (Collection) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
LinkedHashSet

LinkedHashSet 是 HashSet 的一個子類。使用鏈表來維護元素的次序。也就是說當遍歷 LinkedHashSet 集合時,將會以元素添加時的順序來訪問集合中的數據。LinkedHashSet 需要維護元素的插入順序,因此性能低於 HashSet,但是在迭代訪問
Set 全部元素時有較好的性能,因為以鏈表維護內部的順序。是非同步的,非線程安全。

TreeSet

TreeSet 是 SortedSet 接口的實現類。如其名字,TreeSet 可以確保內部元素有序,采用紅黑樹的結構來存儲集合元素,TreeSet 支持兩種排序方法:自然排序和定制排序。

自然排序:TreeSet 調用集合元素的 compareTo(Object obj) 方法來比較元素之間的大小,然后按照元素升序排列。這就要求對象元素必須實現 Comparable 接口中的
compareTo(Object obj) 方法。在自然排序下,TreeSet 判斷兩個對象是否相同的唯一標准:兩個對象通過 compareTo(Object o) 方法比較是否相等:該方法返回 0,則認為相等,返回 1 則認為不相等。Java 的一些常見的類,例如 Character,String,Date 等已經實現了 Comparable 接口。

/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element {@code e} to this set if
* the set contains no element {@code e2} such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns {@code false}.
*
* @param e element to be added to this set
* @return {@code true} if this set did not already contain the specified
* element
* @throws ClassCastException if the specified object cannot be compared
* with the elements currently in this set
* @throws NullPointerException if the specified element is null
* and this set uses natural ordering, or its comparator
* does not permit null elements
*/
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
/**
* The backing map.
*/
private transient NavigableMap<E,Object> m;
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
* @throws ClassCastException if the specified key cannot be compared
* with the keys currently in the map
* @throws NullPointerException if the specified key is null
* and this map uses natural ordering, or its comparator
* does not permit null keys
*/
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;
}
定制排序:在創建 TreeSet 集合對象時,需要關聯一個 Comparator 對象,並且實現 Comparator 中的 compare(T obj1,T obj 2),該方法用於比較 o1 和 o2 的大小。

/**
* Constructs a new, empty tree set, sorted according to the specified
* comparator. All elements inserted into the set must be <i>mutually
* comparable</i> by the specified comparator: {@code comparator.compare(e1,
* e2)} must not throw a {@code ClassCastException} for any elements
* {@code e1} and {@code e2} in the set. If the user attempts to add
* an element to the set that violates this constraint, the
* {@code add} call will throw a {@code ClassCastException}.
*
* @param comparator the comparator that will be used to order this set.
* If {@code null}, the {@linkplain Comparable natural
* ordering} of the elements will be used.
*/
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
/**
* Constructs a new, empty tree map, ordered according to the given
* comparator. All keys inserted into the map must be <em>mutually
* comparable</em> by the given comparator: {@code comparator.compare(k1,
* k2)} must not throw a {@code ClassCastException} for any keys
* {@code k1} and {@code k2} in the map. If the user attempts to put
* a key into the map that violates this constraint, the {@code put(Object
* key, Object value)} call will throw a
* {@code ClassCastException}.
*
* @param comparator the comparator that will be used to order this map.
* If {@code null}, the {@linkplain Comparable natural
* ordering} of the keys will be used.
*/
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
/**
* The comparator used to maintain order in this tree map, or
* null if it uses the natural ordering of its keys.
*
* @serial
*/
private final Comparator<? super K> comparator;
/**
* The comparator used to maintain order in this tree map, or
* null if it uses the natural ordering of its keys.
*
* @serial
*/
private final Comparator<? super K> comparator;
原碼學習

由於 TreeSet 底層其實就是調用的 TreeMap 的方法,所以我們再仔細看一下 TreeMap 的原碼。

TreeMap 的構造方法
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
構造方法一,默認的構造方法,comparator 為空,即采用自然順序維持TreeMap中節點的順序。
構造方法二,提供指定的比較器,用於實現比較。
構造方法三,采用自然序維持TreeMap中節點的順序,同時將傳入的Map中的內容添加到 TreeMap 中。
構造方法四,接收 SortedMap 參數,根據 SortedMap 的比較器維持 TreeMap 中的節點順序。同時通過

private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
int redLevel,
Iterator it,
java.io.ObjectInputStream str,
V defaultVal)
方法將 SortedMap 中的內容添加到 TreeMap 中。

TreeMap 的 put 方法
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
* @throws ClassCastException if the specified key cannot be compared
* with the keys currently in the map
* @throws NullPointerException if the specified key is null
* and this map uses natural ordering, or its comparator
* does not permit null keys
*/
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
//如果根節點為null,將傳入的鍵值對構造成根節點(根節點沒有父節點,所以傳入的父節點為null)
root = new Entry<K,V>(key, value, null);
size = 1;
modCount++;
return null;
}
// 記錄比較結果
int cmp;
Entry<K,V> parent;
// 分割比較器和可比較接口的處理
Comparator<? super K> cpr = comparator;
// 有比較器的處理
if (cpr != null) {
// do while實現在root為根節點移動尋找傳入鍵值對需要插入的位置
do {
// 記錄將要被摻入新的鍵值對將要節點(即新節點的父節點)
parent = t;
// 使用比較器比較父節點和插入鍵值對的key值的大小
cmp = cpr.compare(key, t.key);
// 插入的key較大
if (cmp < 0)
t = t.left;
// 插入的key較小
else if (cmp > 0)
t = t.right;
// key值相等,替換並返回t節點的value(put方法結束)
else
return t.setValue(value);
} while (t != null);
}
// 沒有比較器的處理
else {
// key為null拋出NullPointerException異常
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
// 與if中的do while類似,只是比較的方式不同
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);
}
// 沒有找到key相同的節點才會有下面的操作
// 根據傳入的鍵值對和找到的“父節點”創建新節點
Entry<K,V> e = new Entry<K,V>(key, value, parent);
// 根據最后一次的判斷結果確認新節點是“父節點”的左孩子還是又孩子
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 對加入新節點的樹進行調整
fixAfterInsertion(e);
// 記錄size和modCount
size++;
modCount++;
// 因為是插入新節點,所以返回的是null
return null;
}
首先一點通性是 TreeMap 的 put 方法和其他 Map 的 put 方法一樣,向 Map 中加入鍵值對,若原先 “鍵(key)”已經存在則替換 “值(value)”,並返回原先的值。

在 put(K key,V value) 方法的末尾調用了 fixAfterInsertion(Entry x) 方法,這個方法負責在插入節點后調整樹結構和着色,以滿足「紅黑樹」的要求。

每一個節點或者着成紅色,或者着成黑色
根是黑色的
如果一個節點是紅色的,那么它的子節點必須是黑色的
一個節點到一個 null 引用的每一條路徑必須包含相同數量的黑色節點
幾種 Set 實現類的總結

HashSet 的性能比 TreeSet 的性能高(查詢、添加等),因為 TreeSet 需要紅黑樹算法來維護內部元素的順序。只有要實現保持排序狀態的 Set 時,才用到 TreeSet ,否則應該使用 HashSet;

LinkedHashSet 對於插入和刪除操作性能比 HashSet 要差,因為LinkedHashSet 是利用鏈表來維護添加順序的,但是正因如此,遍歷操作要比 HashSet 要快;

在 HashSet 和 TreeSet 中盡量只添加不可變對象;

HashSet 和 TreeSet 的實現類都是線程不安全的。如果多個線程同時訪問修改一個Set集合,必須手動實現線程同步性。例如通過Collections工具類的synchronizeSorted方法包裝Set集合來保證Set集合的線程安全性。

SortedSet sortedSet = Collection.synchronizeSorted(new HashSet());


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM