Java concurrency集合之ConcurrentSkipListMap_動力節(jié)點Java學(xué)院整理
ConcurrentSkipListMap介紹
ConcurrentSkipListMap是線程安全的有序的哈希表,適用于高并發(fā)的場景。
ConcurrentSkipListMap和TreeMap,它們雖然都是有序的哈希表。但是,第一,它們的線程安全機(jī)制不同,TreeMap是非線程安全的,而ConcurrentSkipListMap是線程安全的。第二,ConcurrentSkipListMap是通過跳表實現(xiàn)的,而TreeMap是通過紅黑樹實現(xiàn)的。
關(guān)于跳表(Skip List),它是平衡樹的一種替代的數(shù)據(jù)結(jié)構(gòu),但是和紅黑樹不相同的是,跳表對于樹的平衡的實現(xiàn)是基于一種隨機(jī)化的算法的,這樣也就是說跳表的插入和刪除的工作是比較簡單的。
ConcurrentSkipListMap原理和數(shù)據(jù)結(jié)構(gòu)
ConcurrentSkipListMap的數(shù)據(jù)結(jié)構(gòu),如下圖所示:

說明:
先以數(shù)據(jù)“7,14,21,32,37,71,85”序列為例,來對跳表進(jìn)行簡單說明。
跳表分為許多層(level),每一層都可以看作是數(shù)據(jù)的索引,這些索引的意義就是加快跳表查找數(shù)據(jù)速度。每一層的數(shù)據(jù)都是有序的,上一層數(shù)據(jù)是下一層數(shù)據(jù)的子集,并且第一層(level 1)包含了全部的數(shù)據(jù);層次越高,跳躍性越大,包含的數(shù)據(jù)越少。
跳表包含一個表頭,它查找數(shù)據(jù)時,是從上往下,從左往右進(jìn)行查找?,F(xiàn)在“需要找出值為32的節(jié)點”為例,來對比說明跳表和普遍的鏈表。
情況1:鏈表中查找“32”節(jié)點
路徑如下圖1-02所示:

需要4步(紅色部分表示路徑)。
情況2:跳表中查找“32”節(jié)點
路徑如下圖1-03所示:

忽略索引垂直線路上路徑的情況下,只需要2步(紅色部分表示路徑)。
下面說說Java中ConcurrentSkipListMap的數(shù)據(jù)結(jié)構(gòu)。
(01) ConcurrentSkipListMap繼承于AbstractMap類,也就意味著它是一個哈希表。
(02) Index是ConcurrentSkipListMap的內(nèi)部類,它與“跳表中的索引相對應(yīng)”。HeadIndex繼承于Index,ConcurrentSkipListMap中含有一個HeadIndex的對象head,head是“跳表的表頭”。
(03) Index是跳表中的索引,它包含“右索引的指針(right)”,“下索引的指針(down)”和“哈希表節(jié)點node”。node是Node的對象,Node也是ConcurrentSkipListMap中的內(nèi)部類。
ConcurrentSkipListMap函數(shù)列表
// 構(gòu)造一個新的空映射,該映射按照鍵的自然順序進(jìn)行排序。 ConcurrentSkipListMap() // 構(gòu)造一個新的空映射,該映射按照指定的比較器進(jìn)行排序。 ConcurrentSkipListMap(Comparator<? super K> comparator) // 構(gòu)造一個新映射,該映射所包含的映射關(guān)系與給定映射包含的映射關(guān)系相同,并按照鍵的自然順序進(jìn)行排序。 ConcurrentSkipListMap(Map<? extends K,? extends V> m) // 構(gòu)造一個新映射,該映射所包含的映射關(guān)系與指定的有序映射包含的映射關(guān)系相同,使用的順序也相同。 ConcurrentSkipListMap(SortedMap<K,? extends V> m) // 返回與大于等于給定鍵的最小鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果不存在這樣的條目,則返回 null。 Map.Entry<K,V> ceilingEntry(K key) // 返回大于等于給定鍵的最小鍵;如果不存在這樣的鍵,則返回 null。 K ceilingKey(K key) // 從此映射中移除所有映射關(guān)系。 void clear() // 返回此 ConcurrentSkipListMap 實例的淺表副本。 ConcurrentSkipListMap<K,V> clone() // 返回對此映射中的鍵進(jìn)行排序的比較器;如果此映射使用鍵的自然順序,則返回 null。 Comparator<? super K> comparator() // 如果此映射包含指定鍵的映射關(guān)系,則返回 true。 boolean containsKey(Object key) // 如果此映射為指定值映射一個或多個鍵,則返回 true。 boolean containsValue(Object value) // 返回此映射中所包含鍵的逆序 NavigableSet 視圖。 NavigableSet<K> descendingKeySet() // 返回此映射中所包含映射關(guān)系的逆序視圖。 ConcurrentNavigableMap<K,V> descendingMap() // 返回此映射中所包含的映射關(guān)系的 Set 視圖。 Set<Map.Entry<K,V>> entrySet() // 比較指定對象與此映射的相等性。 boolean equals(Object o) // 返回與此映射中的最小鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果該映射為空,則返回 null。 Map.Entry<K,V> firstEntry() // 返回此映射中當(dāng)前第一個(最低)鍵。 K firstKey() // 返回與小于等于給定鍵的最大鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果不存在這樣的鍵,則返回 null。 Map.Entry<K,V> floorEntry(K key) // 返回小于等于給定鍵的最大鍵;如果不存在這樣的鍵,則返回 null。 K floorKey(K key) // 返回指定鍵所映射到的值;如果此映射不包含該鍵的映射關(guān)系,則返回 null。 V get(Object key) // 返回此映射的部分視圖,其鍵值嚴(yán)格小于 toKey。 ConcurrentNavigableMap<K,V> headMap(K toKey) // 返回此映射的部分視圖,其鍵小于(或等于,如果 inclusive 為 true)toKey。 ConcurrentNavigableMap<K,V> headMap(K toKey, boolean inclusive) // 返回與嚴(yán)格大于給定鍵的最小鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果不存在這樣的鍵,則返回 null。 Map.Entry<K,V> higherEntry(K key) // 返回嚴(yán)格大于給定鍵的最小鍵;如果不存在這樣的鍵,則返回 null。 K higherKey(K key) // 如果此映射未包含鍵-值映射關(guān)系,則返回 true。 boolean isEmpty() // 返回此映射中所包含鍵的 NavigableSet 視圖。 NavigableSet<K> keySet() // 返回與此映射中的最大鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果該映射為空,則返回 null。 Map.Entry<K,V> lastEntry() // 返回映射中當(dāng)前最后一個(最高)鍵。 K lastKey() // 返回與嚴(yán)格小于給定鍵的最大鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果不存在這樣的鍵,則返回 null。 Map.Entry<K,V> lowerEntry(K key) // 返回嚴(yán)格小于給定鍵的最大鍵;如果不存在這樣的鍵,則返回 null。 K lowerKey(K key) // 返回此映射中所包含鍵的 NavigableSet 視圖。 NavigableSet<K> navigableKeySet() // 移除并返回與此映射中的最小鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果該映射為空,則返回 null。 Map.Entry<K,V> pollFirstEntry() // 移除并返回與此映射中的最大鍵關(guān)聯(lián)的鍵-值映射關(guān)系;如果該映射為空,則返回 null。 Map.Entry<K,V> pollLastEntry() // 將指定值與此映射中的指定鍵關(guān)聯(lián)。 V put(K key, V value) // 如果指定鍵已經(jīng)不再與某個值相關(guān)聯(lián),則將它與給定值關(guān)聯(lián)。 V putIfAbsent(K key, V value) // 從此映射中移除指定鍵的映射關(guān)系(如果存在)。 V remove(Object key) // 只有目前將鍵的條目映射到給定值時,才移除該鍵的條目。 boolean remove(Object key, Object value) // 只有目前將鍵的條目映射到某一值時,才替換該鍵的條目。 V replace(K key, V value) // 只有目前將鍵的條目映射到給定值時,才替換該鍵的條目。 boolean replace(K key, V oldValue, V newValue) // 返回此映射中的鍵-值映射關(guān)系數(shù)。 int size() // 返回此映射的部分視圖,其鍵的范圍從 fromKey 到 toKey。 ConcurrentNavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) // 返回此映射的部分視圖,其鍵值的范圍從 fromKey(包括)到 toKey(不包括)。 ConcurrentNavigableMap<K,V> subMap(K fromKey, K toKey) // 返回此映射的部分視圖,其鍵大于等于 fromKey。 ConcurrentNavigableMap<K,V> tailMap(K fromKey) // 返回此映射的部分視圖,其鍵大于(或等于,如果 inclusive 為 true)fromKey。 ConcurrentNavigableMap<K,V> tailMap(K fromKey, boolean inclusive) // 返回此映射中所包含值的 Collection 視圖。 Collection<V> values()
下面從ConcurrentSkipListMap的添加,刪除,獲取這3個方面對它進(jìn)行分析。
1. 添加
下面以put(K key, V value)為例,對ConcurrentSkipListMap的添加方法進(jìn)行說明。
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
return doPut(key, value, false);
}
實際上,put()是通過doPut()將key-value鍵值對添加到ConcurrentSkipListMap中的。
doPut()的源碼如下:
private V doPut(K kkey, V value, boolean onlyIfAbsent) {
Comparable<? super K> key = comparable(kkey);
for (;;) {
// 找到key的前繼節(jié)點
Node<K,V> b = findPredecessor(key);
// 設(shè)置n為“key的前繼節(jié)點的后繼節(jié)點”,即n應(yīng)該是“插入節(jié)點”的“后繼節(jié)點”
Node<K,V> n = b.next;
for (;;) {
if (n != null) {
Node<K,V> f = n.next;
// 如果兩次獲得的b.next不是相同的Node,就跳轉(zhuǎn)到”外層for循環(huán)“,重新獲得b和n后再遍歷。
if (n != b.next)
break;
// v是“n的值”
Object v = n.value;
// 當(dāng)n的值為null(意味著其它線程刪除了n);此時刪除b的下一個節(jié)點,然后跳轉(zhuǎn)到”外層for循環(huán)“,重新獲得b和n后再遍歷。
if (v == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果其它線程刪除了b;則跳轉(zhuǎn)到”外層for循環(huán)“,重新獲得b和n后再遍歷。
if (v == n || b.value == null) // b is deleted
break;
// 比較key和n.key
int c = key.compareTo(n.key);
if (c > 0) {
b = n;
n = f;
continue;
}
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value))
return (V)v;
else
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
// 新建節(jié)點(對應(yīng)是“要插入的鍵值對”)
Node<K,V> z = new Node<K,V>(kkey, value, n);
// 設(shè)置“b的后繼節(jié)點”為z
if (!b.casNext(n, z))
break; // 多線程情況下,break才可能發(fā)生(其它線程對b進(jìn)行了操作)
// 隨機(jī)獲取一個level
// 然后在“第1層”到“第level層”的鏈表中都插入新建節(jié)點
int level = randomLevel();
if (level > 0)
insertIndex(z, level);
return null;
}
}
}
說明:doPut() 的作用就是將鍵值對添加到“跳表”中。
要想搞清doPut(),首先要弄清楚它的主干部分 —— 我們先單純的只考慮“單線程的情況下,將key-value添加到跳表中”,即忽略“多線程相關(guān)的內(nèi)容”。它的流程如下:
第1步:找到“插入位置”。
即,找到“key的前繼節(jié)點(b)”和“key的后繼節(jié)點(n)”;key是要插入節(jié)點的鍵。
第2步:新建并插入節(jié)點。
即,新建節(jié)點z(key對應(yīng)的節(jié)點),并將新節(jié)點z插入到“跳表”中(設(shè)置“b的后繼節(jié)點為z”,“z的后繼節(jié)點為n”)。
第3步:更新跳表。
即,隨機(jī)獲取一個level,然后在“跳表”的第1層~第level層之間,每一層都插入節(jié)點z;在第level層之上就不再插入節(jié)點了。若level數(shù)值大于“跳表的層次”,則新建一層。
主干部分“對應(yīng)的精簡后的doPut()的代碼”如下(僅供參考):
private V doPut(K kkey, V value, boolean onlyIfAbsent) {
Comparable<? super K> key = comparable(kkey);
for (;;) {
// 找到key的前繼節(jié)點
Node<K,V> b = findPredecessor(key);
// 設(shè)置n為key的后繼節(jié)點
Node<K,V> n = b.next;
for (;;) {
// 新建節(jié)點(對應(yīng)是“要被插入的鍵值對”)
Node<K,V> z = new Node<K,V>(kkey, value, n);
// 設(shè)置“b的后繼節(jié)點”為z
b.casNext(n, z);
// 隨機(jī)獲取一個level
// 然后在“第1層”到“第level層”的鏈表中都插入新建節(jié)點
int level = randomLevel();
if (level > 0)
insertIndex(z, level);
return null;
}
}
}
理清主干之后,剩余的工作就相對簡單了。主要是上面幾步的對應(yīng)算法的具體實現(xiàn),以及多線程相關(guān)情況的處理!
2. 刪除
下面以remove(Object key)為例,對ConcurrentSkipListMap的刪除方法進(jìn)行說明。
public V remove(Object key) {
return doRemove(key, null);
}
實際上,remove()是通過doRemove()將ConcurrentSkipListMap中的key對應(yīng)的鍵值對刪除的。
doRemove()的源碼如下:
final V doRemove(Object okey, Object value) {
Comparable<? super K> key = comparable(okey);
for (;;) {
// 找到“key的前繼節(jié)點”
Node<K,V> b = findPredecessor(key);
// 設(shè)置n為“b的后繼節(jié)點”(即若key存在于“跳表中”,n就是key對應(yīng)的節(jié)點)
Node<K,V> n = b.next;
for (;;) {
if (n == null)
return null;
// f是“當(dāng)前節(jié)點n的后繼節(jié)點”
Node<K,V> f = n.next;
// 如果兩次讀取到的“b的后繼節(jié)點”不同(其它線程操作了該跳表),則返回到“外層for循環(huán)”重新遍歷。
if (n != b.next) // inconsistent read
break;
// 如果“當(dāng)前節(jié)點n的值”變?yōu)閚ull(其它線程操作了該跳表),則返回到“外層for循環(huán)”重新遍歷。
Object v = n.value;
if (v == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果“前繼節(jié)點b”被刪除(其它線程操作了該跳表),則返回到“外層for循環(huán)”重新遍歷。
if (v == n || b.value == null) // b is deleted
break;
int c = key.compareTo(n.key);
if (c < 0)
return null;
if (c > 0) {
b = n;
n = f;
continue;
}
// 以下是c=0的情況
if (value != null && !value.equals(v))
return null;
// 設(shè)置“當(dāng)前節(jié)點n”的值為null
if (!n.casValue(v, null))
break;
// 設(shè)置“b的后繼節(jié)點”為f
if (!n.appendMarker(f) || !b.casNext(n, f))
findNode(key); // Retry via findNode
else {
// 清除“跳表”中每一層的key節(jié)點
findPredecessor(key); // Clean index
// 如果“表頭的右索引為空”,則將“跳表的層次”-1。
if (head.right == null)
tryReduceLevel();
}
return (V)v;
}
}
}
說明:doRemove()的作用是刪除跳表中的節(jié)點。
和doPut()一樣,我們重點看doRemove()的主干部分,了解主干部分之后,其余部分就非常容易理解了。下面是“單線程的情況下,刪除跳表中鍵值對的步驟”:
第1步:找到“被刪除節(jié)點的位置”。
即,找到“key的前繼節(jié)點(b)”,“key所對應(yīng)的節(jié)點(n)”,“n的后繼節(jié)點f”;key是要刪除節(jié)點的鍵。
第2步:刪除節(jié)點。
即,將“key所對應(yīng)的節(jié)點n”從跳表中移除 -- 將“b的后繼節(jié)點”設(shè)為“f”!
第3步:更新跳表。
即,遍歷跳表,刪除每一層的“key節(jié)點”(如果存在的話)。如果刪除“key節(jié)點”之后,跳表的層次需要-1;則執(zhí)行相應(yīng)的操作!
主干部分“對應(yīng)的精簡后的doRemove()的代碼”如下(僅供參考):
final V doRemove(Object okey, Object value) {
Comparable<? super K> key = comparable(okey);
for (;;) {
// 找到“key的前繼節(jié)點”
Node<K,V> b = findPredecessor(key);
// 設(shè)置n為“b的后繼節(jié)點”(即若key存在于“跳表中”,n就是key對應(yīng)的節(jié)點)
Node<K,V> n = b.next;
for (;;) {
// f是“當(dāng)前節(jié)點n的后繼節(jié)點”
Node<K,V> f = n.next;
// 設(shè)置“當(dāng)前節(jié)點n”的值為null
n.casValue(v, null);
// 設(shè)置“b的后繼節(jié)點”為f
b.casNext(n, f);
// 清除“跳表”中每一層的key節(jié)點
findPredecessor(key);
// 如果“表頭的右索引為空”,則將“跳表的層次”-1。
if (head.right == null)
tryReduceLevel();
return (V)v;
}
}
}
3. 獲取
下面以get(Object key)為例,對ConcurrentSkipListMap的獲取方法進(jìn)行說明。
public V get(Object key) {
return doGet(key);
}
doGet的源碼如下:
private V doGet(Object okey) {
Comparable<? super K> key = comparable(okey);
for (;;) {
// 找到“key對應(yīng)的節(jié)點”
Node<K,V> n = findNode(key);
if (n == null)
return null;
Object v = n.value;
if (v != null)
return (V)v;
}
}
說明:doGet()是通過findNode()找到并返回節(jié)點的。
private Node<K,V> findNode(Comparable<? super K> key) {
for (;;) {
// 找到key的前繼節(jié)點
Node<K,V> b = findPredecessor(key);
// 設(shè)置n為“b的后繼節(jié)點”(即若key存在于“跳表中”,n就是key對應(yīng)的節(jié)點)
Node<K,V> n = b.next;
for (;;) {
// 如果“n為null”,則跳轉(zhuǎn)中不存在key對應(yīng)的節(jié)點,直接返回null。
if (n == null)
return null;
Node<K,V> f = n.next;
// 如果兩次讀取到的“b的后繼節(jié)點”不同(其它線程操作了該跳表),則返回到“外層for循環(huán)”重新遍歷。
if (n != b.next) // inconsistent read
break;
Object v = n.value;
// 如果“當(dāng)前節(jié)點n的值”變?yōu)閚ull(其它線程操作了該跳表),則返回到“外層for循環(huán)”重新遍歷。
if (v == null) { // n is deleted
n.helpDelete(b, f);
break;
}
if (v == n || b.value == null) // b is deleted
break;
// 若n是當(dāng)前節(jié)點,則返回n。
int c = key.compareTo(n.key);
if (c == 0)
return n;
// 若“節(jié)點n的key”小于“key”,則說明跳表中不存在key對應(yīng)的節(jié)點,返回null
if (c < 0)
return null;
// 若“節(jié)點n的key”大于“key”,則更新b和n,繼續(xù)查找。
b = n;
n = f;
}
}
}
說明:findNode(key)的作用是在返回跳表中key對應(yīng)的節(jié)點;存在則返回節(jié)點,不存在則返回null。
先弄清函數(shù)的主干部分,即拋開“多線程相關(guān)內(nèi)容”,單純的考慮單線程情況下,從跳表獲取節(jié)點的算法。
第1步:找到“被刪除節(jié)點的位置”。
根據(jù)findPredecessor()定位key所在的層次以及找到key的前繼節(jié)點(b),然后找到b的后繼節(jié)點n。
第2步:根據(jù)“key的前繼節(jié)點(b)”和“key的前繼節(jié)點的后繼節(jié)點(n)”來定位“key對應(yīng)的節(jié)點”。
具體是通過比較“n的鍵值”和“key”的大小。如果相等,則n就是所要查找的鍵。
ConcurrentSkipListMap示例
import java.util.*;
import java.util.concurrent.*;
/*
* ConcurrentSkipListMap是“線程安全”的哈希表,而TreeMap是非線程安全的。
*
* 下面是“多個線程同時操作并且遍歷map”的示例
* (01) 當(dāng)map是ConcurrentSkipListMap對象時,程序能正常運行。
* (02) 當(dāng)map是TreeMap對象時,程序會產(chǎn)生ConcurrentModificationException異常。
*
* @author skywang
*/
public class ConcurrentSkipListMapDemo1 {
// TODO: map是TreeMap對象時,程序會出錯。
//private static Map<String, String> map = new TreeMap<String, String>();
private static Map<String, String> map = new ConcurrentSkipListMap<String, String>();
public static void main(String[] args) {
// 同時啟動兩個線程對map進(jìn)行操作!
new MyThread("a").start();
new MyThread("b").start();
}
private static void printAll() {
String key, value;
Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
key = (String)entry.getKey();
value = (String)entry.getValue();
System.out.print("("+key+", "+value+"), ");
}
System.out.println();
}
private static class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
int i = 0;
while (i++ < 6) {
// “線程名” + "序號"
String val = Thread.currentThread().getName()+i;
map.put(val, "0");
// 通過“Iterator”遍歷map。
printAll();
}
}
}
}
(某一次)運行結(jié)果:
(a1, 0), (a1, 0), (b1, 0), (b1, 0), (a1, 0), (b1, 0), (b2, 0), (a1, 0), (a1, 0), (a2, 0), (a2, 0), (b1, 0), (b1, 0), (b2, 0), (b2, 0), (b3, 0), (b3, 0), (a1, 0), (a2, 0), (a3, 0), (a1, 0), (b1, 0), (a2, 0), (b2, 0), (a3, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (a1, 0), (b3, 0), (a2, 0), (b4, 0), (a3, 0), (a1, 0), (a4, 0), (a2, 0), (b1, 0), (a3, 0), (b2, 0), (a4, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (a1, 0), (b4, 0), (a2, 0), (b5, 0), (a3, 0), (a1, 0), (a4, 0), (a2, 0), (a5, 0), (a3, 0), (b1, 0), (a4, 0), (b2, 0), (a5, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (b6, 0), (b4, 0), (a1, 0), (b5, 0), (a2, 0), (b6, 0), (a3, 0), (a4, 0), (a5, 0), (a6, 0), (b1, 0), (b2, 0), (b3, 0), (b4, 0), (b5, 0), (b6, 0),
結(jié)果說明:
示例程序中,啟動兩個線程(線程a和線程b)分別對ConcurrentSkipListMap進(jìn)行操作。以線程a而言,它會先獲取“線程名”+“序號”,然后將該字符串作為key,將“0”作為value,插入到ConcurrentSkipListMap中;接著,遍歷并輸出ConcurrentSkipListMap中的全部元素。 線程b的操作和線程a一樣,只不過線程b的名字和線程a的名字不同。
當(dāng)map是ConcurrentSkipListMap對象時,程序能正常運行。如果將map改為TreeMap時,程序會產(chǎn)生ConcurrentModificationException異常。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Java集合定義與用法實例總結(jié)【Set、List與Map】
- Java中對list map根據(jù)map某個key值進(jìn)行排序的方法
- 詳解javaweb中jstl如何循環(huán)List中的Map數(shù)據(jù)
- java中快速創(chuàng)建帶初始值的List和Map實例
- Java Builder模式構(gòu)建MAP/LIST的實例講解
- java中List、Array、Map、Set等集合相互轉(zhuǎn)換
- 詳解Java中l(wèi)ist,set,map的遍歷與增強(qiáng)for循環(huán)
- java中實現(xiàn)list或set轉(zhuǎn)map的方法
- java 三種將list轉(zhuǎn)換為map的方法詳解
- java list,set,map,數(shù)組間的相互轉(zhuǎn)換詳解
- Java如何在List或Map遍歷過程中刪除元素
- java List循環(huán)與Map循環(huán)的總結(jié)
- 一段代碼搞懂關(guān)于Java中List、Set集合及Map的使用
- Java集合Set、List、Map的遍歷方法
- Java三種方法將List轉(zhuǎn)換為Map的實例
相關(guān)文章
logback ThrowableProxyConverter類源碼流程解析
這篇文章主要為大家介紹了logback ThrowableProxyConverter類源碼流程解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
使用@DS輕松解決動態(tài)數(shù)據(jù)源的問題
這篇文章主要介紹了使用@DS輕松解決動態(tài)數(shù)據(jù)源的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05
Java中的權(quán)限修飾符(protected)示例詳解
這篇文章主要給大家介紹了關(guān)于Java中權(quán)限修飾符(protected)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
Java 利用枚舉實現(xiàn)接口進(jìn)行統(tǒng)一管理
這篇文章主要介紹了Java 利用枚舉實現(xiàn)接口進(jìn)行統(tǒng)一管理,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02
mybatis-plus 關(guān)于savebatch,saveorupdatebatch遇到的坑及解決辦法
本文主要介紹了mybatis-plus 關(guān)于savebatch,saveorupdatebatch遇到的坑及解決辦法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
SpringMVC中的HandlerMappingIntrospector工具類詳解
這篇文章主要介紹了SpringMVC中的HandlerMappingIntrospector工具類詳解,這是一個Spring MVC助手類,用于集合應(yīng)用所配置的HandlerMapping(url pattern和請求處理handler之間的映射)表,用于獲取針對某個請求的如下信息,需要的朋友可以參考下2023-12-12

