Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should Type type-hierarchy types be implemented?

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;
      }
   }
like image 939
Tom Hawtin - tackline Avatar asked Dec 27 '18 16:12

Tom Hawtin - tackline


2 Answers

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.

like image 129
Jesse Wilson Avatar answered Oct 12 '22 23:10

Jesse Wilson


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)
  }
}
like image 37
Andrey Tyukin Avatar answered Oct 13 '22 01:10

Andrey Tyukin