How to initialize a map using a lambda?

23,175

Solution 1

Here is how to implement a field initializer in Java 8 in a single statement using a lambda.

private static final Map<Integer,Boolean> map =
        ((Supplier<Map<Integer,Boolean>>)() -> {
            Map<Integer,Boolean> mutableMap = new HashMap<>();
            mutableMap.put( 1, false );
            mutableMap.put( 2, true );
            return Collections.unmodifiableMap( mutableMap );
        }).get();

Java 9 solution:

private static final Map<Integer,Boolean> map = Map.of( 1, false, 2, true );

and if you have more than 10 entries, Map.of() won't work, so you need this:

private static final Map<Integer,Boolean> map = Map.ofEntries( 
        Map.entry( 1, false ), 
        Map.entry( 2, true ), 
        Map.entry( 3, false ), 
        Map.entry( 4, true ), 
        Map.entry( 5, false ), 
        Map.entry( 6, true ), 
        Map.entry( 7, false ), 
        Map.entry( 8, true ), 
        Map.entry( 9, false ), 
        Map.entry( 10, true ), 
        Map.entry( 11, false ) );

Solution 2

If you want to initialize a Map in a single statement, you can use Collectors.toMap.

Imagine you want to build a Map<Integer, Boolean> mapping an integer to the result of calling some function f:

private static final Map<Integer,Boolean> MAP = 
        Collections.unmodifiableMap(IntStream.range(0, 1000)
                                             .boxed()
                                             .collect(Collectors.toMap(i -> i, i -> f(i))));

private static final boolean f(int i) {
    return Math.random() * 100 > i;
}

If you want to initialize it with "static" known values, like the example in your answer, you can abuse the Stream API like this:

private static final Map<Integer, Boolean> MAP = 
       Stream.of(new Object[] { 1, false }, new Object[] { 2, true })
             .collect(Collectors.toMap(s -> (int) s[0], s -> (boolean) s[1]));

Note that this is a real abuse and I personally would never use it: if you want to construct a Map with known static values, there is nothing to gain from using Streams and you would be better off to use a static initializer.

Solution 3

Google Guava's Maps class provides some nice utility methods for this. Additionally, there is the ImmutableMap class and its static methods. Have a look at:

    ImmutableMap.of(key1, value1, key2, value2, ...);
    Maps.uniqueIndex(values, keyExtractor);
    Maps.toMap(keys, valueMapper);

Solution 4

If you really want to do initialize the map in single statement, you can write your custom builder and use it in your project. Something like this:

public class MapBuilder<K, V> {
    private final Map<K, V> map;

    private MapBuilder(Map<K, V> map) {
        this.map = map;
    }

    public MapBuilder<K, V> put(K key, V value) {
        if(map == null)
            throw new IllegalStateException();
        map.put(key, value);
        return this;
    }

    public MapBuilder<K, V> put(Map<? extends K, ? extends V> other) {
        if(map == null)
            throw new IllegalStateException();
        map.putAll(other);
        return this;
    }

    public Map<K, V> build() {
        Map<K, V> m = map;
        map = null;
        return Collections.unmodifiableMap(m);
    }

    public static <K, V> MapBuilder<K, V> unordered() {
        return new MapBuilder<>(new HashMap<>());
    }

    public static <K, V> MapBuilder<K, V> ordered() {
        return new MapBuilder<>(new LinkedHashMap<>());
    }

    public static <K extends Comparable<K>, V> MapBuilder<K, V> sorted() {
        return new MapBuilder<>(new TreeMap<>());
    }

    public static <K, V> MapBuilder<K, V> sorted(Comparator<K> comparator) {
        return new MapBuilder<>(new TreeMap<>(comparator));
    }
}

Usage example:

Map<Integer, Boolean> map = MapBuilder.<Integer, Boolean>unordered()
    .put(1, true).put(2, false).build();

This works in Java-7 as well.

As a side note, we will likely to see something like Map.of(1, true, 2, false) in Java-9. See JDK-8048330 for details.

Share:
23,175
Mike Nakis
Author by

Mike Nakis

"International Man of Mystery" (No, seriously now, you can search for me using my full name "Michael Belivanakis" on facebook, linkedin, etc., and you can even find my personal email address on my website.)

Updated on January 06, 2020

Comments

  • Mike Nakis
    Mike Nakis over 4 years

    I want to declare a fully populated map field in a single statement, (which may contain several nested statements,) like this:

    private static final Map<Integer,Boolean> map = 
        something-returning-an-unmodifiable-fully-populated-HashMap;
    

    Anonymous initializers won't do, for the same reason that invoking a function which returns a new populated map won't do: they require two top-level statements: one for the variable declaration, and one for the method or initializer.

    The double curly bracket ({{ and }}) idiom will work, but it creates a whole new class which extends HashMap<>, and I do not like the overhead represented by this.

    Do the lambdas of Java 8 perhaps offer a better way of accomplishing this?

  • Chii
    Chii over 8 years
    Good solution, however, this only works if the values can be computed. What if it's an enumerated list of arbituary values, which needs to be mapped to a key (not an integer)?
  • Mike Nakis
    Mike Nakis over 8 years
    I was about to ask the same question as @Chii.
  • Tunaki
    Tunaki over 8 years
    @Chii Can you give an example?
  • Mike Nakis
    Mike Nakis over 8 years
    See my answer. Both the keys and the values are provided.
  • Mike Nakis
    Mike Nakis over 8 years
    This is what I was implying in the question, by saying "which may contain several nested statements", though I should have probably made it clearer.
  • Tunaki
    Tunaki over 8 years
    @MikeNakis Edited but I'm not proud of it
  • Holger
    Holger over 8 years
    What’s the advantage of this twisted code over a simple initializer? By the way, you don’t need to create your own interface, you can just use Supplier.
  • Holger
    Holger over 8 years
    Yeah, now that we have a solution, we may search for a problem that it could solve…
  • Alexander
    Alexander almost 7 years
    You should really check for null in the constructor, rather than in every put
  • Tagir Valeev
    Tagir Valeev almost 7 years
    @Alexander, no. Null-check is necessary to prevent using the builder after .build() is called. Null-check in constructor is useless for this. As constructor is private, we fully control that null is not passed to it.
  • Alexander
    Alexander almost 7 years
    "Null-check is necessary to prevent using the builder after .build() is called" Why limit to only building once? I can imagine scenarios (particularly for testing) where I would want to build a series of maps, each with one new k/v pair added to the previous
  • Tagir Valeev
    Tagir Valeev almost 7 years
    @Alexander, as this builder creates mutable Maps, newly-added elements after a single Map is created will modify Map previously returned by build() call. You will need an additional code to copy Map when builder is used after build(), and this should not degrade the performance of common case when only one Map is necessary (i.e. you cannot just copy Map inside build()). This is doable, but somewhat more complex and was unnecessary for this answer.
  • Alexander
    Alexander almost 7 years
    Ah. I made two key mistakes: one, I forgot that Collections.unmodifiableMap is just a wrapper, which doesn't perform a copy. Second, I've been getting spoiled by my recent dive into Swift, where Dictionaries are value types with copy-on-write