Implementing a Map with Multiple Keys in Java – 在Java中实现具有多个键的地图

最后修改: 2022年 7月 30日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

We often make use of maps in our programs, as a means to associate keys with values. Typically in our Java programs, especially since the introduction of generics, we will have all of the keys be the same type and all of the values be the same type. For example, a map of IDs to values in a data store.

我们经常在我们的程序中使用映射,作为将键与值联系起来的一种手段。通常在我们的Java程序中,特别是自从引入generics之后,我们会让所有的键都是同一类型,所有的值都是同一类型。例如,在一个数据存储中,一个ID到值的映射。

On some occasions, we might want to use a map where the keys are not always the same type. For example, if we change our ID types from Long to String, then our data store will need to support both key types – Long for the old entries and String for the new ones.

在某些情况下,我们可能想要使用一个键的类型不总是相同的地图。例如,如果我们将ID类型从Long改为String,那么我们的数据存储将需要支持两种键类型–Long用于旧条目,String用于新条目。

Unfortunately, the Java Map interface doesn’t allow for multiple key types, so we need to find another solution. We’re going to explore a few ways this can be achieved in this article.

不幸的是,Java的Map接口不允许有多个键类型,所以我们需要找到另一种解决方案。我们将在本文中探讨几种可以实现的方法。

2. Using Generic Supertypes

2.使用通用超类型

The easiest way to achieve this is to have a map where the key type is the closest supertype to all of our keys. In some cases, this might be easy – for example, if our keys are Long and Double then the closest supertype is Number:

实现这一目标的最简单方法是建立一个映射,其中键的类型是与我们所有键最接近的超类型。在某些情况下,这可能很容易 – 例如,如果我们的键是LongDouble,那么最接近的超类型是Number

Map<Number, User> users = new HashMap<>();

users.get(longId);
users.get(doubleId);

However, in other cases, the closest supertype is Object. This has the downside that it completely removes type safety from our map:

然而,在其他情况下,最接近的超类型是Object。这有一个缺点,那就是它从我们的地图中完全删除了类型安全。

Map<Object, User> users = new HashMap<>();

users.get(longId); /// Works.
users.get(stringId); // Works.
users.get(Instant.now()); // Also works.

In this case, the compiler doesn’t stop us from passing the wrong types in, effectively removing all type safety from our map. In some cases, this might be fine. For example, this will probably be fine if another class encapsulates the map so as to enforce the type safety itself.

在这种情况下,编译器不会阻止我们传入错误的类型,有效地从我们的地图中移除所有类型安全。在某些情况下,这可能是好的。例如,如果有另一个类对地图进行了封装,从而使类型安全本身得以实施,那么这种情况可能是好的。

However, it still opens up risks in how the map can be used.

然而,它仍然为如何使用地图带来了风险。

3. Multiple Maps

3.多种地图

If type safety is important, and we’ll be encapsulating our map inside another class, another simple option is to have multiple maps. In this case, we’d have a different map for each of our supported keys:

如果类型安全很重要,而且我们将在另一个类中封装我们的地图,那么另一个简单的选择就是拥有多个地图。在这种情况下,我们为每个支持的键都有一个不同的地图。

Map<Long, User> usersByLong = new HashMap<>();
Map<String, User> usersByString = new HashMap<>();

Doing this ensures that the compiler will keep type safety for us. If we try to use an Instant here, then the compiler won’t let us, so we’re safe here.

这样做可以确保编译器为我们保持类型安全。如果我们试图在这里使用一个 Instant,那么编译器就不会让我们使用,所以我们在这里是安全的。

Unfortunately, this adds complexity because we need to know which of our maps to use. This means that we either have different methods working with different maps, or else we’re doing type checking everywhere.

不幸的是,这增加了复杂性,因为我们需要知道使用哪一个映射。这意味着我们要么有不同的方法与不同的映射一起工作,要么我们到处进行类型检查。

This also doesn’t scale well. We will need to add a new map and new checks all over if we ever need to add a new key type. For two or three key types, this is manageable, but it quickly gets to be too much.

这也不能很好地扩展。如果我们需要增加一个新的钥匙类型,我们将需要添加一个新的地图和新的检查。对于两个或三个键类型,这是可以管理的,但它很快就会变得太多了。

4. Key Wrapper Types

4.关键包装类型

If we need to have type safety, and we don’t want the maintainability burden of many maps, then we need to find a way to have a single map that can have different values in the key. This means that we need to find some way to have a single type that is actually different types. We can achieve this in two different ways – with a single wrapper or with an interface and subclasses.

如果我们需要类型安全,并且我们不希望有许多映射的可维护性负担,那么我们需要找到一种方法来拥有一个可以在键中拥有不同值的单一映射。这意味着我们需要找到某种方法来拥有一个实际上是不同类型的单一类型。我们可以通过两种不同的方式来实现这一点–用一个单一的包装器或者用一个接口和子类。

4.1. Single Wrapper Class

4.1.单一封装器类

One option we have is to write a single class that can wrap any of our possible key types. This will have a single field for the actual key value, correct equals and hashCode methods, and then one constructor for each possible type:

我们有一个选择,就是写一个单一的类,它可以包裹我们任何可能的键类型。这将有一个用于实际键值的字段,正确的equalshashCode方法,然后为每个可能的类型提供一个构造函数。

class MultiKeyWrapper {
    private final Object key;

    MultiKeyWrapper(Long key) {
        this.key = key;
    }

    MultiKeyWrapper(String key) {
        this.key = key;
    }

    @Override
    public bool equals(Object other) { ... }

    @Override
    public int hashCode() { ... }
}

This is guaranteed to be typesafe because it can only be constructed with either a Long or a String. And we can use it as a single type in our map because it is in itself a single class:

这保证是类型安全的,因为它只能用LongString构建。而且我们可以在我们的map中使用它作为一个单一的类型,因为它本身就是一个单一的类。

Map<MultiKeyWrapper, User> users = new HashMap<>();
users.get(new MultiKeyWrapper(longId)); // Works
users.get(new MultiKeyWrapper(stringId)); // Works
users.get(new MultiKeyWrapper(Instant.now())); // Compilation error

We simply need to wrap our Long or String in our new MultiKeyWrapper for every access to the map.

我们只需要将我们的LongString包裹在我们新的MultiKeyWrapper中,用于每次访问地图。

This is relatively simple, but it will make extension slightly harder. Whenever we want to support any additional types then, we’ll need to change our MultiKeyWrapper class to support it.

这相对简单,但它会使扩展变得稍微困难。无论何时我们想要支持任何额外的类型,我们都需要改变我们的MultiKeyWrapper类来支持它。

4.2. Interface and Subclasses

4.2.接口和子类

Another alternative is to write an interface to represent our key wrapper and then write an implementation of this interface for every type that we want to support:

另一种方法是编写一个接口来表示我们的密钥包装器,然后为我们想要支持的每一种类型编写这个接口的实现。

interface MultiKeyWrapper {}

record LongMultiKeyWrapper(Long value) implements MultiKeyWrapper {}
record StringMultiKeyWrapper(String value) implements MultiKeyWrapper {}

As we can see, these implementations can use the Record functionality introduced in Java 14, which will make the implementation much easier.

我们可以看到,这些实现可以使用Java 14中引入的Record功能,这将使实现更加容易。

As before, we can then use our MultiKeyWrapper as the single key type for a map. We then use the appropriate implementation for the key type that we want to use:

像以前一样,我们可以使用我们的MultiKeyWrapper作为map的单一键类型。然后我们为我们想要使用的键类型使用适当的实现。

Map<MultiKeyWrapper, User> users = new HashMap<>();
users.get(new LongMultiKeyWrapper(longId)); // Works
users.get(new StringMultiKeyWrapper(stringId)); // Works

In this case, we don’t have a type to use for anything else, so we can’t even write invalid code in the first place.

在这种情况下,我们没有一个类型可以用来做其他事情,所以我们甚至不能在第一时间写出无效的代码。

With this solution, we support additional key types not by changing the existing classes but by writing a new one. This is easier to support, but it also means that we have less control over what key types are supported.

有了这个解决方案,我们不是通过改变现有的类来支持额外的键类型,而是通过编写一个新的类。这更容易支持,但这也意味着我们对支持哪些键类型的控制更少。

However, this can be managed by the correct use of visibility modifiers. Classes can only implement our interface if they have access to it, so if we make it package-private, then only classes in the same package can implement it.

然而,这可以通过正确使用visibility modifiers进行管理。类只有在能够访问我们的接口时才能实现它,所以如果我们将其设为包专用,那么只有同一包中的类才能实现它。

5. Conclusion

5.结论

Here we’ve seen some ways to represent a map of keys to values, but where the keys are not always of the same type. Examples of these strategies can be found over on GitHub.

在这里,我们看到了一些表示键到值的映射的方法,但其中的键并不总是同一类型。这些策略的例子可以在GitHub上找到over