【问题标题】:How to make a specialization of Map unmodifiable?如何使 Map 的专业化不可修改?
【发布时间】:2019-08-31 17:13:16
【问题描述】:

我目前正在通过一个中型编码示例更新我的 Java 知识。我有一个数据结构Map<String, String>,通常使用new LinkedHashMap<>() 对其进行初始化以保留插入顺序。我在我的代码中经常使用它,我想摆脱声明重复。在 C++ 中我会为地图设置别名,但据我所知,在 Java 中没有别名。

所以我想出了这样子类化泛型的想法:

public class Attributes extends LinkedHashMap<String, String> {

    public Attributes() {
        super();
    }

    public Attributes(Map<? extends String, ? extends String> map) {
        super(map);
    }

}

到目前为止这看起来不错,但现在我想创建一个不可修改的副本,因为属性应该是不可变/不可修改数据结构的一部分。在我使用它之前:

Map<String, String> unmodifiableAttributes = Collections.unmodifiableMap(
        new LinkedHashMap<>(attributes)
);

这不适用于派生类,我试过这个:

Attributes unmodifiableAttributes = Collections.unmodifiableMap(
        new Attributes(attributes)
);

编译器用Incompatible types 拒绝它。

是否有一种简单的方法可以获取此类子类的不可修改(或不可变)副本?还是我的想法完全错误?我不想写一个功能齐全的装饰器,就几行代码。

更新

到目前为止,我想做的事情似乎没有好的解决方案。我查看了 Java Collections 类的源代码,其中有用于不可修改的映射和类似集合的内部类。这些用于包装输入集合并由相应的静态方法返回。可以重新实现这一点,但我认为开销太大。

我们更多地讨论了 LSP 违规而不是原来的问题,这确实也是一个有趣的问题。

【问题讨论】:

  • “在我使用这个之前”有什么问题?
  • Map 没有说明数据的含义。它可以是 (lastName, firstName), (auto, title) 或类似的映射。首先,我希望有更好的自记录代码。其次,要输入的字符更少。

标签: java generics collections unmodifiable


【解决方案1】:

您不能使子类 LinkedHashMap 不可修改,因为它会违反 Liskov 可替换性:LinkedHashMap 被记录为是可变的,因此所有子类也必须是可变的。

您还有一个额外的问题是,要使地图不可修改实际上需要做很多工作:您不仅有 putremove 之类的明显方法,还有 clearputAll 之类的东西,putIfAbsentcomputeIfAbsentcomputeIfPresent。然后您必须担心返回视图的方法:entrySetkeySetvalues 都必须返回不可修改的视图。我确定我错过了几个也需要覆盖的方法,但我的观点仍然是让可变映射不可修改并非易事。

但是,您可以拥有不可修改的 Map 实现。最简单的方法是扩展AbstractMap,并委托给实际的LinkedHashMap

public class Attributes extends AbstractMap<String, String> {
    private final LinkedHashMap<String, String> delegate;

    public Attributes() {
        this(Collections.emptyMap());
    }

    public Attributes(Map<? extends String, ? extends String> map) {
        this.delegate = new LinkedHashMap<>(map);
    }

    // Override the methods you need to, and that are required by AbstractMap.
    // Details of methods to override in AbstractMap are given in Javadoc.
}

但我也会质疑您的 Attributes 类是否真的需要实现像 Map 接口一样通用的东西 - 如果您需要这种通用性,您可以直接使用 Map

【讨论】:

  • LSP 违规确实是一个有趣的点。但我不确定这是不是真正的原因?在使用 Map 的情况下,该实例还有一个 put() 方法和其他修改方法。天真的用户可以尝试修改,但会得到异常。对我来说,这看起来也像 LSP。
  • @Andi 不违反 LSP:方法是 documented as optional:它将值放入映射中,或者抛出异常。因此,抛出异常对于子类来说是完全可以接受的。坏事是 Map 接口让你不调用方法就无法知道。这基本上意味着您永远不应该在您不确定的 Map 上调用突变方法来支持这些方法。
  • 我明白你的意思,是的。但据我了解,“可选”只是文档中的文本,而不是接口定义中的文本。我不相信这一点,这个实现对我来说有点模糊。
  • @Andi 是的,可选的只是文本。正是它说“Throws:UnsupportedOperationException - 如果此映射不支持 put 操作”这一事实使其成为接口定义的一部分。
【解决方案2】:

Collections.unmodifiableMap 返回一个Map&lt;K,V&gt;,所以你必须像这样使用它:

Map<String, String> unmodifiableAttributes = Collections.unmodifiableMap(
            new Attributes(attributes)
);

并且您无法将返回的对象转换为Attributes,例如:

Attributes unmodifiableAttributes = (Attributes) Collections.unmodifiableMap(
            new Attributes(attributes)
);

因为Collections.unmodifiableMap 返回private static UnmodifiableMap 的实例,所以您将获得ClassCastException。而Attributes 不是UnmodifiableMap 的子类型。

我还认为,在您的情况下,直接使用LinkedHashMap 而不是从中创建派生类会更容易,因为我看到功能与原始功能没有区别。然后将Collections.unmodifiableMap返回的对象作为Map使用。

【讨论】:

  • 是的,这就是我目前发现的。子类化不是我的主要意图,我只想有一种更短的方法来定义变量和方法参数。正如我的问题中提到的,在 C++ 中,“使用”关键字有助于使源代码更具可读性,而无需添加额外的代码。但是Java好像没有这么简单的方法?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-07
  • 2019-09-22
  • 2015-06-15
  • 1970-01-01
  • 1970-01-01
  • 2020-07-18
相关资源
最近更新 更多