Как должны быть реализованы типы иерархии типов?

Когда дженерики были добавлены в 1.5, java.lang.reflect добавил интерфейс Type с различными подтипами для представления типов. Class модифицирован для реализации Type для типов до 1.5. Type подтипы доступны для новых типов универсального типа от 1,5.

Это все хорошо. Немного неловко, так как Type должен быть опущен, чтобы сделать что-нибудь полезное, но выполнимое с пробным, ошибочным, изменчивым и (автоматическим) тестированием. За исключением случаев, когда речь идет о реализации...

Как должно быть реализовано equals и hashCode. Описание API для подтипа ParameterizedType Type говорит:

Экземпляры классов, которые реализуют этот интерфейс, должны реализовывать метод equals(), который приравнивает любые два экземпляра, которые совместно используют одно и то же объявление универсального типа и имеют одинаковые параметры типа.

(Я предполагаю, что это означает getActualTypeArguments и getRawType но не getOwnerType??)

Из общего контракта java.lang.Object мы знаем, что hashCode также должен быть реализован, но, похоже, не существует спецификации относительно того, какие значения должен генерировать этот метод.

Ни один из других подтипов Type видимому, не упоминает equals или hashCode, за исключением того, что у Class есть отдельные экземпляры для каждого значения.

Так, что я помещаю в мои equals и hashCode?

(В случае, если вам интересно, я TypeVariable<?> заменить параметры типа для реальных типов. Поэтому, если я знаю, что во время выполнения TypeVariable<?> T равен Class<?> String тогда я хочу заменить Type s, поэтому List<T> становится List<String>, T[] становится String[], List<T>[] (может случиться!) Становится List<String>[] и т.д.)

Или я должен создать свою собственную параллельную иерархию типов типов (без дублирования Type по предполагаемым юридическим причинам)? (Есть ли библиотека?)

Изменить: было несколько вопросов о том, почему мне это нужно. Действительно, зачем вообще смотреть на информацию общего типа?

Я начинаю с неуниверсального типа класса/интерфейса. (Если вам нужны параметризованные типы, такие как List<String> тогда вы всегда можете добавить слой косвенности с новым классом.) Затем я следую за полями или методами. Они могут ссылаться на параметризованные типы. Пока они не используют подстановочные знаки, я все еще могу определять реальные статические типы, когда сталкиваюсь с подобными T

Таким образом, я могу делать все с помощью качественной статической печати. Ни один из этих instanceof динамических проверок типов не виден.

Конкретное использование в моем случае это сериализация. Но это может относиться к любому другому разумному использованию отражения, например, к тестированию.

Текущее состояние кода, который я использую для замены ниже. typeMap - это Map<String,Type>. Представлено как снимок "как есть". В любом случае не прибрано (throw null; если не веришь мне).

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

Ответы

Ответ 1

Я решал эту проблему неудовлетворительно в течение 10 лет. Сначала с Guices MoreTypes.java, скопированными и отредактированными с помощью Gsons GsonTypes.java, и снова в Moshis Util.java.

У Моши лучший подход, который не говорит, что это хорошо.

Вы не можете вызвать equals() в произвольных реализациях Type и ожидать, что это сработает.

Это потому, что API типов Java предлагают несколько несовместимых способов моделирования массивов простых классов. Вы можете сделать Date[] как Class<Date[]> или как GenericArrayType, тип компонента которого является Date. Я полагаю, что вы получите первое из отражения в поле типа Date[] а второе - в качестве параметра поля типа List<Date[]>.

Хеш-коды не указаны.

Я также начал работать над реализацией этих классов, которые использует Android. В самых ранних версиях Android хэш-коды отличаются от Java, но сегодня все, что вы найдете в дикой природе, использует те же хеш-коды, что и Java.

Методы toString не хороши

Если вы используете типы в сообщениях об ошибках, отстойно, что вам нужно написать специальный код, чтобы красиво их печатать.

Копировать Вставить и быть грустным

Я рекомендую не использовать equals() + hashCode() с неизвестными реализациями Type. Используйте функцию канонизации для преобразования в конкретную известную реализацию и сравнивайте только те, которыми вы управляете.

Ответ 2

Вот небольшой эксперимент, который опирается непосредственно на Sun API и рефлексию (то есть использует рефлексию для работы с классами, которые реализуют рефлексию):

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

Результат замены E на String в List<E> выглядит следующим образом:

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

Основная идея заключается в следующем:

  • Получить классы sun.reflect.generics.reflectiveObjects.XyzImpl
  • Получить их конструкторы, убедиться, что они accessible
  • .newInstance вызовы конструктора .newInstance во вспомогательных методах
  • Используйте вспомогательные методы в простом рекурсивном методе под названием substituteTypeVariable который перестраивает Type -expressions с переменными типа, замененными конкретными типами.

Я не реализовывал каждый отдельный случай, но он должен работать и с более сложными вложенными типами (из-за рекурсивного вызова substituteTypeVariable).

Компилятору не очень нравится этот подход, он генерирует предупреждения об использовании внутреннего API Sun:

предупреждение: ParameterizedTypeImpl является внутренним частным API и может быть удален в будущем выпуске

но для этого есть @SuppressWarnings.

Приведенный выше Java-код был получен путем перевода следующего небольшого фрагмента Scala (вот почему код Java может выглядеть немного странно и не совсем Java-идиоматическим):

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