Java: Map<K, V1>をMap<K, V2>に変換する(MapのValueを変換する)

# 概要

Mapのvalues()を一括で別のクラスに変換したいとき、どのように書けばよいのでしょうか。
`Map<String, String>`を`Map<String, Integer>` に変換するサンプルで、説明してみます。

# 実装案

## 案1: for文を使う
```java
public Map<String, Integer> strToInt(final Map<String, String> map) {
    final Map<String, Integer> ret = new HashMap<>();
    for (final Map.Entry<String, String> entry : map.entrySet()) {
        ret.put(entry.getKey(), Integer.valueOf(entry.getValue()));
    }
    return ret;
}
```

とりあえずJava 7までの書き方で書いたものです。冗長であり、本質的でないコードが多いですね。

## 案2:  Java 8 Stream APIを使う

```java
public Map<String, Integer> strToInt(final Map<String, String> map) {
    return map.entrySet().stream()
        .collect(Collectors.toMap(Map.Entry::getKey, e -> Integer.valueOf(e.getValue())));
}
```

案1よりは短くなったけれど、まだ、ちょっと長いですね。

## 案3: Guavaの[Maps::transformValues](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/Maps.html#transformValues-java.util.Map-com.google.common.base.Function-)を使う

```java
public Map<String, Integer> strToInt(final Map<String, String> map) {
    return Maps.transformValues(map, new Function<String, Integer>() {
        @Override
        public Integer apply(String input) {
            return Integer.valueOf(input);
        }
    });
}
```

あれ？案2より長くなりましたか？
いいえ、抽象メソッドが一つの関数型interfaceなので、[ラムダ式](https://qiita.com/sano1202/items/64593e8e981e8d6439d3)が使えます。

```java
public Map<String, Integer> strToInt(final Map<String, String> map) {
    return Maps.transformValues(map, v -> Integer.valueOf(v));
}
```

ついでに[メソッド参照](https://qiita.com/kws9/items/d1cfbcb483be87bf25e7)も使えるので、もう少し短くすることができます。

```java
public Map<String, Integer> strToInt(final Map<String, String> map) {
    return Maps.transformValues(map, Integer::valueOf);
}
```

## 案4: Guavaの[Maps::transformValues](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/Maps.html#transformValues-java.util.Map-com.google.common.base.Function-)を使った別解

```java
public Map<String, Integer> strToInt(final Map<String, String> map) {
    return ImmutableMap.copyOf(Maps.transformValues(map, Integer::valueOf));
}
```

案3を[ImmutableMap::copyOf](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/ImmutableMap.html#copyOf-java.util.Map-)でWrapしたものです。説明は後述します。

# テスト

さて、正しく動いているかどうかをテストしてみましょう。

## テスト対象クラス

```java
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;

interface MapValuesTransformer {

    Map<String, Integer> strToInt(final Map<String, String> map);

    /**
     * 案1: for文を使う
     */
    static class ForImpl implements MapValuesTransformer {
        @Override
        public Map<String, Integer> strToInt(final Map<String, String> map) {
            final Map<String, Integer> ret = new HashMap<>();
            for (final Map.Entry<String, String> entry : map.entrySet()) {
                ret.put(entry.getKey(), Integer.valueOf(entry.getValue()));
            }
            return ret;
        }
    }

    /**
     * 案2:  Java 8 Stream APIを使う
     */
    static class StreamImpl implements MapValuesTransformer {
        @Override
        public Map<String, Integer> strToInt(final Map<String, String> map) {
            return map.entrySet().stream()
                    .collect(Collectors.toMap(Map.Entry::getKey, e -> Integer.valueOf(e.getValue())));
        }
    }

    /**
     * 案3: GuavaのMaps::transformValuesを使う
     */
    static class TranformValuesImpl implements MapValuesTransformer {
        @Override
        public Map<String, Integer> strToInt(final Map<String, String> map) {
            return Maps.transformValues(map, Integer::valueOf);
        }
    }

    /**
     * 案4: GuavaのMaps::transformValuesを使った別解
     */
    static class CopyOfTranformValuesImpl implements MapValuesTransformer {
        @Override
        public Map<String, Integer> strToInt(final Map<String, String> map) {
            return ImmutableMap.copyOf(Maps.transformValues(map, Integer::valueOf));
        }
    }

}
```

## テストコードのスケルトン

```java
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class MapValuesTramsformerTest {

    private final MapValuesTransformer transformer;

    public MapValuesTramsformerTest(MapValuesTransformer transformer) {
        this.transformer = transformer;
    }

    @Parameters(name = "{0}")
    public static Iterable<MapValuesTransformer> testClasses() {
        return Arrays.asList(
                new MapValuesTransformer.ForImpl(),
                new MapValuesTransformer.StreamImpl(),
                new MapValuesTransformer.TranformValuesImpl(),
                new MapValuesTransformer.CopyOfTranformValuesImpl()
        );
    }

    // ここに各種テストを書く
}
```

## 値が変換されているかどうかのテスト
```java
@Test
public void 値が変換されているか() {
    Map<String, String> source = new HashMap<>();
    source.put("one", "1");
    source.put("zero", "0");
    source.put("min", "-2147483648");

    Map<String, Integer> expected = new HashMap<>();
    expected.put("one", 1);
    expected.put("zero", 0);
    expected.put("min", Integer.MIN_VALUE);

    // [案1: ForImpl] Passed
    // [案2: StreamImpl] Passed
    // [案3: TranformValuesImpl] Passed
    // [案4: CopyOfTranformValuesImpl] Passed
    assertThat(transformer.strToInt(source), is(expected));
}
```
無事にPassしました。

## 順序が保持されているかどうかのテスト
```java
@Test
public void 順序が保持されているか() {
    Map<String, String> source = new LinkedHashMap<>();
    source.put("one", "1");
    source.put("zero", "0");
    source.put("min", "-2147483648");

    Map<String, Integer> transformed = transformer.strToInt(source);

    // [案1: ForImpl] Expected: is "[one, zero, min]" but: was "[zero, min, one]"
    // [案2: StreamImpl] Expected: is "[one, zero, min]" but: was "[zero, min, one]"
    // [案3: TranformValuesImpl] Passed
    // [案4: CopyOfTranformValuesImpl] Passed
    assertThat(transformed.keySet().toString(), is(source.keySet().toString()));
}
```
(結果をtoStringするという雑なテストですが、)案1と案2ではFailedとなってしまいました。

案1や案2の実装では[HashMap](https://docs.oracle.com/javase/jp/8/docs/api/java/util/HashMap.html)に変化してしまうため、[LinkedHashMap](https://docs.oracle.com/javase/jp/8/docs/api/java/util/LinkedHashMap.html)や[TreeMap](https://docs.oracle.com/javase/jp/8/docs/api/java/util/TreeMap.html)を渡した場合、その順序の保証が失われてしまっています。

## Immutable(不変)かどうかのテスト
```java
@Test(expected = UnsupportedOperationException.class)
public void Immutable不変か() {
    Map<String, String> source = new HashMap<>();
    source.put("one", "1");

    Map<String, Integer> expected = new HashMap<>();
    expected.put("one", 1);

    Map<String, Integer> transformed = transformer.strToInt(source);

    // [案1: ForImpl] Failed
    // [案2: StreamImpl] Failed
    // [案3: TranformValuesImpl] Passed
    // [案4: CopyOfTranformValuesImpl] Passed
    transformed.put("two", 2);
}
```

案3と案4の場合、変換後のMapのputメソッドを呼び出すと、実行時に[UnsupportedOperationException](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/UnsupportedOperationException.html)が投げられます。

案1と案2はMutable(可変)、案3と案4はImmutable(不変)という結果になりました。

## Mapがコピーされているかどうかのテスト
```java
@Test
public void Mapがコピーされているか() {
    Map<String, String> source = new HashMap<>();
    source.put("one", "1");

    Map<String, Integer> expected = new HashMap<>();
    expected.put("one", 1);

    Map<String, Integer> transformed = transformer.strToInt(source);
    source.put("two", "2");

    // [案1: ForImpl] Passed
    // [案2: StreamImpl] Passed
    // [案3: TransformValuesImpl]: Expected: is <{one=1}> but: was <{one=1, two=2}>
    // [案4: CopyOfTranformValuesImpl] Passed
    assertThat(transformed, is(expected));
}
```
案3の実装のみ、変換後に、変換元のMapに加えた変更が、変換先のMapにも反映されました。

これは、[Maps::transformValuesのJavaDoc](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/Maps.html#transformValues-java.util.Map-com.google.common.base.Function-)にも記載されている通りですが、このメソッドは「Mapのビューを返す」ためです。

このため、コピーがほしい（変換元のMapに対する変更を、変換先に反映したくない）場合は、案4のように、[ImmutableMap::copyOf](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/ImmutableMap.html#copyOf-java.util.Map-)でWrapする必要があります。

## テスト結果まとめ

|テスト観点|案1|案2|案3|案4|
|----|----|----|----|----|
|値が変換されているか|Yes|Yes|Yes|Yes|
|順序が保持されているか|No|No|Yes|Yes|
|Immutable(不変)か|No|No|Yes|Yes|
|Mapがコピーされているか|Yes|Yes|No|Yes|

[不変性のメリット](https://www.ibm.com/developerworks/jp/java/library/j-jtp02183/index.html)を意識した場合、案3または案4の実装方法が良いのでは？と思われます。

# 参考URL

- [In Java 8 how do I transform a Map<K,V> to another Map<K,V> using a lambda?](https://stackoverflow.com/questions/22742974/in-java-8-how-do-i-transform-a-mapk-v-to-another-mapk-v-using-a-lambda)