概要
Mapのvalues()を一括で別のクラスに変換したいとき、どのように書けばよいのでしょうか。
Map<String, String>
をMap<String, Integer>
に変換するサンプルで、説明してみます。
実装案
案1: for文を使う
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を使う
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を使う
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なので、ラムダ式が使えます。
public Map<String, Integer> strToInt(final Map<String, String> map) {
return Maps.transformValues(map, v -> Integer.valueOf(v));
}
ついでにメソッド参照も使えるので、もう少し短くすることができます。
public Map<String, Integer> strToInt(final Map<String, String> map) {
return Maps.transformValues(map, Integer::valueOf);
}
案4: GuavaのMaps::transformValuesを使った別解
public Map<String, Integer> strToInt(final Map<String, String> map) {
return ImmutableMap.copyOf(Maps.transformValues(map, Integer::valueOf));
}
案3をImmutableMap::copyOfでWrapしたものです。説明は後述します。
テスト
さて、正しく動いているかどうかをテストしてみましょう。
テスト対象クラス
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));
}
}
}
テストコードのスケルトン
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()
);
}
// ここに各種テストを書く
}
値が変換されているかどうかのテスト
@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しました。
順序が保持されているかどうかのテスト
@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に変化してしまうため、LinkedHashMapやTreeMapを渡した場合、その順序の保証が失われてしまっています。
Immutable(不変)かどうかのテスト
@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が投げられます。
案1と案2はMutable(可変)、案3と案4はImmutable(不変)という結果になりました。
Mapがコピーされているかどうかのテスト
@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にも記載されている通りですが、このメソッドは「Mapのビューを返す」ためです。
このため、コピーがほしい(変換元のMapに対する変更を、変換先に反映したくない)場合は、案4のように、ImmutableMap::copyOfでWrapする必要があります。
テスト結果まとめ
テスト観点 | 案1 | 案2 | 案3 | 案4 |
---|---|---|---|---|
値が変換されているか | Yes | Yes | Yes | Yes |
順序が保持されているか | No | No | Yes | Yes |
Immutable(不変)か | No | No | Yes | Yes |
Mapがコピーされているか | Yes | Yes | No | Yes |
不変性のメリットを意識した場合、案3または案4の実装方法が良いのでは?と思われます。