leangen / geantyref

Advanced generic type reflection library with support for working with AnnotatedTypes (for Java 8+)
Apache License 2.0
99 stars 15 forks source link

Calling hashCode on two equal `TypeToken`s returns different results #17

Closed solonovamax closed 7 months ago

solonovamax commented 1 year ago

According to the javadocs for java.lang.Object#hashCode,

If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.

Currently, TypeToken does not abide by this. It is possible to get two type tokens where first.equals(second) is true, but first.hashCode() == second.hashCode() is false. This violates the contract of hashCode and breaks any attempt to use TypeToken as the key to a HashMap.

Here is some example code that can reproduce the issue:

import io.leangen.geantyref.TypeToken;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Test {
    public static void main(String[] args) throws NoSuchMethodException {
        TypeToken<Set<? extends Test>> testTypeToken = new TypeToken<>() {};
        Method method = Test.class.getMethod("testFunction", Set.class);
        TypeToken<?> otherTypeToken = TypeToken.get(method.getParameters()[0].getParameterizedType());

        Map<TypeToken<?>, String> testMap = new HashMap<>();
        testMap.put(otherTypeToken, "test");

        System.out.println("testTypeToken: " + testTypeToken);
        System.out.println("testTypeToken.getType(): " + testTypeToken.getType());
        System.out.println("testTypeToken.hashCode(): " + testTypeToken.hashCode());
        System.out.println();
        System.out.println("otherTypeToken: " + otherTypeToken);
        System.out.println("otherTypeToken.getType(): " + otherTypeToken.getType());
        System.out.println("otherTypeToken.hashCode(): " + otherTypeToken.hashCode());
        System.out.println();
        System.out.println("testTypeToken.equals(otherTypeToken): " + testTypeToken.equals(otherTypeToken));
        System.out.println("otherTypeToken.equals(testTypeToken): " + otherTypeToken.equals(testTypeToken));
        System.out.println();
        System.out.println("testMap: " + testMap);
        System.out.println("testMap.get(testTypeToken): " + testMap.get(testTypeToken));
    }

    public static void testFunction(Set<? extends Test> testSet) {}
}

The current output of this program is:

testTypeToken: Test$1@60f4877b
testTypeToken.getType(): java.util.Set<? extends Test>
testTypeToken.hashCode(): 1626638203

otherTypeToken: io.leangen.geantyref.TypeToken$2@17a57871
otherTypeToken.getType(): java.util.Set<? extends Test>
otherTypeToken.hashCode(): 396720241

testTypeToken.equals(otherTypeToken): true
otherTypeToken.equals(testTypeToken): true

testMap: {io.leangen.geantyref.TypeToken$2@17a57871=test}
testMap.get(testTypeToken): null

The expected output is:

 testTypeToken: Test$1@60f4877b
 testTypeToken.getType(): java.util.Set<? extends Test>
-testTypeToken.hashCode(): 1626638203
+testTypeToken.hashCode(): 396720241

 otherTypeToken: io.leangen.geantyref.TypeToken$2@17a57871
 otherTypeToken.getType(): java.util.Set<? extends Test>
 otherTypeToken.hashCode(): 396720241

 testTypeToken.equals(otherTypeToken): true
 otherTypeToken.equals(testTypeToken): true

 testMap: {io.leangen.geantyref.TypeToken$2@17a57871=test}
-testMap.get(testTypeToken): null
+testMap.get(testTypeToken): "test"

As you can see, if reflection is used to get the parameterized type of a method, and that type is then added to a hash map, you cannot access that parameterized type using one constructed via new TypeToken<Set<? extends Test>>() {}.

kaqqao commented 1 year ago

While this is a fair finding, and I will fix it, I wanted to point out TypeToken is implementing equals and hashCode more by accident than conscious choice. It wasn't exactly intended to be used as a key, rather the contained type can be turned into the "canonical" form (TypeToken#getCanonicalType, which delegates to GenericTypeReflector#toCanonical) and be used instead, or even simpler, the provided Map implementation (AnnotatedTypeMap) can be used for convenience.

solonovamax commented 1 year ago

I will forward this to the developers of the library I'm using, as they are currently using TypeToken as a key to a hash map