如何初始化静态Map?

1241

如何在Java中初始化静态Map

方法一:使用静态初始化器
方法二:使用实例化初始化器(匿名子类) 或 其他方法?

每种方法的优缺点是什么?

以下是两种方法的示例:

import java.util.HashMap;
import java.util.Map;

public class Test {
    private static final Map<Integer, String> myMap = new HashMap<>();
    static {
        myMap.put(1, "one");
        myMap.put(2, "two");
    }

    private static final Map<Integer, String> myMap2 = new HashMap<>(){
        {
            put(1, "one");
            put(2, "two");
        }
    };
}

2
在Java 8中初始化一个Map:https://dev59.com/questions/1nRB5IYBdhLWcg3wyqOo#37384773 - akhil_mittal
2
请不要使用双括号初始化(double brace initialization) - 这是一种hack方法,容易导致内存泄漏和其他问题。 - dimo414
Java 9?如果输入数量<=10,请使用Map.of,否则请使用Map.ofEntries,请参见https://dev59.com/1nRB5IYBdhLWcg3wyqOo#37384773。 - akhil_mittal
43个回答

1194

在这种情况下,实例初始化程序只是语法糖,对吗?我不明白为什么需要额外的匿名类来进行初始化。而且,如果被创建的类是final,它将无法正常工作。

您也可以使用静态初始化程序创建不可变映射:

public class Test {
    private static final Map<Integer, String> myMap;
    static {
        Map<Integer, String> aMap = ....;
        aMap.put(1, "one");
        aMap.put(2, "two");
        myMap = Collections.unmodifiableMap(aMap);
    }
}

11
这是我多年来一直在使用的成语,从未有人对此表示异议。我也同样对不可修改的常量 Set 和 List 进行了相同的处理。 - jasonmp85
3
我该如何处理一个键为String类型的HashMap<String, String>呢?Map对象不允许我使用String作为键,因此我不能使用unmodifiableMap()。我猜将其强制转换为HashMap也会失去效果。有什么想法吗? - Luke
32
@Luke,我严重怀疑Android有这样的限制,这完全没有意义。快速搜索发现这个问题(以及许多其他问题)似乎暗示您可以在Android中使用String键来操作Map对象。 - mluisbrown
11
我可以确认,在Android上使用字符串键(String key)作为Map对象的索引是没有问题的,因为没有其他人再去调查这个问题。 - Jordan
11
乔丹:这是一个老话题了,但我怀疑 @Luke 当时试图将一个字符串用作在一个键类型不同的 map 中的键,例如 Map<Integer, String>。 - Miserable Variable
显示剩余4条评论

485

我喜欢使用Guava初始化静态不可变映射的方法:

static final Map<Integer, String> MY_MAP = ImmutableMap.of(
    1, "one",
    2, "two"
);

从方便的工厂方法ImmutableMap可以看出,它非常简洁。

如果您希望该映射具有超过5个条目,则无法再使用ImmutableMap.of()。而是尝试使用ImmutableMap.builder(),如下所示:

static final Map<Integer, String> MY_MAP = ImmutableMap.<Integer, String>builder()
    .put(1, "one")
    .put(2, "two")
    // ... 
    .put(15, "fifteen")
    .build();

要了解Guava不可变集合工具的优势,可以参考Guava用户指南中的不可变集合解释

(一部分)Guava曾被称为Google Collections。如果您还没有在Java项目中使用此库,我强烈建议尝试一下! Guava已经迅速成为Java最受欢迎和有用的免费第三方库之一,正如其他SO用户所认同的。(如果您对此库不甚了解,此链接提供了一些优秀的学习资源。)


更新(2015):至于Java 8,我仍然会使用Guava方法,因为它比其他方法更加清晰。如果您不想使用Guava依赖项,请考虑传统的初始化方法。如果您问我,在问题中使用二维数组和Stream API的hack非常丑陋,而且如果您需要创建键和值类型不同的Map(例如问题中的Map<Integer, String>),则更加复杂。

至于Guava在Java 8方面的未来,Louis Wasserman在2014年曾这样说过,而[更新]在2016年宣布Guava 21将需要并正确支持Java 8


更新(2016):正如Tagir Valeev指出的那样Java 9最终将通过添加集合的方便工厂方法,使此操作变得更加简单,只需使用纯JDK即可完成:

static final Map<Integer, String> MY_MAP = Map.of(
    1, "one", 
    2, "two"
);

21
看起来我们的SO管理员已经删除了我链接的那个值得尊敬的“最有用的免费第三方Java库”问题。 :( 该死的他们。 - Jonik
2
我同意,这是初始化常量映射的最佳方式。不仅更易读,而且由于Collections.unmodifiableMap返回基础映射的只读视图(仍然可以修改),因此更加安全。 - crunchdog
11
我现在可以看到已被删除的问题(需要10k+声望),所以这里是一个“最有用的免费第三方Java库”的拷贝。这只是第一页,但至少你可以找到上面提到的Guava资源 - Jonik
2
我非常喜欢这种方式,尽管了解如何在没有额外依赖的情况下完成它也很有益。 - Wrench
2
JEP 186仍未关闭,因此可能会引入与集合字面值相关的新功能。 - cybersoft
显示剩余3条评论

209

我会使用:

public class Test {
    private static final Map<Integer, String> MY_MAP = createMap();

    private static Map<Integer, String> createMap() {
        Map<Integer, String> result = new HashMap<>();
        result.put(1, "one");
        result.put(2, "two");
        return Collections.unmodifiableMap(result);
    }
}
  1. 这避免了使用匿名类,我个人认为这是一种不好的风格,应该避免
  2. 它使map的创建更加明确
  3. 它使map成为不可修改的
  4. 由于MY_MAP是常量,我会像常量一样给它命名

5
在所有纯JDK选项(不涉及库)中,我最喜欢这个,因为它清晰地将Map定义与其初始化联系起来。我也同意常量命名的做法。 - Jonik
我从未想过你可以这样做。 - romulusnr

192

Java 5提供了更紧凑的语法:

static final Map<String , String> FLAVORS = new HashMap<String , String>() {{
    put("Up",    "Down");
    put("Charm", "Strange");
    put("Top",   "Bottom");
}};

48
这种技术被称为双括号初始化:https://dev59.com/IXM_5IYBdhLWcg3wdy1g#1372124。它不是Java 5的特殊语法,而只是使用匿名类和实例初始化程序的一种技巧。 - Jesper
13
关于双括号初始化的快速问题:使用此方法时,Eclipse会发出有关缺少Serial ID的警告。一方面,我不明白为什么在这种特定情况下需要Serial ID,但另一方面,我通常不喜欢忽略警告。你对此有何想法? - nbarraille
9
那是因为HashMap实现了Serializable接口。由于你使用了这个"技巧"来创建HashMap的子类,你隐式地创建了一个可序列化的类。为了做到这一点,你应该提供一个serialUID。 - noone
6
在非静态上下文中使用双括号初始化可能会导致内存泄漏,因为所创建的匿名类将保留对周围对象的引用。与常规初始化相比,由于需要额外的类加载,它的性能较差。如果equals()方法不接受子类作为参数,则它可能导致equals()比较失败。最后,在Java 9之前,它不能与菱形操作符结合使用,因为无法与匿名类一起使用。 - Mark Jeronimus
3
建议使用的确实是静态上下文。性能可能会稍差,但在处理预计数量较小的静态定义映射时不会明显。 HashMap.equals 定义在 AbstractMap 中,适用于 Map 的 任何 子类,因此这里不需要考虑。钻石操作符的问题很烦人,但如前所述,现在已经解决了。 - Jules
显示剩余3条评论

104

第二种方法的一个优点是,您可以将其包装在Collections.unmodifiableMap()中,以确保没有任何东西会更新集合:

private static final Map<Integer, String> CONSTANT_MAP = 
    Collections.unmodifiableMap(new HashMap<Integer, String>() {{ 
        put(1, "one");
        put(2, "two");
    }});

 // later on...

 CONSTANT_MAP.put(3, "three"); // going to throw an exception!

3
你可以通过将new操作符移至static {}块并进行包装,在第一种方法中轻松地完成这个任务。 - Patrick
2
我会将构造函数调用移至静态初始化中。其他任何方式看起来都很奇怪。 - Tom Hawtin - tackline
2
使用匿名类与具体类相比,可能会有什么性能损失? - Kip

78

Java 9+ 中的 Map.of

private static final Map<Integer, String> MY_MAP = Map.of(1, "one", 2, "two");

详情请参见JEP 269。JDK 9于2017年9月达到一般可用性


10
如果您需要超过10个键值对,您可以使用Map.ofEntries - ZhekaKozlov
10
这个看起来很干净整洁,直到你意识到它是如何被实现的 - mid
5
只要JDK实现可以正常工作并满足合同,它的实现清洁度就不重要。就像任何黑盒子一样,如果有需要,实现细节总是可以在将来进行修复。 - vikingsteve
@LukeHutchison 我不认为一个库必须在内部保持清洁。 - mid
@mid 我同意。Java 方法中的最大参数数量为 255,因此从技术上讲,您只能在支持类型安全的情况下高达 128 个键值对。这里真正的问题是数组无法通用化,因此无法声明 varargs 类型,例如 Map.of(Entry<Integer, String>...) - Luke Hutchison
显示剩余3条评论

66

这是一个 Java 8 的一行静态映射初始化器:

private static final Map<String, String> EXTENSION_TO_MIMETYPE =
    Arrays.stream(new String[][] {
        { "txt", "text/plain" }, 
        { "html", "text/html" }, 
        { "js", "application/javascript" },
        { "css", "text/css" },
        { "xml", "application/xml" },
        { "png", "image/png" }, 
        { "gif", "image/gif" }, 
        { "jpg", "image/jpeg" },
        { "jpeg", "image/jpeg" }, 
        { "svg", "image/svg+xml" },
    }).collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]));

编辑:要像问题中那样初始化一个 Map<Integer, String>,你需要这样做:

修改:要初始化一个类似于问题中的 Map<Integer, String>,你需要这样做:

static final Map<Integer, String> MY_MAP = Arrays.stream(new Object[][]{
        {1, "one"},
        {2, "two"},
}).collect(Collectors.toMap(kv -> (Integer) kv[0], kv -> (String) kv[1]));

编辑(2): 这里有一个更好的,能够处理混合类型的版本,由i_am_zero编写,使用了一系列new SimpleEntry<>(k, v)调用。查看该答案: https://dev59.com/1nRB5IYBdhLWcg3wyqOo#37384773


7
我已经擅自添加了一个等价于问题和其他回答的版本:初始化一个键和值类型不同的Map(因此String[][]行不通,需要使用Object[][])。在我看来,这种方法很丑陋(即使使用强制类型转换也更加如此),而且很难记住;我不会使用它。 - Jonik

63

Java 9

我们可以使用Map.ofEntries,调用Map.entry(k,v)来创建每个条目。

import static java.util.Map.entry;
private static final Map<Integer,String> map = Map.ofEntries(
        entry(1, "one"),
        entry(2, "two"),
        entry(3, "three"),
        entry(4, "four"),
        entry(5, "five"),
        entry(6, "six"),
        entry(7, "seven"),
        entry(8, "eight"),
        entry(9, "nine"),
        entry(10, "ten"));

我们也可以像Tagir在他的回答这里中建议的那样使用Map.of,但是如果使用Map.of,我们最多只能有10个条目。
Java 8
我们可以创建一个映射条目的流。我们已经在java.util.AbstractMap中有两个Entry实现,它们分别是SimpleEntrySimpleImmutableEntry。对于这个示例,我们可以使用前者:
import java.util.AbstractMap.*;
private static final Map<Integer, String> myMap = Stream.of(
            new SimpleEntry<>(1, "one"),
            new SimpleEntry<>(2, "two"),
            new SimpleEntry<>(3, "three"),
            new SimpleEntry<>(4, "four"),
            new SimpleEntry<>(5, "five"),
            new SimpleEntry<>(6, "six"),
            new SimpleEntry<>(7, "seven"),
            new SimpleEntry<>(8, "eight"),
            new SimpleEntry<>(9, "nine"),
            new SimpleEntry<>(10, "ten"))
            .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue));
            

5
使用 new SimpleEntry<>() 的方式比静态方法 put() 更难以阅读。:/ - Danon

31

使用Eclipse Collections,以下所有内容都可以工作:

import java.util.Map;

import org.eclipse.collections.api.map.ImmutableMap;
import org.eclipse.collections.api.map.MutableMap;
import org.eclipse.collections.impl.factory.Maps;

public class StaticMapsTest
{
    private static final Map<Integer, String> MAP =
        Maps.mutable.with(1, "one", 2, "two");

    private static final MutableMap<Integer, String> MUTABLE_MAP =
       Maps.mutable.with(1, "one", 2, "two");


    private static final MutableMap<Integer, String> UNMODIFIABLE_MAP =
        Maps.mutable.with(1, "one", 2, "two").asUnmodifiable();


    private static final MutableMap<Integer, String> SYNCHRONIZED_MAP =
        Maps.mutable.with(1, "one", 2, "two").asSynchronized();


    private static final ImmutableMap<Integer, String> IMMUTABLE_MAP =
        Maps.mutable.with(1, "one", 2, "two").toImmutable();


    private static final ImmutableMap<Integer, String> IMMUTABLE_MAP2 =
        Maps.immutable.with(1, "one", 2, "two");
}

您还可以使用Eclipse Collections静态初始化原始映射。

import org.eclipse.collections.api.map.primitive.ImmutableIntObjectMap;
import org.eclipse.collections.api.map.primitive.MutableIntObjectMap;
import org.eclipse.collections.impl.factory.primitive.IntObjectMaps;

public class StaticPrimitiveMapsTest
{
    private static final MutableIntObjectMap<String> MUTABLE_INT_OBJ_MAP =
            IntObjectMaps.mutable.<String>empty()
                    .withKeyValue(1, "one")
                    .withKeyValue(2, "two");

    private static final MutableIntObjectMap<String> UNMODIFIABLE_INT_OBJ_MAP =
            IntObjectMaps.mutable.<String>empty()
                    .withKeyValue(1, "one")
                    .withKeyValue(2, "two")
                    .asUnmodifiable();

    private static final MutableIntObjectMap<String> SYNCHRONIZED_INT_OBJ_MAP =
            IntObjectMaps.mutable.<String>empty()
                    .withKeyValue(1, "one")
                    .withKeyValue(2, "two")
                    .asSynchronized();

    private static final ImmutableIntObjectMap<String> IMMUTABLE_INT_OBJ_MAP =
            IntObjectMaps.mutable.<String>empty()
                    .withKeyValue(1, "one")
                    .withKeyValue(2, "two")
                    .toImmutable();

    private static final ImmutableIntObjectMap<String> IMMUTABLE_INT_OBJ_MAP2 =
            IntObjectMaps.immutable.<String>empty()
                    .newWithKeyValue(1, "one")
                    .newWithKeyValue(2, "two");
} 

注意:我是Eclipse Collections的贡献者


1
我真的希望Eclipse Collections成为Java的默认集合库。我比Guava + JCL更喜欢它。 - Kenny Cason

29

在这种情况下,我绝不会创建匿名子类。如果您想使地图无法修改,静态初始化程序同样有效:

private static final Map<Integer, String> MY_MAP;
static
{
    Map<Integer, String>tempMap = new HashMap<Integer, String>();
    tempMap.put(1, "one");
    tempMap.put(2, "two");
    MY_MAP = Collections.unmodifiableMap(tempMap);
}

1
在什么情况下,您会使用匿名子类来初始化哈希映射? - dogbane
6
永远不要初始化一个集合。 - eljenso
你能解释一下,为什么使用静态初始化器比创建匿名子类更好吗? - leba-lev
3
其他答案中列举了几个支持静态初始化的理由。这里的目标就是进行初始化,那么为什么要引入子类化呢?除非只是为了节省一些按键次数。(如果你想要节省按键次数,Java 绝对不是一个好的编程语言选择。)我在使用 Java 编程时遵循的一个经验法则是:尽可能少地进行子类化(并且只有在合理可以避免时才这样做)。 - eljenso
@eljenso - 我通常偏爱使用子类语法的原因是它将初始化放在了内联中,这正是它应该存在的地方。第二选择是调用返回已初始化映射的静态方法。但我担心我会看着你的代码,花费几秒钟来弄清楚MY_MAP来自哪里,这是我不想浪费的时间。任何提高可读性的改进都是一个奖励,而性能影响很小,所以对我来说似乎是最好的选择。 - Jules
你不能在静态初始化器中使用方法参数。 - Danon

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