【JDK8】Map 便利的預設方法


JDK8 的 API 有不少便利的預設方法,其中像是 IterableStream 等,在一些討論 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 中,可以透過 MapforEach 來取得可讀性:

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

Mapget 在鍵對應的值不存在時會傳回 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);
    }
}

computeIfAbsentcomputeIfPresent、 指定的 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 來決定值的結果。