JDK8 的 API 有不少便利的預設方法,其中像是 Iterable
、Stream
等,在一些討論 JDK8 的文件,多半都有介紹過,實際上,Map
上也有一些不錯的預設方法可以使用。
forEach
在過去如果要同時迭代 Map
的鍵值,可能會是如下:
public static void main(String[] args) {
Map<String, String> enChMap = new TreeMap<>();
enChMap.put("one", "一");
enChMap.put("two", "二");
enChMap.put("three", "三");
foreach(enChMap.entrySet());
}
static void foreach(Iterable<Map.Entry<String, String>> iterable) {
for(Map.Entry<String, String> entry: iterable) {
out.printf("(鍵 %s, 值 %s)%n",
entry.getKey(), entry.getValue());
}
}
主要是透過 entrySet
傳回 Set<Map.Entry<K,V>>
,不過泛型使得可讀性變差了,撰寫程式還是得兼顧可讀性,在 JDK8 中,可以透過 Map
的 forEach
來取得可讀性:
Map<String, String> enChMap = new TreeMap<>();
enChMap.put("one", "一");
enChMap.put("two", "二");
enChMap.put("three", "三");
enChMap.forEach(
(key, value) -> out.printf("(鍵 %s, 值 %s)%n", key, value)
);
範例中看到的 forEach
方法是定義在 Map
介面上,兩個參數分別接受每次迭代 Map
而得的鍵與值,結合 Lambda 表示式可獲得不錯的可讀性。
getOrDefault,putIfAbsent
Map
的 get
在鍵對應的值不存在時會傳回 null
,因此過去總是會寫以下的檢查程式碼:
String ch = enChMap.get(en);
if(ch == null) {
ch = "Unknown";
}
在 JDK8 中可以改為:
String ch = enChMap.getOrDefault(en, "Unknown");
getOrDefault
只會單純傳回指定的值,如果同時希望鍵不存在時,以指定的值置入並傳回該值,可以使用 putIfAbsent
,例如若有段程式碼如下:
V v = map.get(key);
if(v == null) {
v = map.put(key, value);
}
return v;
JDK8 中可以直接改寫為:
return map.putIfAbsent(key, value);
computeIfAbsent、computeIfPresent、compute
有時會想要檢查鍵是否有對應的值,若不存在時將為鍵設定對應的值,這時可以使用 pubIfAbsent
,例如,也許你想實現一個簡易快取:
static Map<Integer, Integer> cache = new HashMap<>();
static int primeNumberOf(int nth) {
Integer prime = cache.get(nth);
if(prime == null) {
prime = calculatePrime(nth); // calculatePrime 實際計算第 n 質數
cache.put(nth, prime);
}
return prime;
}
使用 JDK8 的話,你可以改為:
static int primeNumberOf(int nth) {
return cache.computeIfAbsent(nth, key -> calculatePrime(key));
}
computeIfAbsent
會在鍵沒有對應的值時,進行指定的 Lambda 運算,並將結果設定為鍵的對應值同時傳回,也就是說它做了類似以下的動作:
if(map.get(key) == null) {
V newValue = mappingFunction.apply(key);
if(newValue != null) {
map.put(key, newValue);
}
}
computeIfPresent
則會在鍵有對應值時進行指定的 Lambda 運算,Lambda 會有兩個參數,傳回值若不為 null
,會用傳回值取代原本鍵對應的值,傳回值若為 null
,原本鍵對應的值會被移除,也就是它進行了類似以下的動作:
if(map.get(key) != null) {
V oldValue = map.get(key);
V newValue = remappingFunction.apply(key, oldValue);
if(newValue != null) {
map.put(key, newValue);
} else {
map.remove(key);
}
}
computeIfAbsent
、computeIfPresent
、 指定的 Lambda 是 惰性求值 的概念,只有在條件成立下,才會執行指定的 Lambda 運算。
compute、merge
compute
做的事更多一些,你可以指定鍵,用指定的 Lambda 運算來決定鍵的對應值,這是它之所以命名為 compute
的原因,例如 API 文件上的例子:
map.compute(key, (k, v) -> (v == null) ? msg : v.concat(msg))
鍵有對應的值時,Lambda 的傳回值若不為 null
,以新值取代舊值,若傳回值為 null
,將鍵對應的舊值移除;若鍵沒有對應的值,Lambda 的傳回值若不為 null
,作為鍵對應的值,否則傳回 null
,也就是做了類似以下的事:
V oldValue = map.get(key);
V newValue = remappingFunction.apply(key, oldValue);
if(oldValue != null ) {
if(newValue != null) {
map.put(key, newValue);
} else {
map.remove(key);
}
} else {
if (newValue != null) {
map.put(key, newValue);
} else {
return null;
}
}
merge
方法比 compute
多了一個參數,可以指定 value
,取名為 merge
,表示鍵對應的值由 value
或指定的 Lambda 運算來決定,例如 API 文件上的例子:
map.merge(key, msg, String::concat)
key
有對應的訊息時,用 value
取代,否則用 Lambda 計算出新值,如果新值不為 null
,取代舊值,否則移除舊值,也就是相當於做了以下類似的事情:
V oldValue = map.get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if(newValue == null) {
map.remove(key);
} else {
map.put(key, newValue);
}
remove、replace、replaceAll
Map
上有了新的 remove
重載版本,可以同時指定鍵值,如果鍵值都符合才會移除,並傳回 boolean
值代表是否移除,也就是 return map.remove(key, value)
可用來取代以下情況:
if(map.containsKey(key) && Objects.equals(map.get(key), value)) {
map.remove(key);
return true;
} else {
return false;
}
類似地,Map
上有個新的 replace
方法可以同時指定鍵值,如果鍵值都符合才會用指定新值取代,並傳回 boolean
值代表是否取代,也就是 return map.replace(key, oldValue, newValue)
可用來取代以下情況:
if(map.containsKey(key) && Objects.equals(map.get(key), oldValue)) {
map.put(key, newValue);
return true;
} else {
return false;
}
replaceAll
可以讓你指定 Lambda,它會迭代所有鍵值,並傳入 Lambda,由 Lambda 來決定值的結果。