[JAVA] Convert Map <K, V1> to Map <K, V2> (Convert Map Value)

Overview

How should I write when I want to convert Map values () to another class at once? Let's take a look at a sample that converts Map <String, String> to Map <String, Integer>.

Implementation plan

Proposal 1: Use for statement

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;
}

For the time being, it was written up to Java 7. There is a lot of code that is verbose and non-essential.

Option 2: Use 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())));
}

It's shorter than Plan 1, but it's still a bit long.

Proposal 3: Guava's [Maps :: transformValues](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/Maps.html#transformValues-java.util.Map- Use com.google.common.base.Function-)

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);
        }
    });
}

that? Is it longer than Plan 2? No, since the abstract method is a functional interface, you can use lambda expression.

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

You can also use Method Reference, so you can make it a little shorter.

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

Proposal 4: Guava's [Maps :: transformValues](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/Maps.html#transformValues-java.util.Map- Another solution using com.google.common.base.Function-)

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

Proposal 3 with ImmutableMap :: copyOf Wrapped. The explanation will be described later.

test

Now let's test if it works properly.

Tested class

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);

    /**
     *Proposal 1:Use for statement
     */
    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;
        }
    }

    /**
     *Proposal 2:Use 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())));
        }
    }

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

    /**
     *Proposal 4:Guava Maps::Another solution using 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));
        }
    }

}

Test code skeleton

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()
        );
    }

    //Write various tests here
}

Test if the value has been converted

@Test
public void Is the value converted?() {
    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);

    // [Proposal 1: ForImpl] Passed
    // [Proposal 2: StreamImpl] Passed
    // [Proposal 3: TranformValuesImpl] Passed
    // [Proposal 4: CopyOfTranformValuesImpl] Passed
    assertThat(transformer.strToInt(source), is(expected));
}

I passed it safely.

Test if order is preserved

@Test
public void Is the order maintained?() {
    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);

    // [Proposal 1: ForImpl] Expected: is "[one, zero, min]" but: was "[zero, min, one]"
    // [Proposal 2: StreamImpl] Expected: is "[one, zero, min]" but: was "[zero, min, one]"
    // [Proposal 3: TranformValuesImpl] Passed
    // [Proposal 4: CopyOfTranformValuesImpl] Passed
    assertThat(transformed.keySet().toString(), is(source.keySet().toString()));
}

(Although it is a rough test to toString the result) Plan 1 and Plan 2 have failed.

In the implementation of Plan 1 and Plan 2, it changes to HashMap, so LinkedHashMap and [TreeMap](https://docs.oracle.com/javase/jp/8/ If you pass docs / api / java / util / TreeMap.html), the guarantee of the order is lost.

Immutable test

@Test(expected = UnsupportedOperationException.class)
public void Immutable Is it 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);

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

In case of plan 3 and plan 4, when the put method of the converted Map is called, [UnsupportedOperationException](https://docs.oracle.com/javase/jp/8/docs/api/java/lang/UnsupportedOperationException] .html) is thrown.

Proposals 1 and 2 were Mutable, and Proposals 3 and 4 were Immutable.

Test if Map is copied

@Test
Is the public void Map copied?() {
    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");

    // [Proposal 1: ForImpl] Passed
    // [Proposal 2: StreamImpl] Passed
    // [Proposal 3: TransformValuesImpl]: Expected: is <{one=1}> but: was <{one=1, two=2}>
    // [Proposal 4: CopyOfTranformValuesImpl] Passed
    assertThat(transformed, is(expected));
}

Only in the implementation of Proposal 3, the changes made to the conversion source Map after conversion were reflected in the conversion destination Map.

This is [JavaDoc for Maps :: transformValues](https://guava.dev/releases/snapshot-jre/api/docs/com/google/common/collect/Maps.html#transformValues-java.util.Map- As described in com.google.common.base.Function-), this method "returns a view of Map".

Therefore, if you want a copy (you do not want to reflect the changes to the conversion source Map in the conversion destination), as in Proposal 4, ImmutableMap :: copyOf You need to Wrap with -jre / api / docs / com / google / common / collect / ImmutableMap.html # copyOf-java.util.Map-).

Test result summary

Test perspective Proposal 1 Proposal 2 Proposal 3 Proposal 4
Is the value converted? Yes Yes Yes Yes
Is the order maintained? No No Yes Yes
Immutable(Immutable)Or No No Yes Yes
Is the Map copied? Yes Yes No Yes

If you are aware of Advantages of immutability, the implementation method of Plan 3 or Plan 4 may be better. ?? It seems that.

Reference URL

Recommended Posts

Convert Map <K, V1> to Map <K, V2> (Convert Map Value)
[Java] Convert 1-to-N List to Map
[Core ML] How to convert YOLO v3 to Core ML
[Java] Convert DB code to code value using enum
How to use Map
How to use map
I learned stream (I want to convert List to Map <Integer, List>)
Cannot convert value of type'DocumentSnapshot' to expected argument type'QueryDocumentSnapshot'
How to use Map
For Java beginners: List, Map, Iterator / Array ... How to convert?
[Java] How to use Map
[Java] How to use Map
9 Corresponds to the return value
Convert Java Powerpoint to XPS
I want to convert characters ...
Convert String type to Timestamp type
Convert to Ruby Leet string
Convert Serializable Object to byte []
How to use Java Map
Convert from ○ months to ○ years ○ months
How to convert Java radix
[Java] Convert ArrayList to array
[Android] Convert Map to JSON using GSON in Kotlin and Java
How to increment the value of Map in one line in Java