1. 程式人生 > 實用技巧 >JAVA集合框架 - Map介面

JAVA集合框架 - Map介面

Map

介面大致說明(jdk11):

整體介紹:

一個將鍵對映到值的(key-value)物件, 鍵值(key)不能重複, 每個鍵值只能影射一個物件(一一對應).

這個介面取代了Dictionary類,後者是一個完全抽象的類,而不是一個介面。

Map介面提供了三個集合檢視(Set<K> keySet(); Collection<V> values();, Set<Map.Entry<K, V>> entrySet();),允許將Map的內容視為一組鍵、一組值或一組鍵-值對映。對映的順序被定義為對映集合檢視上的迭代器返回元素的順序。一些對映實現,比如TreeMap類,對它們的順序做出了特定的保證;其他類,比如HashMap,則不需要。


注意: 如果使用可變物件作為對映鍵,必須非常小心。這種情況下equals和hashCode方法會很難定義...最好別用.

所有通用對映實現類都應該提供兩個“標準”建構函式:一個是void(無引數)建構函式,它建立一個空對映;另一個是帶有一個型別為map的引數的建構函式,它建立一個具有與它的引數相同的鍵-值對映的新對映。實際上,後一個建構函式允許使用者複製任何對映,生成所需類的等效對映。雖然沒有辦法強制執行這一規範(因為介面不能包含建構函式),但是JDK中的所有通用對映實現都遵循這一規範。

此介面中包含的“破壞性”方法,即修改它們所操作的對映的方法,被指定為在該對映不支援操作時丟擲UnsupportedOperationException。如果呼叫對對映沒有影響,這些方法可能(但不是必需的)丟擲UnsupportedOperationException。例如,在不可修改的對映上呼叫putAll(Map)方法,如果要“疊加”的對映為空的對映可能(但不是必需的)丟擲異常。

有些map實現對它們可能包含的鍵和值有限制。例如,有些實現禁止空鍵和值,而有些則限制其鍵的型別。試圖插入不合格的鍵或值會丟擲未檢查的異常,通常是NullPointerExceptionClassCastException。試圖查詢是否存在不合格的鍵或值可能會丟擲異常,或者它可能僅僅返回false;有些實現會顯示前一種行為,有些則會顯示後一種行為。更普遍的情況是,嘗試對不符合條件的鍵或值執行操作,如果完成操作不會導致在對映中插入不符合條件的元素,則可能會丟擲異常或成功,具體取決於實現的選項。這種異常在該介面的規範中被標記為“可選”。

集合框架介面中的許多方法都是根據equals方法定義的。

例如,containsKey(物件鍵)方法的規範說:“當且僅當這個對映包含一個鍵k的對映,這樣(key==null ?k = = null: key.equals (k))。”此規範不應被解釋為暗示呼叫對映。包含一個非空引數key的containsKey會導致key.equals(k)被任何key k呼叫。實現可以自由地實現優化,從而避免呼叫equals.


例如,首先比較兩個key的雜湊碼。(Object.hashCode()規範保證兩個雜湊碼不相等的物件不能相等。)更普遍的是,在實現者認為合適的地方,各種集合框架介面的實現可以自由地利用底層物件方法的指定行為。

不可變的Map:

Map.of, Map.ofEntries, Map.copyOf靜態工廠方法提供了一種建立不可修改對映的方便方法。這些方法建立的對映例項具有以下特徵:

  • 他們是無法改變的。不能新增、刪除或更新鍵和值。呼叫對映上的任何mutator方法總是會導致丟擲UnsupportedOperationException。但是,如果包含的鍵或值本身是可變的,這可能會導致對映的行為不一致或其內容似乎發生了變化。
  • 它們不允許空鍵和值。嘗試使用空鍵或值建立它們會導致NullPointerException。
  • 如果所有鍵和值都是可序列化的,則它們是可序列化的。
  • 它們在建立時拒絕重複的key。傳遞給靜態工廠方法的重複鍵會導致IllegalArgumentException異常。
  • 對映的迭代順序是未指定的,並且可能會更改
  • value值如果是物件的話,是不完全可靠的.有可能會發生變化.
  • 它們被序列化為在序列化表單頁面上指定的格式。

2. 常用類繼承結構

方法介紹

查詢操作

/**
* 返回此對映中鍵-值對映的數目。如果對映包含超過整數。MAX_VALUE元素,返回整數。MAX_VALUE。
* @return 該對映中鍵-值對映的數量
*/
int size(); /**
* 判斷是否為空
*/
boolean isEmpty(); /**
* 判斷傳入的鍵值在該map中是否存在.
* 等於 Objects.equals(key, k) (最多隻能有一個這樣的對映)
*/
boolean containsKey(Object key); /**
* 如果此對映將一個或多個鍵對映到指定的值,則返回true。
* 更正式地說,當且僅當這個對映包含至少一個到值v的對映時,返回true。
* 對於大多數map介面的實現來說,這個操作可能需要對映大小的線性時間。(時間複雜度為O(n))
*/
boolean containsValue(Object value); /**
* 返回指定鍵對映到的值,如果該對映不包含該鍵的對映,則返回null。
*/
V get(Object key);

修改操作

/**
* 將此對映中的指定值與指定鍵關聯(可選操作)。
* 如果對映以前包含該鍵的對映,則舊值將被指定的值替換。
* (當且僅當m. containskey (k)返回true時,對映m被稱為包含鍵k的對映。
*/
V put(K key, V value); /**
* 如果金鑰存在,則從該對映中刪除該金鑰的對映(可選操作)。
* 返回此對映以前與鍵關聯的值,如果對映不包含該鍵的對映,則返回null。
* 如果該對映允許空值,那麼null的返回值並不一定表明該對映不包含該鍵的對映;對映也可能顯式地將鍵對映為null。
* 一旦呼叫返回,對映將不包含指定鍵的對映
*/
V remove(Object key);

批量處理

/**
* 將指定對映中的所有對映覆制到此對映(可選操作)。
* 這個呼叫的效果相當於對指定對映中從鍵k到值v的每個對映在這個對映上呼叫一次put(k, v)。
* 如果在操作過程中修改了指定的對映,則此操作的行為未定義。
*/
void putAll(Map<? extends K, ? extends V> m); /**
* 從該對映中刪除所有對映(可選操作)。此呼叫返回後對映將為空。
*/
void clear();

檢視

/**
* 返回此對映中包含的鍵的Set檢視。
* 集合受到對映的支援,因此對對映的更改反映在集合中,反之亦然。
* 如果在對集合進行迭代時修改了對映(迭代器自己的刪除操作除外),那麼迭代的結果是未定義的。
* 這個集合支援元素刪除,從Map中刪除相應的對映,
* 通過remove、Set.remove、removeAll、retainAll和clear操作。
* 它不支援add或addAll操作。
*/
Set<K> keySet(); /**
* 返回此對映中包含的值的Collection檢視。
* 對映支援集合,因此對對映的更改反映在集合中,反之亦然。
* 如果在對集合進行迭代時修改了對映(迭代器自己的刪除操作除外),那麼迭代的結果是未定義的。
* 集合支援元素刪除,元素刪除通過迭代器從對映中刪除相應的對映,通過Iterator.remove, Set.remove, removeAll, retainAll和clear操作。
* 它不支援add或addAll操作。
*/
Collection<V> values(); /**
* 迭代的是map中的key-val對映實體(Entry), 其它的與上面倆差不多
Set<Map.Entry<K, V>> entrySet();

比較和雜湊

/**
* 比較指定的物件與此對映是否相等。
* 如果給定的物件也是對映,並且這兩個對映表示相同的對映,則返回true。
* 更正式地說,兩個對映m1和m2表示相同的對映,如果m1. entryset ().equals(m2. entryset())。
* 這確保了equals方法在Map介面的不同實現之間正常工作。
*/
boolean equals(Object o); /**
* 返回此對映的雜湊碼值。
* 對映的雜湊碼被定義為對映的entrySet()檢視中每個條目的雜湊碼的和。
* 這確保了m1.equals(m2)意味著對於任意兩個對映m1和m2, m1. hashcode ()==m2. hashcode(),這是Object.hashCode的一般契約所要求的。
*/
int hashCode();

預設方法 (jdk1.8提供的)

/**
* 獲取key對應的值, 如果沒有就返回傳入的預設值
*/
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
} /**
* foreach迭代
* 例子:
* for (Map.Entry<K, V> entry : map.entrySet())
* action.accept(entry.getKey(), entry.getValue());
* }
*/
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
} /**
* 遍歷過程中對值進行替換
* 例子:
* for (Map.Entry<K, V> entry : map.entrySet())
* entry.setValue(function.apply(entry.getKey(), entry.getValue()));
* }
*/
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
} // ise thrown from function is not a cme.
v = function.apply(k, v); try {
entry.setValue(v);
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
} /**
* 如果指定的鍵尚未與值關聯(或對映為null),則將其與給定值關聯並返回null,否則返回當前值。
*/
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
} return v;
} /**
* 僅當指定鍵項當前對映到指定值時才刪除該項。
*/
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
} /**
* 只有在key對應的value值與傳入的oldvalue相等時才替換並返回true, 否則返回false
*/
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
} /**
* 如果key/value鍵值對存在就替換
*/
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
// 用於流處理的
// 之後寫流處理的時候再整理吧...
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
} return v;
}
default V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue;
if ((oldValue = get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
put(key, newValue);
return newValue;
} else {
remove(key);
return null;
}
} else {
return null;
}
}
default V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = get(key); V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
// delete mapping
if (oldValue != null || containsKey(key)) {
// something to remove
remove(key);
return null;
} else {
// nothing to do. Leave things as they were.
return null;
}
} else {
// add or replace old mapping
put(key, newValue);
return newValue;
}
}
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if (newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
} // 十一個of方法
static <K, V> Map<K, V> of() {
return ImmutableCollections.emptyMap();
} static <K, V> Map<K, V> of(K k1, V v1) {
return new ImmutableCollections.Map1<>(k1, v1);
} static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2);
} static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2, K k3, V v3) {
return new ImmutableCollections.MapN<>(k1, v1, k2, v2, k3, v3);
}
// ...剩下的都是ImmutableCollections.MapN<>接收可變長引數進行實現的 // ImmutableCollections 中的Map1 MapN
// static final class Map1<K,V> extends AbstractImmutableMap<K,V>
// static final class MapN<K,V> extends AbstractImmutableMap<K,V>
// 這倆都是繼承自AbstractImmutableMap
// 而AbstractImmutableMap繼承自AbstractMap, 把所有跟put相關的方法都預設返回UnsupportedOperationException()
// 在Map1 MapN中,都是隻有建構函式可以接收引數並建立好需要的集合.
// 除構造方法之外沒有別的修改以建立好集合的方法.
abstract static class AbstractImmutableMap<K,V> extends AbstractMap<K,V> implements Serializable {
@Override public void clear() { throw uoe(); }
@Override public V compute(K key, BiFunction<? super K,? super V,? extends V> rf) { throw uoe(); }
@Override public V computeIfAbsent(K key, Function<? super K,? extends V> mf) { throw uoe(); }
@Override public V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> rf) { throw uoe(); }
@Override public V merge(K key, V value, BiFunction<? super V,? super V,? extends V> rf) { throw uoe(); }
@Override public V put(K key, V value) { throw uoe(); }
@Override public void putAll(Map<? extends K,? extends V> m) { throw uoe(); }
@Override public V putIfAbsent(K key, V value) { throw uoe(); }
@Override public V remove(Object key) { throw uoe(); }
@Override public boolean remove(Object key, Object value) { throw uoe(); }
@Override public V replace(K key, V value) { throw uoe(); }
@Override public boolean replace(K key, V oldValue, V newValue) { throw uoe(); }
@Override public void replaceAll(BiFunction<? super K,? super V,? extends V> f) { throw uoe(); }
} // 建立一個不可變的對映
static <K, V> Entry<K, V> entry(K k, V v) {
// KeyValueHolder checks for nulls
return new KeyValueHolder<>(k, v);
} /**
* 返回包含給定對映項的不可修改對映。給定的對映不能為空,也不能包含任何空鍵或值。
* 如果給定的對映隨後被修改,返回的對映將不會反映這種修改。
*/
static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map) {
if (map instanceof ImmutableCollections.AbstractImmutableMap) {
return (Map<K,V>)map;
} else {
return (Map<K,V>)Map.ofEntries(map.entrySet().toArray(new Entry[0]));
}
}

Map.Entity介面

Map.Entity介面定義了Map集合中的實際儲存節點.

Map的實現類, 諸如Hashmap, TreeMap等, 儲存節點都是實現了Map.Entity的靜態內部類.

上面提到的Map.entrySet迴圈方法, 得到的就是所有Map.Entry<K, V>的集合.

interface Entry<K, V> {
/**
* 返回節點儲存的key值
*/
K getKey(); /**
* 返回節點儲存的value值
*/
V getValue(); /**
* 將與此項對應的值替換為指定的值
*/
V setValue(V value); /**
* 比較指定的物件與此條目是否相等。
* 如果給定的物件也是對映項,並且這兩個項表示相同的對映,則返回true。
*/
boolean equals(Object o); /**
* 返回此對映項的雜湊碼值。
*/
int hashCode(); // 用於流處理的比較器
// 之後寫流處理的時候再整理吧...
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
} public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
} public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}

4. 雜湊洪水攻擊

概念

雜湊洪水攻擊(Hash-Flooding Attack)是一種拒絕服務攻擊(Denial of Service),一旦後端介面存在合適的攻擊面,攻擊者就能輕鬆讓整臺伺服器陷入癱瘓。

雜湊表的時間複雜性

雜湊表(Hash Table)的'平均執行時間'和'最差執行時間'會差很遠。

假設我們想要連續插入n個元素到雜湊表中:

  • 如果這些元素的鍵(Key)極少出現相同雜湊值,這項任務就只需O(n)的時間。
  • 如果這些鍵頻繁出現相同的雜湊值(頻繁發生碰撞),這項任務就需要O(n^2)的時間。

衍生出的奇思妙想:

既然雜湊表資料結構的最差執行時間這麼廢物,我們有沒有可能通過演演算法上的漏洞,強行構造出一個最差情況,讓伺服器把全部的資源都浪費在處理這個最差情況上?

由雜湊map的預設計算雜湊值和Object類的計算雜湊值策略可以看到, 如果不做自定義的話, 預設就是使用Java自帶的字串雜湊函式 :“DJBX33A演演算法”的變種.

// hashmap計算雜湊值的策略
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// Object類的計算雜湊值策略
@HotSpotIntrinsicCandidate
public native int hashCode();

而根據這個演演算法定義,就可以輕鬆地構造出一批具有一樣雜湊值的字串, 這樣只要構造出足夠多的雜湊的字串,把它們提交給伺服器做雜湊表, 就能用很低的成本將伺服器打癱.

如何防禦

雜湊洪水攻擊的根本在於黑客可以通過已知的雜湊演演算法算出雜湊值相同的數.

所以我們可以使用帶金鑰雜湊演演算法(Keyed Hash Function), 在演演算法中加入一個祕密引數 - 雜湊種子(Hash Seed), 讓黑客無法掌握具體的雜湊演演算法,就可以進行有效的防禦了.

來自Google、UIC等機構的眾多研究人員設計了許多新的雜湊函式:SipHashMurmurHashCityHash等等。這些演演算法不停地被推翻,不停地更新版本,到現在已經形成了一套穩定的演演算法標準,被眾多程式語言和開源專案所採納。

Java提出的解決方案:

從JDK 8開始,HashMapLinkedHashMapConcurrentHashMap三個類引入了一套新的策略來處理雜湊碰撞。

  • 當一個位置儲存的元素個數小於8個時,仍然使用連結串列儲存。
  • 當一個位置儲存的元素個數大於等於8個時,改為使用平衡樹來儲存。

這樣一來,就能保證最差的執行時間是O(n log n)了。

為什麼要設立“8個元素”(TREEIFY threshold):

因為平衡樹相比連結串列而言有著更高的開銷,以及更散亂的記憶體佈局(影響快取命中率)。在正常情況下,雜湊表的一個位置大約只會儲存1~4個左右的元素,所以沒有必要專門開一個平衡樹來儲存衝突的元素,對一些效能敏感的應用來說會造成顯著的負面影響。

雜湊洪水概念說明整理自知乎大佬的回覆:

什麼是雜湊洪水攻擊(Hash-Flooding Attack)? - Gh0u1L5的回答 - 知乎

程式碼測試

測試了一下最極端的每次的雜湊值都相同和正常插入雜湊值做對比:

public static void main(String[] args) {
Map<TestString, Integer> map = new HashMap();
Long l = System.currentTimeMillis();
System.out.println(l);
for (int i = 0; i < 100000; i++ ) {
map.put(new TestString("ddd" + i), i);
}
System.out.println(System.currentTimeMillis() - l);
Long c = System.currentTimeMillis();
map.get(new TestString("ddd"+5000));
System.out.println("-------" + (System.currentTimeMillis() - c)); Map<TestStringNom, Integer> mapNom = new HashMap();
Long ll = System.currentTimeMillis();
System.out.println(l);
for (int i = 0; i < 100000; i++ ) {
mapNom.put(new TestStringNom("ddd" + i) , i);
}
System.out.println(System.currentTimeMillis() - ll);
Long cc = System.currentTimeMillis();
mapNom.get(map.get(new TestStringNom("ddd5000")));
System.out.println("-------" + (System.currentTimeMillis() - cc));
} public class TestString {
final String val; public TestString(String val) {
this.val = val;
} @Override
public int hashCode() {
super.hashCode();
return 88590;
}
} public class TestStringNom {
final String val; public TestStringNom(String val) {
this.val = val;
} @Override
public int hashCode() {
return super.hashCode();
}
}
數量級 模擬洪水插入時間(ms) 正常插入時間(ms) 模擬洪水查詢時間(ms) 正常查詢時間(ms)
1000 24 1 0 0
10000 749 2 0 0
100000 154941 42 4 0