为什么要将私有静态不可变的Lists/Sets/Maps?

10

我刚刚阅读了一位更有经验的程序员编写的代码,并发现了以下内容:

public class ConsoleFormatter extends Formatter {
    private static final Map<Level, String> PREFIXES;

    static {
        Map<Level, String> prefixes = new HashMap<Level, String>();
        prefixes.put(Level.CONFIG,  "[config]");
        prefixes.put(Level.FINE,    "[debug]");
        prefixes.put(Level.FINER,   "[debug]");
        prefixes.put(Level.FINEST,  "[trace]");
        prefixes.put(Level.INFO,    "[info]");
        prefixes.put(Level.SEVERE,  "[error]");
        prefixes.put(Level.WARNING, "[warning]");

        PREFIXES = Collections.unmodifiableMap(prefixes);
    }

    // ...

}

正如您所看到的,这是一个用于格式化日志输出的类。然而,吸引我眼球的是静态初始化块中的代码:PREFIXES = Collections.unmodifiableMap(prefixes);

PREFIXES 为什么要成为一个不可修改的映射?它是一个私有常量,所以在该类之外修改数据的风险不存在。 这样做是为了使常量的不可变性更加完整吗?

个人而言,我会将 PREFIXES 直接初始化为一个 HashMap,然后直接使用 put 方法来添加键值对,而不需要创建一个虚拟的占位符映射或使字段成为不可变映射。我错过了什么吗?


这也可以被认为是在实例化类时防止并发初始化问题的一种方法,因为整个数据的PREFIXES只需要单步赋值。 - Joop Eggen
6个回答

9
如果你在方法中不小心返回PREFIXES,那么其他代码可能会突然修改它。如果将常量设为完全不可变,则可以防止你在未来凌晨修改该代码时出现错误。

否则,final 对于 Map<Level, String> PREFIXES 映射将不完整。 - Chan

9
通过使列表不可修改,作者记录了他的假设,即值永远不会改变。之后可能编辑该类的任何人不仅可以看到这个假设,而且在它被打破时还会被提醒。
只有从长远来看,这才有意义。它降低了通过维护产生新问题的风险。我喜欢这种编程风格,因为即使在我的自己的类中,我也往往会弄坏东西。有一天你可能需要快速修复,但你忘记了最初所做的假设对于正确性是相关的。你能够将代码锁定得越多,就越好。

2
有一天,你可能会进行快速修复,却忘记了最初做出的假设,而这对于正确性来说是相关的。 - Konstantin

3

让一个私有的map、集合或数组在类外可修改是极为容易的。如果你将其标记为final,那么也应该明确它是不可变的。


我理解你的意思,但是提到的字段没有getter方法。你是说即使如此,将它们设为不可变也是一个好的实践吗? - Konstantin
1
@KonstantinĐ。不需要是getter。我最喜欢的例子在Michael Feather的《Working With Legacy Code》一书中。在第13章中,他详细介绍了对象如何泄漏,讲得非常枯燥乏味。而在第14章中,他揭示了一个private static数组。如果他这样做了,那么我不指望普通程序员能正确理解它——除非他们非常明确地将可变对象隐藏在不可修改的包装器后面。 - Tom Hawtin - tackline

3

假设你的朋友离开了他的工作,由一个经验较少的程序员接手。这个经验较少的程序员试图修改同一类中另一个方法中 PREFIXES 的内容。如果 PREFIXES 不可更改,那么它将不起作用。这是告诉别人“这是一个常量,永远不要更改它”的正确方式。


1

Map接口并不表明您希望某些内容是不可变或不可修改的。

以下方法在Eclipse Collections中可行。

private static final ImmutableMap<Level, String> PREFIXES = UnifiedMap.<Level, String>newMap()
    .withKeyValue(Level.CONFIG, "[config]")
    .withKeyValue(Level.FINE, "[debug]")
    .withKeyValue(Level.FINER, "[debug]")
    .withKeyValue(Level.FINEST, "[trace]")
    .withKeyValue(Level.INFO, "[info]")
    .withKeyValue(Level.SEVERE, "[error]")
    .withKeyValue(Level.WARNING, "[warning]")
    .toImmutable();

这将创建一个合同不可变的Map,因为ImmutableMap在其API中没有变异方法。

如果您更喜欢保留Map接口,则此方法也适用。

private static final Map<Level, String> PREFIXES = UnifiedMap.<Level, String>newMap()
    .withKeyValue(Level.CONFIG, "[config]")
    .withKeyValue(Level.FINE, "[debug]")
    .withKeyValue(Level.FINER, "[debug]")
    .withKeyValue(Level.FINEST, "[trace]")
    .withKeyValue(Level.INFO, "[info]")
    .withKeyValue(Level.SEVERE, "[error]")
    .withKeyValue(Level.WARNING, "[warning]")
    .asUnmodifiable();

你应该注意到在这两种情况下都不需要静态块。

注意:我是 Eclipse Collections 的提交者。


0
如果将集合设置为final,就无法将新对象放入其中。 但是,仍然可以向相同的对象添加或删除项目。
当你将其设置为不可修改时,甚至不能向集合中添加或删除项目。 因此,建议始终将集合设置为不可修改,而不仅仅将其保持为final。

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