7

Consider the following:

Map<Class<?>, Object> myMap = new HashMap<Class<?>, Object>();
Foo fooObject = New Foo();
myMap.put(fooObject.getClass(), fooObject)

Notice that java.lang.Class does not implement the hashCode() method itself, but inherits it from java.lang.Object implicitly. I verified this in JDK 1.8.

Is java.lang.Class safe to use as a key for a java.util.HashMap? Will myMap.get(Foo.class) always return the values which I put like myMap.put(fooObject.getClass(), fooObject)? Consider the software to have various classloaders and serialization mechanisms. Will it still be the same result? If not... What would be an alternative?

Boann
  • 48,794
  • 16
  • 117
  • 146
Maarten
  • 808
  • 1
  • 6
  • 17

6 Answers6

9

Off the top of my head, would there be any reason to just not use the string class names? E.g. instead use:

myMap.put("Foo", fooObject);

If you are paranoid that maybe there could be more than one Foo class in scope, you could use the full canonical name:

myMap.put(Foo.class.getCanonicalName(), fooObject);
Ori Marko
  • 56,308
  • 23
  • 131
  • 233
Tim Biegeleisen
  • 502,043
  • 27
  • 286
  • 360
  • 2
    I think this is a sane suggestion. Using an instance of Class itself is probably not safe, since the object id would be used as identity, so a different instance of the same class would not be equal, and thus not be found to match as key in the Map. – Adriaan Koster Jan 07 '19 at 10:33
  • 2
    @AdriaanKoster No one has ever called me sane before, thanks :-) – Tim Biegeleisen Jan 07 '19 at 10:34
  • The `getCanonicalName()` indeed looks like a good candidate as an alternative. Being paranoid, I am sure there will be classes with the same name in scope. So I will do the sane thing and use that. – Maarten Jan 07 '19 at 10:41
  • @TimBiegeleisen - Strictly speaking, he said that your *suggestion* was sane. Even an insane person *could* make a sane suggestion :-) – Stephen C Jan 07 '19 at 13:34
  • @StephenC Are you saying I called Tim insane!!??!!?? ;-) – Adriaan Koster Jan 07 '19 at 13:45
  • No. I am saying that you didn't say he was sane :-) – Stephen C Jan 07 '19 at 13:54
6

Is java.lang.Class safe to use as a key for a java.util.HashMap?

Yes.

Will myMap.get(Foo.class) always return the values which I put like myMap.put(fooObject.getClass(), fooObject)?

Yes.

Using a Class object as a key in a HashMap is safe. The Class class inherits the Object::equals and Object::hashCode methods. Thus equals for Class objects is testing object identity.

This is the correct semantics for type equality in Java. The implementation of the ClassLoader::defineClass method ensures that you can never get two different Class objects representing the same Java type.

However, there is a wrinkle. The Java Language specification (JLS 4.3.4) states this:

At run time, several reference types with the same binary name may be loaded simultaneously by different class loaders. These types may or may not represent the same type declaration. Even if two such types do represent the same type declaration, they are considered distinct.

(The binary name is related to the FQDN of a named type, and takes account of anonymous classes, and array types.)

What this means is that if you (successfully) call ClassLoader::defineClass for classes with the same fully qualified name in two different classloaders, you will get different Java types. Irrespective of the bytecodes you used. Furthermore, if you attempt to cast from one type to the other one, you will get a class cast exception.


Now the question is does this matter in your use-case?

Answer: probably not.

  • Unless you (or your framework) are doing tricky things with classloaders, the situation does not arise.

  • If it does, then you probably need the two types (with the same FQDN and different classloaders) to have the different entries in the HashMap. (Because the types are different!)

  • But if you need the two types to have the same entry, then you can use the FQDN for the class as the key, which you can obtain using Class::getCanonicalName. If you need to cope with array classes, etc, then use Class::getName which returns the binary name for the type.


What about serialization mechanisms?

A Class object cannot be serialized using Object serialization, since Class does not implement Serializable. If you implement / use some other serialization mechanism that does support the serialization of Class objects, then that mechanism needs to be compatible with JLS 4.3.4.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
5

Instances of Class are unique per ClassLoader so it is not needed to override hashCode or equals.

Boann
  • 48,794
  • 16
  • 117
  • 146
talex
  • 17,973
  • 3
  • 29
  • 66
  • I am going to test this just to be sure. 1 second. – Adriaan Koster Jan 07 '19 at 10:34
  • 1
    @AdriaanKoster - No need. It is specified in JLS 4.3.4. – Stephen C Jan 07 '19 at 13:33
  • @StephenC I guess, but for me doing is always a better way to understand and remember than just reading – Adriaan Koster Jan 07 '19 at 13:42
  • 3
    Yes ... but in general 1) the "try it out" approach can give you the wrong answer if you do the test the wrong way, and 2) you may end up demonstrating a behavior *that is specific to a specific Java implementation*. Carefully reading (and understanding!) the spec is a more sound approach. – Stephen C Jan 07 '19 at 14:18
1

@talex I tested it like below and you seem to be right:

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));
    }
}

Output is true

EDIT: @Maarten I think you are right. Especially if you are running within an application container like Websphere or Weblogic there might be multiple class loaders in play which could screw this up. So in the end the simplest correct solution would be to just use the Class instance itself.

Adriaan Koster
  • 15,870
  • 5
  • 45
  • 60
  • I am still worried if I read something like this: https://stackoverflow.com/questions/11759414/java-how-to-load-different-versions-of-the-same-class – Maarten Jan 07 '19 at 11:06
  • I think the wisest choice is to use the fully qualified classname String as the key and accept that weird classloader tricks break the matrix. – Adriaan Koster Jan 07 '19 at 13:44
  • As Stephen C mentiones in his answer. Two classes using the same FQDN from different classloaders will be considered as being different from each other and will cause a ClassCastException when casting. In my casethat would make the FQDN string not eligible as it would mix up classes from various class loaders. The key consisting of a combination of class loader with the FQDN would be correct then or as Stephen C states, the class itself is safe to use. – Maarten Jan 08 '19 at 10:02
1

There is a difference between run-time and compile-time type. It is possible to simultaneously load multiple classes of the same fully-qualified class name, if (and only if) they are loaded by different class loaders. Then such classes are distinct run-time types, and cannot be cast to one another, even if they are identical.

Hence the answer to your question depends simply on which effect you consider desirable:

  • If you desire to treat those separately loaded and incompatible classes as distinct in your map, use Class as the key. There will never be more than one live instance of Class with the same name and loader, so the Class class correctly does not override the hashCode and equals methods. So it is fine to use it as a HashMap key, although IdentityHashMap will give the same behavior, probably more efficiently.

  • If you desire to distinguish classes based only on their name, regardless of how (or whether) they were loaded, then use their string name as the map key.

Boann
  • 48,794
  • 16
  • 117
  • 146
0

I would consider using IdentityHashMap. It does not rely on equals.

This class is designed for use only in the rare cases wherein reference-equality semantics are required.

OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213