14

When generics were added to 1.5, java.lang.reflect added a Type interface with various subtypes to represent types. Class is retrofitted to implement Type for the pre-1.5 types. Type subtypes are available for the new types of generic type from 1.5.

This is all well and good. A bit awkward as Type has to be downcast to do anything useful, but doable with trial, error, fiddling and (automatic) testing. Except when it comes to implementation...

How should equals and hashCode be implemented. The API description for the ParameterizedType subtype of Type says:

Instances of classes that implement this interface must implement an equals() method that equates any two instances that share the same generic type declaration and have equal type parameters.

(I guess that means getActualTypeArguments and getRawType but not getOwnerType??)

We know from the general contract of java.lang.Object that hashCode must also be implemented, but there appears to be no specification as what values this method should produce.

None of the other subtype of Type appear to mention equals or hashCode, other than that Class has distinct instances per value.

So what do I put in my equals and hashCode?

(In case you are wondering, I am attempting to substitute type parameters for actual types. So if I know at runtime TypeVariable<?> T is Class<?> String then I want to replace Types, so List<T> becomes List<String>, T[] becomes String[], List<T>[] (can happen!) becomes List<String>[], etc.)

Or do I have to create my own parallel type type hierarchy (without duplicating Type for presumed legal reasons)? (Is there a library?)

Edit: There's been a couple of queries as to why I need this. Indeed, why look at generic type information at all?

I'm starting with a non-generic class/interface type. (If you want a parameterised types, such as List<String> then you can always add a layer of indirection with a new class.) I am then following fields or methods. Those may reference parameterised types. So long as they aren't using wildcards, I can still work out actual static types when faced with the likes of T.

In this way I can do everything with high quality, static typing. None of these instanceof dynamic type checks in sight.

The specific usage in my case is serialisation. But it could apply to any other reasonable use of reflection, such as testing.

Current state of code I am using for the substitution below. typeMap is a Map<String,Type>. Present as an "as is" snapshot. Not tidied up in anyway at all (throw null; if you don't believe me).

   Type substitute(Type type) {
      if (type instanceof TypeVariable<?>) {
         Type actualType = typeMap.get(((TypeVariable<?>)type).getName());
         if (actualType instanceof TypeVariable<?>) { throw null; }
         if (actualType == null) {
            throw new IllegalArgumentException("Type variable not found");
         } else if (actualType instanceof TypeVariable<?>) {
            throw new IllegalArgumentException("TypeVariable shouldn't substitute for a TypeVariable");
         } else {
            return actualType;
         }
      } else if (type instanceof ParameterizedType) {
         ParameterizedType parameterizedType = (ParameterizedType)type;
         Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
         int len = actualTypeArguments.length;
         Type[] actualActualTypeArguments = new Type[len];
         for (int i=0; i<len; ++i) {
            actualActualTypeArguments[i] = substitute(actualTypeArguments[i]);
         }
         // This will always be a Class, wont it? No higher-kinded types here, thank you very much.
         Type actualRawType = substitute(parameterizedType.getRawType());
         Type actualOwnerType = substitute(parameterizedType.getOwnerType());
         return new ParameterizedType() {
            public Type[] getActualTypeArguments() {
               return actualActualTypeArguments.clone();
            }
            public Type getRawType() {
               return actualRawType;
            }
            public Type getOwnerType() {
               return actualOwnerType;
            }
            // Interface description requires equals method.
            @Override public boolean equals(Object obj) {
               if (!(obj instanceof ParameterizedType)) {
                  return false;
               }
               ParameterizedType other = (ParameterizedType)obj;
               return
                   Arrays.equals(this.getActualTypeArguments(), other.getActualTypeArguments()) &&
                   this.getOwnerType().equals(other.getOwnerType()) &&
                   this.getRawType().equals(other.getRawType());
            }
         };
      } else if (type instanceof GenericArrayType) {
         GenericArrayType genericArrayType = (GenericArrayType)type;
         Type componentType = genericArrayType.getGenericComponentType();
         Type actualComponentType = substitute(componentType);
         if (actualComponentType instanceof TypeVariable<?>) { throw null; }
         return new GenericArrayType() {
            // !! getTypeName? toString? equals? hashCode?
            public Type getGenericComponentType() {
               return actualComponentType;
            }
            // Apparently don't have to provide an equals, but we do need to.
            @Override public boolean equals(Object obj) {
               if (!(obj instanceof GenericArrayType)) {
                  return false;
               }
               GenericArrayType other = (GenericArrayType)obj;
               return
                   this.getGenericComponentType().equals(other.getGenericComponentType());
            }
         };
      } else {
         return type;
      }
   }
Tom Hawtin - tackline
  • 145,806
  • 30
  • 211
  • 305
  • Why do you need to add equals/hashCode? Are you sure the builtin types are not correct? – Peter Lawrey Dec 27 '18 at 16:22
  • @PeterLawrey I need to check that the Types match. How could the builtin implementations be equal when there doesn't appear to be a spec. I mean you could probably work out what the implementation does by observation - it's not something you are going to use cryptography on. – Tom Hawtin - tackline Dec 27 '18 at 16:25
  • 1
    The implementations appear to have these methods from reading the source. You could try creating your own system, but that seems like a lot of work given it might never be needed. A simple solution is to compare the `toString()` results. – Peter Lawrey Dec 27 '18 at 16:30
  • @PeterLawrey I'm not sure that the `toString` is guaranteed to be unique. (Also bleurgh, what a hack. And not going to fit in my `HashMap` nicely.) – Tom Hawtin - tackline Dec 27 '18 at 16:32
  • It's not unique if you have multiple class loaders involved, this is where using a `Type` is better. You could duplicate the code for the different types is you are concerned you are running on a JVM which doesn't support equals/hashCode as you need it to. – Peter Lawrey Dec 27 '18 at 16:35
  • @PeterLawrey What do you mean "different types" - types or type of Types. It's reflection all the way down, so I don't know these types. I can't do a full implementation of the `Type` type hierarchy because of `Class`. I could resort to duplicating the whole API (without copying any of it for the usual legal reasons). – Tom Hawtin - tackline Dec 27 '18 at 16:40
  • (Or create a `ClassLoader` load a class that has the required type and reflect on that... Though that doesn't work if there are multiple class loaders.) – Tom Hawtin - tackline Dec 27 '18 at 16:49
  • There are four classes here for describing generic types. http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/sun/reflect/generics/reflectiveObjects/ – Peter Lawrey Dec 27 '18 at 16:53
  • Not a direct answer, but you might want to look at the [implementations of Type subtypes in Guava](https://github.com/google/guava/blob/master/guava/src/com/google/common/reflect/Types.java). And [this bug report](https://github.com/google/guava/issues/1635) gives an idea of the difficulties involved in such an implementation. – Daniel Pryden Dec 29 '18 at 20:08
  • This topic is very interesting; do you have any concrete example? – Bsquare ℬℬ Dec 30 '18 at 15:40
  • Could you maybe provide an example of a type with type variables on which you would like to perform an interesting substitution? It's not quite clear where you get those complex type expressions, and how you want to use them afterwards. – Andrey Tyukin Jan 02 '19 at 05:20
  • @TomHawtin-tackline could you explain a bit around your need? For what exactly do you need this Type? Maybe I could help you better if I understand the problem behind your question? – cilap Jan 03 '19 at 14:02
  • I think `equals` should compare the owner type as well. For `hashCode`, just do a typical `Objects.hashCode` for `rawType` and `ownerType` and `Arrays.hashCode` for `actualTypeArguments`. Use either an XOR or an IDE generated hash to blend each field's hash code into a final result. I would also be extremely careful about having only ONE instance of this `ParameterizedType` anonymous inner class for each parameterized generic type. I still don't get how you'd use this method, because all generics type information is erased at runtime, except for supertypes, but I guess you already know this... – fps Jan 04 '19 at 13:52
  • 1
    Additionally, be extra careful with recursive types, i.e. `>` – fps Jan 04 '19 at 13:55
  • `typeMap.get(((TypeVariable>)type).getName())` looks highly dubious. Even a single generic declaration can depend on multiple type variables with the same name. – Holger Jan 08 '19 at 08:55
  • @FedericoPeraltaSchaffner Despite XOR is often used in `hashCode` implementations, it’s a poor choice. Even in the case of `Map.Entry`, where the specification mandates to use it, it was a wrong decision. XOR is symmetric, so it produces the same result for `a ^ b` and `b ^ a`, which is not intended when two elements have different semantics. Further, equal bits cancel out with XOR, to the extend that `x ^ x` results in zero. If elements truly have the same semantics (like the elements of a set), you should use PLUS (`+`) instead. That’s basically an “XOR with carry”, easily solving the issue. – Holger Jan 08 '19 at 09:12
  • @Holger I always thought XOR was chosen because of 50-50% distribution of 0 and 1... Interesting point – Eugene Jan 08 '19 at 22:16
  • @Eugene well, a lot of people use it because it does “something with the bits”, without ever evaluating the actual impact on what it is supposed to support. Thankfully, there’s `Object.hash(…)` now, providing a reasonable result for those who don’t want to think about it further. For a fun task, try to predict the result of `Map.of("foo", "foo", 1000L, 1000L, 42.001, 42.001).hashCode()` (experts do it in less than one second)… – Holger Jan 09 '19 at 17:20
  • @Holger I can only assume its zero or 31, no laptop on vacation to actually test – Eugene Jan 09 '19 at 17:27
  • @Eugene you only need to read the documentation of [`Map.hashCode`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Map.html#hashCode()), which refers to [`Map.Entry.hashCode`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Map.Entry.html#hashCode()) and recall what I said about the XOR. The wording of `Map.hashCode`’s is a bit complicated, considering that it simply is the same contract as for `entrySet().hashCode()` so they could have said just that. Spoiler: there’s no 31 involved. – Holger Jan 10 '19 at 08:17

2 Answers2

6

I've been solving this problem in unsatisfying ways for 10 years. First with Guice’s MoreTypes.java, copy-pasted and revised with Gson’s GsonTypes.java, and again in Moshi’s Util.java.

Moshi has my best approach, which isn't to say that it's good.

You can't call equals() on arbitrary implementations of Type and expect it to work.

This is because the Java Types APIs offers multiple incompatible ways to model arrays of simple classes. You can make a Date[] as a Class<Date[]> or as a GenericArrayType whose component type is Date. I believe you’ll get the former from reflection on a field of type Date[] and the latter from reflection as the parameter of a field of type List<Date[]>.

The hash codes aren't specified.

I also got to work on the implementation of these classes that Android uses. Very early versions of Android have different hash codes vs. Java, but everything you'll find in the wild today uses the same hash codes as Java.

The toString methods aren't good

If you're using types in error messages it sucks to have to write special code to print them nicely.

Copy Paste and Be Sad

My recommendation is to not use equals() + hashCode() with unknown Type implementations. Use a canonicalize function to convert into a specific known implementation and only compare within the ones you control.

Jesse Wilson
  • 39,078
  • 8
  • 121
  • 128
4

Here is a little experiment that relies directly on the Sun API and reflection (that is, it uses reflection to work with classes that implement reflection):

import java.lang.Class;
import java.lang.reflect.*;
import java.util.Arrays;
import sun.reflect.generics.reflectiveObjects.*;

class Types {

  private static Constructor<ParameterizedTypeImpl> PARAMETERIZED_TYPE_CONS =
    ((Constructor<ParameterizedTypeImpl>)
      ParameterizedTypeImpl
      .class
      .getDeclaredConstructors()
      [0]
    );

  static {
      PARAMETERIZED_TYPE_CONS.setAccessible(true);
  }

  /** 
   * Helper method for invocation of the 
   *`ParameterizedTypeImpl` constructor. 
   */
  public static ParameterizedType parameterizedType(
    Class<?> raw,
    Type[] paramTypes,
    Type owner
  ) {
    try {
      return PARAMETERIZED_TYPE_CONS.newInstance(raw, paramTypes, owner);
    } catch (Exception e) {
      throw new Error("TODO: better error handling", e);
    }
  }

  // (similarly for `GenericArrayType`, `WildcardType` etc.)

  /** Substitution of type variables. */
  public static Type substituteTypeVariable(
    final Type inType,
    final TypeVariable<?> variable,
    final Type replaceBy
  ) {
    if (inType instanceof TypeVariable<?>) {
      return replaceBy;
    } else if (inType instanceof ParameterizedType) {
      ParameterizedType pt = (ParameterizedType) inType;
      return parameterizedType(
        ((Class<?>) pt.getRawType()),
        Arrays.stream(pt.getActualTypeArguments())
          .map((Type x) -> substituteTypeVariable(x, variable, replaceBy))
          .toArray(Type[]::new),
        pt.getOwnerType()
      );
    } else {
      throw new Error("TODO: all other cases");
    }
  }

  // example
  public static void main(String[] args) throws InstantiationException {

    // type in which we will replace a variable is `List<E>`
    Type t = 
      java.util.LinkedList
      .class
      .getGenericInterfaces()
      [0];

    // this is the variable `E` (hopefully, stability not guaranteed)
    TypeVariable<?> v = 
      ((Class<?>)
        ((ParameterizedType) t)
        .getRawType()
      )
      .getTypeParameters()
      [0];

    // This should become `List<String>`
    Type s = substituteTypeVariable(t, v, String.class);

    System.out.println("before: " + t);
    System.out.println("after:  " + s);
  }
}

The result of substitution of E by String in List<E> looks as follows:

before: java.util.List<E>
after:  java.util.List<java.lang.String>

The main idea is as follows:

  • Get the sun.reflect.generics.reflectiveObjects.XyzImpl classes
  • Get their constructors, ensure that they are accessible
  • Wrap the constructor .newInstance invocations in helper methods
  • Use the helper methods in a simple recursive method called substituteTypeVariable that rebuilds the Type-expressions with type variables substituted by concrete types.

I didn't implement every single case, but it should work with more complicated nested types too (because of the recursive invocation of substituteTypeVariable).

The compiler doesn't really like this approach, it generates warnings about the usage of the internal Sun API:

warning: ParameterizedTypeImpl is internal proprietary API and may be removed in a future release

but, there is a @SuppressWarnings for that.

The above Java code has been obtained by translating the following little Scala snippet (that's the reason why the Java code might look a bit strange and not entirely Java-idiomatic):

object Types {

  import scala.language.existentials // suppress warnings
  import java.lang.Class
  import java.lang.reflect.{Array => _, _}
  import sun.reflect.generics.reflectiveObjects._

  private val ParameterizedTypeCons = 
    classOf[ParameterizedTypeImpl]
    .getDeclaredConstructors
    .head
    .asInstanceOf[Constructor[ParameterizedTypeImpl]]

  ParameterizedTypeCons.setAccessible(true)

  /** Helper method for invocation of the `ParameterizedTypeImpl` constructor. */
  def parameterizedType(raw: Class[_], paramTypes: Array[Type], owner: Type)
  : ParameterizedType = {
    ParameterizedTypeCons.newInstance(raw, paramTypes, owner)
  }

  // (similarly for `GenericArrayType`, `WildcardType` etc.)

  /** Substitution of type variables. */
  def substituteTypeVariable(
    inType: Type,
    variable: TypeVariable[_],
    replaceBy: Type
  ): Type = {
    inType match {
      case v: TypeVariable[_] => replaceBy
      case pt: ParameterizedType => parameterizedType(
        pt.getRawType.asInstanceOf[Class[_]],
        pt.getActualTypeArguments.map(substituteTypeVariable(_, variable, replaceBy)),
        pt.getOwnerType
      )
      case sthElse => throw new NotImplementedError()
    }
  }

  // example
  def main(args: Array[String]): Unit = {

    // type in which we will replace a variable is `List<E>`
    val t = 
      classOf[java.util.LinkedList[_]]
      .getGenericInterfaces
      .head

    // this is the variable `E` (hopefully, stability not guaranteed)
    val v = 
      t
      .asInstanceOf[ParameterizedType]
      .getRawType
      .asInstanceOf[Class[_]]          // should be `List<E>` with parameter
      .getTypeParameters
      .head                            // should be `E`

    // This should become `List<String>`
    val s = substituteTypeVariable(t, v, classOf[String])

    println("before: " + t)
    println("after:  " + s)
  }
}
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93