5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-30

概要

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に変化してしまうため、LinkedHashMapTreeMapを渡した場合、その順序の保証が失われてしまっています。

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の実装方法が良いのでは?と思われます。

参考URL

5
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?