如何使用lambda表达式初始化一个map?

10

我想要在单个语句中声明一个完全填充的映射字段,(该语句可能包含多个嵌套语句,)就像这样:

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

匿名初始化程序不行,原因与调用返回新填充地图的函数一样不行:它们需要两个顶级语句:一个变量声明和一个方法或初始化器。

双大括号({{}})习惯用法可行,但它创建了一个扩展 HashMap<> 的全新类,我不喜欢这样带来的开销。

Java 8的lambda函数也许提供了更好的实现方式吗?


2
这样做有任何合理的理由吗?对于一个Map来说,无论你写 Type variable = expression; 或者 Type variable; { variable = expression; },它都会创建完全相同的代码。 - Holger
这两个独立的语句在语法上没有绑定在一起,因此它们可以通过复制粘贴或意外插入其他内容来分开。我想将它们合并为一个语句,以获得单个语句所带来的整洁舒适感。 - Mike Nakis
1
即使在它们之间插入了一些语句,它也不会破坏或改变语义。对于final字段,如果有人删除了初始化程序或添加了第二个,编译器将立即报错。但是,如果您认为这太脆弱,仍然可以使用方法。然后,在用作初始化程序时,调用与变量绑定,而方法应命名为其创建的内容,因此不需要绑定到使用它的变量。 - Holger
@Holger 在所有方面都是正确的。但是假设我们想要一个漂亮而紧凑的单语句的感觉。 - Mike Nakis
2
好的,你有一些解决方案。虽然我不确定它们是否会产生舒适的感觉... - Holger
4个回答

12

如果您想在单个语句中初始化一个Map,可以使用Collectors.toMap

假设您想要构建一个将整数映射到调用某些函数f的结果的Map<Integer, Boolean>

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

如果您想使用“静态”已知的值进行初始化,就像您回答中的示例一样,您可以像这样滥用Stream API:

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

请注意,这是真正的滥用,我个人永远不会使用它:如果您想构造一个带有已知静态值的Map,使用流没有任何收益,最好使用静态初始化程序。


1
好的解决方案,但仅适用于可以计算值的情况。如果它是任意值的枚举列表,需要映射到键(而不是整数),该怎么办? - Chii
这就是我在问题中所暗示的,通过说“可能包含多个嵌套语句”,尽管我应该更清楚地表达。 - Mike Nakis
1
@MikeNakis 编辑过,但我并不自豪。 - Tunaki
5
现在我们有了解决方案,就可以寻找它能够解决的问题了... - Holger
1
Java 9 提供了用于创建 Map 的工厂方法,而不是您所要求的“使用 lambda 初始化 Map”,这远非一行代码。正如我在七年前的这条评论中所写:“使用方法仍然没有错”,这正是 Java 9 给您的——一个方法。没有过度复杂的 lambda。 - Holger
显示剩余4条评论

4
以下是使用lambda在Java 8中实现字段初始化器的方法:
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 解决方案:

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

如果您有超过10个条目,Map.of() 将无法工作,因此您需要使用以下代码:

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

4

Google Guava的Maps类提供了一些很好的实用方法。 此外,还有ImmutableMap类及其静态方法。 请参阅:

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

4

如果你真的想在单个语句中初始化地图,你可以编写自定义构建器并在项目中使用它。就像这样:

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

使用示例:

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

这在Java-7中同样有效。

顺便提一下,在Java-9中我们很可能会看到类似Map.of(1, true, 2, false)的东西。详情请参见JDK-8048330


你应该在构造函数中检查 null,而不是在每个 put 中检查。 - Alexander
@Alexander,不需要空值检查来防止在调用.build()之后使用构建器。构造函数中的空值检查对此无用。由于构造函数是私有的,我们完全控制不会将空值传递给它。 - Tagir Valeev
"在调用 .build() 后,进行空值检查是必要的,以防止继续使用构建器。为什么限制只能构建一次呢?我可以想象出各种场景(特别是测试方面),我希望构建一系列的映射,每个映射都添加了一个新的键值对到前一个映射中。" - Alexander
@Alexander,由于此构建器创建可变映射表,因此在创建单个映射表后添加的新元素将修改先前通过build()调用返回的映射表。当在build()之后使用构建器时,你需要额外的代码来复制映射表,这不应该降低只需要一个映射表的常见情况的性能(即,你不能仅在build()内部复制映射表)。这是可以实现的,但有点复杂,而且对此答案来说是不必要的。 - Tagir Valeev
啊,我犯了两个关键错误:一是忘记了Collections.unmodifiableMap只是一个包装器,它不执行复制。第二,我最近深入学习Swift,被其中的字典作为值类型并采用写时复制技术所宠坏了。 - Alexander

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接