使用Class作为HashMap的键是否会导致不良影响?

7
请看以下内容:
Map<Class<?>, Object> myMap = new HashMap<Class<?>, Object>();
Foo fooObject = New Foo();
myMap.put(fooObject.getClass(), fooObject)

请注意,java.lang.Class本身不实现hashCode()方法,但隐式地从java.lang.Object继承它。我在JDK 1.8中验证了这一点。
是否可以将java.lang.Class用作java.util.HashMap的键?myMap.get(Foo.class)是否总是会返回像myMap.put(fooObject.getClass(), fooObject)这样的值?考虑到软件具有各种类加载器和序列化机制,结果是否仍然相同?如果不能,有什么替代方案?
6个回答

9

光从我的经验来看,是否有理由不使用字符串类名呢?例如,可以使用以下代码:

myMap.put("Foo", fooObject);

如果你担心可能有多个Foo类在作用域内,你可以使用完整的规范名称:

myMap.put(Foo.class.getCanonicalName(), fooObject);

2
我认为这是一个明智的建议。直接使用类本身的实例可能不安全,因为对象ID将被用作标识,因此同一类的不同实例将不相等,因此无法在Map中找到匹配的键。 - Adriaan Koster
2
@AdriaanKoster 没有人曾经称呼我为理智,谢谢 :-) - Tim Biegeleisen
getCanonicalName()看起来确实是一个不错的替代选择。作为一个谨慎的程序员,我相信在范围内会有同名类。因此,我会理智地使用它。 - Maarten
@TimBiegeleisen - 严格来说,他说你的建议是明智的。即使是一个疯狂的人也可以提出明智的建议 :-) - Stephen C
@StephenC 你是在说我称呼Tim为疯子吗!!??!!?? ;-) - Adriaan Koster
不,我是在说你没有说他是神智正常的 :-) - Stephen C

6
“java.lang.Class”是否可以安全地用作“java.util.HashMap”的键?
是的。
如果我使用“myMap.put(fooObject.getClass(), fooObject)”这样的方式,那么“myMap.get(Foo.class)”是否总是返回我放置的值?
是的。
在“HashMap”中使用“Class”对象作为键是安全的。 “Class”类继承了“Object :: equals”和“Object :: hashCode”方法。因此,“Class”对象的“equals”测试对象标识。
这是Java中类型相等的正确语义。 “ClassLoader :: defineClass”方法的实现确保您永远无法获得表示同一Java类型的两个不同的“Class”对象。
但是,有一个问题。 Java语言规范(JLS 4.3.4)规定:
在运行时,多个具有相同二进制名称的引用类型可能会被不同的类加载器同时加载。这些类型可能代表相同的类型声明,也可能不是。即使两种类型确实表示相同的类型声明,它们仍被视为不同。这意味着,如果您在两个不同的类加载器中对具有完全限定名称的类(成功地)调用ClassLoader :: defineClass,则会获得不同的Java类型。无论您使用的字节码如何。此外,如果您尝试从一个类型强制转换为另一个类型,您将收到一个类转换异常。
现在的问题是,这对你的使用情况是否重要?
答案:可能不重要。
除非您(或您的框架)正在使用类加载器进行复杂操作,否则不会出现这种情况。
如果确实需要,那么您可能需要让这两种类型(具有相同的FQDN和不同的类加载器)在HashMap中具有不同的条目。(因为这两种类型是不同的!)
但是,如果您需要这两种类型具有相同的条目,则可以使用类的FQDN作为键,您可以使用Class :: getCanonicalName获取它。 如果需要处理数组类等,则使用返回类型的二进制名称的Class :: getName。
什么是序列化机制?
使用对象序列化,无法对Class对象进行序列化,因为Class没有实现Serializable。如果您实现/使用其他支持序列化Class对象的序列化机制,则该机制需要与JLS 4.3.4兼容。

那正是我需要知道的。谢谢。 - Maarten

5

每个 ClassLoader 的 Class 实例都是唯一的,因此不需要重写 hashCodeequals


我要测试一下,只是为了确保。1秒钟。 - Adriaan Koster
1
@AdriaanKoster - 不需要。这在JLS 4.3.4中已经指定了。 - Stephen C
@StephenC 我猜是这样,但对我来说,做实践总是比仅仅阅读更好地理解和记忆。 - Adriaan Koster
3
是的,但是总的来说,1)“试一试”方法如果测试方式错误可能会给出错误答案,2)你可能最终展示了一个仅适用于特定Java实现的行为。仔细阅读(并理解!)规范是更可靠的方法。 - Stephen C

1
运行时类型和编译时类型是有区别的。如果(且仅当)它们是由不同的类加载器加载的,那么可以同时加载同一完全限定类名的多个类。然后这些类就是不同的运行时类型,即使它们相同,也不能彼此转换
因此,你的问题的答案取决于你认为哪种效果更可取:
  • 如果你希望将这些单独加载和不兼容的类视为映射中的不同类,请使用Class作为键。永远不会有两个具有相同名称和加载器的活动Class实例,因此Class类正确地不会覆盖hashCodeequals方法。所以把它用作HashMap键是可以的,虽然IdentityHashMap可能会更有效。

  • 如果你只想根据它们的名称来区分类,而不考虑它们是如何(或是否)被加载的,请使用它们的字符串名称作为映射键。


1

@talex 我按照下面的方式进行了测试,你似乎是正确的:

public class ClassUnique {

    public static void main(String [] args) throws ClassNotFoundException {
        Class<?>  c1 = Class.forName("java.util.Date");
        Class<?>  c2 = Class.forName("java.util.Date");

        System.out.println(c1.equals(c2));
    }
}

输出为true

编辑:@Maarten 我认为你是对的。特别是如果你在像Websphere或Weblogic这样的应用程序容器中运行,可能会涉及多个类加载器,这可能会搞砸它。因此,最简单正确的解决方案最终将只使用Class实例本身。


我仍然担心如果我读到这样的东西: https://dev59.com/XWgt5IYBdhLWcg3w3xIw - Maarten
我认为最明智的选择是使用完全限定的类名String作为键,并接受奇怪的类加载器技巧会破坏矩阵的事实。 - Adriaan Koster
正如Stephen C在他的回答中提到的那样。使用相同FQDN的两个类来自不同的类加载器将被认为是不同的,将导致ClassCastException异常。 在我的情况下,这会使FQDN字符串无法使用,因为它会混合来自各种类加载器的类。由类加载器与FQDN组成的键将是正确的选择,或者像Stephen C所说,类本身是安全可用的。 - Maarten

0

我会考虑使用IdentityHashMap。它不依赖于equals

这个类仅设计用于需要引用相等语义的罕见情况。


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