将Class用作HashMap的键会导致不良后果吗?

时间:2019-01-07 10:28:19

标签: java class hashmap hashcode

请考虑以下内容:

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 个答案:

答案 0 :(得分:7)

在我的头上,有什么理由不使用字符串类名称?例如。而是使用:

myMap.put("Foo", fooObject);

如果您对范围内可能存在多个Foo类抱有偏执的想法,则可以使用完整的规范名称:

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

答案 1 :(得分:4)

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

答案 2 :(得分:3)

  

java.lang.Class可以安全用作java.util.HashMap的键吗?

是的

  

myMap.get(Foo.class)是否总是返回我放置的值,例如myMap.put(fooObject.getClass(),fooObject)?

是的

Class中使用HashMap对象作为键是安全的。 Class类继承了Object::equalsObject::hashCode方法。因此,equals个对象的Class正在测试对象身份。

这是Java中类型相等的正确语义。 ClassLoader::defineClass方法的实现可确保您永远无法获得代表相同Java类型的两个不同的Class对象。

但是,有皱纹。 Java语言规范(JLS 4.3.4)指出:

  

在运行时,具有相同二进制名称的几种引用类型可以由不同的类加载器同时加载。这些类型可能代表相同的类型声明,也可能不代表相同的类型声明。即使两个这样的类型确实表示相同的类型声明,它们也被认为是不同的。

(二进制名称与命名类型的FQDN相关,并考虑了匿名类和数组类型。)

这意味着,如果您成功(成功)在两个不同的类加载器中为具有相同完全限定名称的类调用ClassLoader::defineClass,则您将获得不同的Java类型。与您使用的字节码无关。此外,如果尝试从一种类型转换为另一种类型,则将获得类转换异常。


现在的问题是,这在您的用例中重要吗?

答案:可能不会。

  • 除非您(或您的框架)使用类加载器进行棘手的事情,否则不会出现这种情况。

  • 如果这样做,那么您可能需要两种类型(具有相同的FQDN和不同的类加载器)才能在HashMap中具有不同的条目。 (因为类型不同!)

  • 但是,如果您需要两种类型具有相同的条目,则可以使用该类的FQDN作为键,您可以使用Class::getCanonicalName获得该键。如果需要处理数组类等,请使用Class::getName,它返回类型的二进制名称。


序列化机制如何?

Class对象无法使用对象序列化进行序列化,因为Class未实现Serializable。如果您实现/使用其他确实支持Class对象序列化的序列化机制,则该机制需要与JLS 4.3.4兼容。

答案 3 :(得分:1)

运行时和编译时类型之间有区别。如果(且仅)当它们由不同的类加载器加载时,可以同时加载具有相同标准类名称的多个类。这样的类是不同的运行时类型,即使它们相同,也不能相互转换

因此,问题的答案仅取决于您认为理想的效果:

  • 如果您希望将那些单独加载的和不兼容的类视为地图中的不同类,请使用Class作为键。具有相同名称和加载器的Class的活动实例永远不会超过一个,因此Class类不会正确覆盖hashCodeequals方法。因此,尽管HashMap可以提供相同的行为,但可能更有效,但可以将其用作IdentityHashMap键。

  • 如果您希望仅基于类的名称来区分类,而不管它们是如何加载的(或是否加载),请使用其字符串名称作为映射键。

答案 4 :(得分:0)

@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实例本身。

答案 5 :(得分:0)

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

  

此类仅设计用于在少数情况下需要引用相等语义的情况。