2011-02-02 20 views
8

Dado el siguiente esquema de código, ¿es posible determinar que la propiedad foo es de hecho del tipo String?Generics y java.beans.Introspector

public class TestIntrospection { 
    public static class SuperBean<T> { 
     private T foo; 

     public T getFoo() { return foo; } 
     public void setFoo(T foo) { this.foo = foo; } 
    } 

    public static class SubBean extends SuperBean<String> { 
    } 

    public static void main(String[] args) throws IntrospectionException { 
     BeanInfo beanInfo = Introspector.getBeanInfo(SubBean.class); 
     PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); 
     for (PropertyDescriptor prop : propertyDescriptors) { 
      if ("foo".equals(prop.getName())) { 
       System.out.printf("%s of %s\n", prop.getName(), prop.getPropertyType()); 

       Method readMethod = prop.getReadMethod(); 
       Type returnType = prop.getReadMethod().getGenericReturnType(); 
       if (returnType instanceof TypeVariable) { 
        TypeVariable t = (TypeVariable) returnType; 
        GenericDeclaration d = t.getGenericDeclaration(); 
        System.out.println("TypeVariable : " + t.getName() + " " + t.getBounds()[0]); 
       } 
      } 
     } 
    } 
} 

La salida real es

foo de la clase java.lang.Object
TypeVariable: clase T java.lang.Object

Editar: que debería haber mentionend que sé sobre el borrado de tipos y que el método está devolviendo un Objeto en el nivel de bytecode. Sin embargo, los metadatos sobre los tipos genéricos están disponibles en el archivo de clase y pueden consultarse por reflexión como en el código de muestra. Aquí hay otro fragmento que muestra que SubBean de hecho tiene un parámetro de tipo de tipo String:

   Type superClass = SubBean.class.getGenericSuperclass(); 
       ParameterizedType pt = (ParameterizedType) superClass; 
       System.out.println(pt.getActualTypeArguments()[0]); 

salida:

clase java.lang.String

La pregunta entonces sigue siendo, ¿Cómo relaciono este argumento de tipo real con la variable de tipo? Si sé que solo hay un parámetro de tipo, esto es simple, pero me gustaría que este código también funcione para los beans que tienen múltiples parámetros genéricos.

+1

La biblioteca ClassMate java descrita en http://www.cowtowncoder.com/blog/archives/2012/04/entry_471.html parece apoyar muy bien la resolución de tipos genéricos. El proyecto está disponible en github: https://github.com/cowtowncoder/java-classmate –

+0

A partir de Java 1.7.0_80 la salida es 'foo de la clase java.lang.String TypeVariable: clase T java.lang.Object' –

Respuesta

5

Mientras la clase en tiempo de ejecución del objeto determina el valor del parámetro de tipo , se puede inferir su valor real mediante la sustitución de forma recursiva los parámetros de tipo formal por parte de los reales obtenidos de Class.getGenericSuperClass (código):

class Substitution extends HashMap<String, TypeExpr> { 
    Substitution(TypeVariable[] formals, TypeExpr[] actuals) { 
     for (int i = 0; i < actuals.length; i++) { 
      put(formals[i].getName(),actuals[i]); 
     } 
    } 

} 

abstract class TypeExpr { 
    abstract TypeExpr apply(Substitution s); 

    public abstract String toString(); 

    static TypeExpr from(Type type) { 
     if (type instanceof TypeVariable) { 
      return new TypeVar((TypeVariable) type); 
     } else if (type instanceof Class) { 
      return new ClassType((Class) type); 
     } else if (type instanceof ParameterizedType) { 
      return new ClassType((ParameterizedType) type); 
     } else if (type instanceof GenericArrayType) { 
      return new ArrayType((GenericArrayType) type); 
     } else if (type instanceof WildcardType) { 
      return new WildcardTypeExpr((WildcardType) type); 
     } 
     throw new IllegalArgumentException(type.toString()); 
    } 

    static TypeExpr[] from(Type[] types) { 
     TypeExpr[] t = new TypeExpr[types.length]; 
     for (int i = 0; i < types.length; i++) { 
      t[i] = from(types[i]); 
     } 
     return t; 
    } 

    static TypeExpr[] apply(TypeExpr[] types, Substitution s) { 
     TypeExpr[] t = new TypeExpr[types.length]; 
     for (int i = 0; i < types.length; i++) { 
      t[i] = types[i].apply(s); 
     } 
     return t; 
    } 

    static void append(StringBuilder sb, String sep, Object[] os) { 
     String s = ""; 
     for (Object o : os) { 
      sb.append(s); 
      s = sep; 
      sb.append(o); 
     } 
    } 
} 

class TypeVar extends TypeExpr { 
    final String name; 

    public TypeVar(String name) { 
     this.name = name; 
    } 

    public TypeVar(TypeVariable var) { 
     name = var.getName(); 
    } 

    @Override 
    public String toString() { 
     return name; 
    } 

    @Override 
    TypeExpr apply(Substitution s) { 
     TypeExpr e = s.get(name); 
     return e == null ? this : e; 
    } 
} 

class ClassType extends TypeExpr { 
    final Class clazz; 
    final TypeExpr[] arguments; // empty if the class is not generic 

    public ClassType(Class clazz, TypeExpr[] arguments) { 
     this.clazz = clazz; 
     this.arguments = arguments; 
    } 

    public ClassType(Class clazz) { 
     this.clazz = clazz; 
     arguments = from(clazz.getTypeParameters()); 
    } 

    @Override 
    public String toString() { 
     String name = clazz.getSimpleName(); 
     if (arguments.length == 0) { 
      return name; 
     } 

     StringBuilder sb = new StringBuilder(); 
     sb.append(name); 
     sb.append("<"); 
     append(sb, ", ", arguments); 
     sb.append(">"); 
     return sb.toString(); 
    } 

    public ClassType(ParameterizedType pt) { 
     clazz = (Class) pt.getRawType(); 
     Type[] args = pt.getActualTypeArguments(); 
     arguments = TypeExpr.from(args); 
    } 

    @Override 
    ClassType apply(Substitution s) { 
     return new ClassType(clazz, apply(arguments, s)); 
    } 
} 

class ArrayType extends TypeExpr { 
    final TypeExpr componentType; 

    public ArrayType(TypeExpr componentType) { 
     this.componentType = componentType; 
    } 

    public ArrayType(GenericArrayType gat) { 
     this.componentType = TypeExpr.from(gat.getGenericComponentType()); 
    } 

    @Override 
    public String toString() { 
     return componentType + "[]"; 
    } 

    @Override 
    TypeExpr apply(Substitution s) { 
     return new ArrayType(componentType.apply(s)); 
    } 
} 

class WildcardTypeExpr extends TypeExpr { 
    final TypeExpr[] lowerBounds; 
    final TypeExpr[] upperBounds; 

    public WildcardTypeExpr(TypeExpr[] lowerBounds, TypeExpr[] upperBounds) { 
     this.lowerBounds = lowerBounds; 
     this.upperBounds = upperBounds; 
    } 

    WildcardTypeExpr(WildcardType wct) { 
     lowerBounds = from(wct.getLowerBounds()); 
     upperBounds = from(wct.getUpperBounds()); 
    } 

    @Override 
    TypeExpr apply(Substitution s) { 
     return new WildcardTypeExpr(
      apply(lowerBounds, s), 
      apply(upperBounds, s) 
     ); 
    } 

    @Override 
    public String toString() { 
     StringBuilder sb = new StringBuilder(); 
     sb.append("?"); 
     if (lowerBounds.length > 0) { 
      sb.append(" super "); 
      append(sb, " & ", lowerBounds); 
     } 
     if (upperBounds.length > 0) { 
      sb.append(" extends "); 
      append(sb, " & ", upperBounds); 
     } 
     return sb.toString(); 
    } 
} 

public class Test { 

    /** 
    * @return {@code superClazz}, with the replaced type parameters it has for 
    *   instances of {@code ct}, or {@code null}, if {@code superClazz} 
    *   is not a super class or interface of {@code ct} 
    */ 
    static ClassType getSuperClassType(ClassType ct, Class superClazz) { 
     if (ct.clazz == superClazz) { 
      return ct; 
     } 

     Substitution sub = new Substitution(ct.clazz.getTypeParameters(), ct.arguments); 

     Type gsc = ct.clazz.getGenericSuperclass(); 
     if (gsc != null) { 
      ClassType sct = (ClassType) TypeExpr.from(gsc); 
      sct = sct.apply(sub); 
      ClassType result = getSuperClassType(sct, superClazz); 
      if (result != null) { 
       return result; 
      } 
     } 

     for (Type gi : ct.clazz.getGenericInterfaces()) { 
      ClassType st = (ClassType) TypeExpr.from(gi); 
      st = st.apply(sub); 
      ClassType result = getSuperClassType(st, superClazz); 
      if (result != null) { 
       return result; 
      } 

     } 
     return null; 
    } 

    public static ClassType getSuperClassType(Class clazz, Class superClazz) { 
     return getSuperClassType((ClassType) TypeExpr.from(clazz), superClazz); 
    } 

prueba:

public static void check(Class c, Class sc, String expected) { 
     String actual = getSuperClassType(c, sc).toString(); 
     if (!actual.equals(expected)) { 
      throw new AssertionError(actual + " != " + expected); 
     } 
    } 

    public static void main(String[] args) { 
     check(Substitution.class, Map.class, "Map<String, TypeExpr>"); 
     check(HashMap.class, Map.class, "Map<K, V>"); 
     check(Bar.class, Foo.class, "Foo<List<? extends String[]>>"); 
    } 
} 

interface Foo<X> { 

} 
class SuperBar<X, Y> implements Foo<List<? extends Y[]>> { 

} 

class Bar<X> extends SuperBar<X, String> { } 

Si, por otro lado, la clase no determina el valor del parámetro de tipo, deberá extender su bean para retener el objeto de clase para el parámetro de tipo real en tiempo de ejecución por otros medios, p. haciendo:

class Super<T> { 
    final Class<T> clazz; 

    T foo; 

    Super(Class<T> clazz) { 
     this.clazz = clazz; 
    } 

    public T getFoo() { 
     return foo; 
    } 

    public T setFoo() { 
     this.foo = foo; 
    } 
} 
1

Los genéricos de Java experimentan el borrado de tipo durante la compilación. En tiempo de ejecución, no es posible determinar el tipo de T que estuvo presente durante la compilación.

Aquí hay un enlace: type erasure

+1

Me temo que es uno de los casos en que el Tutorial de Java no dice la verdad completa. Lo que quieren decir es que los parámetros de tipo no existen en bytecode. Los tipos declarados, incluso si son genéricos, están presentes en el archivo de clase y pueden inspeccionarse mediante reflexión. En el ejemplo de código de la pregunta, esa información es suficiente para reconstruir el parámetro de tipo en tiempo de ejecución. – meriton

0

Por desgracia, no:

Los genéricos son implementadas por el tipo de borrado: información de tipo genérico está presente sólo en tiempo de compilación, después de lo cual se borra por el compilador. La principal ventaja de este enfoque es que proporciona interoperabilidad total entre el código genérico y el código heredado que usa tipos no parametrizados (que técnicamente se conocen como tipos sin procesar). Las desventajas principales son que la información del tipo de parámetro no está disponible en el tiempo de ejecución, y que las conversiones generadas automáticamente pueden fallar al interoperar con el código legacy de mal comportamiento. Sin embargo, existe una forma de garantizar la seguridad del tipo de tiempo de ejecución garantizado para las colecciones genéricas, incluso cuando se interopera con el código heredado de mal comportamiento.

Como se cita http://download.oracle.com/javase/1.5.0/docs/guide/language/generics.html

+2

Me temo que es uno de los casos en que el Tutorial de Java no dice la verdad completa. Lo que quieren decir es que los parámetros de tipo no existen en bytecode. Los tipos declarados, incluso si son genéricos, están presentes en el archivo de clase y pueden inspeccionarse mediante reflexión. En el ejemplo de código de la pregunta, esa información es suficiente para reconstruir el parámetro de tipo en tiempo de ejecución. – meriton

2

Usted puede obtener el tipo de tiempo de ejecución genérica mediante this hack. Código extraído del enlace.

public class Base<T> { 

     private final Class<T> klazz; 

     @SuppressWarnings("unchecked") 
     public Base() { 
     Class<? extends Base> actualClassOfSubclass = this.getClass(); 
     ParameterizedType parameterizedType = (ParameterizedType) actualClassOfSubclass.getGenericSuperclass(); 
     Type firstTypeParameter = parameterizedType.getActualTypeArguments()[0]; 
     this.klazz = (Class) firstTypeParameter; 
     } 

     public boolean accepts(Object obj) { 
     return this.klazz.isInstance(obj); 
     } 

    } 

    class ExtendsBase extends Base<String> { 

     // nothing else to do! 

    } 
public class ExtendsBaseTest { 

    @Test 
    public void testTypeDiscovery() { 
    ExtendsBase eb = new ExtendsBase(); 
    assertTrue(eb.accepts("Foo")); 
    assertFalse(eb.accepts(123)); 
    } 
} 
0

Aquí está el código de bytes de SuperBean:

public class foo.bar.SuperBean { 

    // Field descriptor #6 Ljava/lang/Object; 
    // Signature: TT; 
    private java.lang.Object foo; 

    // Method descriptor #10()V 
    // Stack: 1, Locals: 1 
    public SuperBean(); 
    0 aload_0 [this] 
    1 invokespecial java.lang.Object() [12] 
    4 return 
     Line numbers: 
     [pc: 0, line: 3] 
     Local variable table: 
     [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean 
     Local variable type table: 
     [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean<T> 

    // Method descriptor #21()Ljava/lang/Object; 
    // Signature:()TT; 
    // Stack: 1, Locals: 1 
    public java.lang.Object getFoo(); 
    0 aload_0 [this] 
    1 getfield foo.bar.SuperBean.foo : java.lang.Object [24] 
    4 areturn 
     Line numbers: 
     [pc: 0, line: 8] 
     Local variable table: 
     [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean 
     Local variable type table: 
     [pc: 0, pc: 5] local: this index: 0 type: foo.bar.SuperBean<T> 

    // Method descriptor #27 (Ljava/lang/Object;)V 
    // Signature: (TT;)V 
    // Stack: 2, Locals: 2 
    public void setFoo(java.lang.Object foo); 
    0 aload_0 [this] 
    1 aload_1 [foo] 
    2 putfield foo.bar.SuperBean.foo : java.lang.Object [24] 
    5 return 
     Line numbers: 
     [pc: 0, line: 12] 
     [pc: 5, line: 13] 
     Local variable table: 
     [pc: 0, pc: 6] local: this index: 0 type: foo.bar.SuperBean 
     [pc: 0, pc: 6] local: foo index: 1 type: java.lang.Object 
     Local variable type table: 
     [pc: 0, pc: 6] local: this index: 0 type: foo.bar.SuperBean<T> 
     [pc: 0, pc: 6] local: foo index: 1 type: T 
} 

Como se puede ver, tanto el comprador y el colocador son del tipo java.lang.Object. Introspector usa Getters y Setters para generar el PropertyDescriptor (los campos se ignoran), por lo que no hay manera de que la Propiedad pueda conocer el tipo genérico de T.

+0

El objeto que se inspecciona es del tipo 'SubBean', no' SuperBean'. El archivo de clase de 'SubBean' contiene la cláusula extends completa, que revela que' T' en realidad representa String para instancias de 'SubBean'. Esta información es accesible en tiempo de ejecución. – meriton

+0

@meriton Verdadero (supongo que no eché un buen vistazo a la pregunta). Aún así, los métodos se definen en la superclase e Introspector usa esas definiciones e ignora la información de tipo genérico del niño. –

1

Desafortunadamente, la borradura de tipo está en plena vigencia.

Aunque parece que el SubBean debe tener un tipo fijo de Cadena para ese ivar y esos métodos porque el parámetro de tipo para SuperBean es conocido en tiempo de compilación, lamentablemente, no de la manera en que funciona. El compilador no crea una versión -ified String de SuperBean en tiempo de compilación para SubBean derivar de - sólo hay una SuperBean

Una solución posiblemente fea (tipo-borrado) que se me ocurre sin embargo es que SubBean puede ser capaz de anular el método de la superclase con la versión de tipo específico y luego BeanInfo puede devolver lo que esperas de los métodos:

public static class SubBean 
extends SuperBean<String> { 
    // Unfortunate this is necessary for bean reflection ... 
    public String getFoo()   { return super.getFoo(); } 
    public void setFoo(String foo) { super.setFoo(foo); } 
} 

Actualización: Lo anterior no funciona. Tenga en cuenta esta información que @ Jörn Horstmann publica en los comentarios:

Esto no parece funcionar ya que el Introspector todavía está devolviendo un método de lectura de tipo Objeto. Además, este parece ser un método de puente generado (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102), lo que significa que podría ejecutar en bugs.sun.com/view_bug.do?bug_id=6788525 si quiero acceder a las anotaciones en este método.

Otro feo variación de la solución anterior es un alias la propiedad:

public static class SubBean 
extends SuperBean<String> { 
    // Unfortunate this is necessary for bean reflection ... 
    public String getFooItem()   { return super.getFoo(); } 
    public void setFooItem(String foo) { super.setFoo(foo); } 
} 

SubBean tiene ahora una propiedad distinta FooItem que es un alias para el SuperBean propiedad original Foo.

+1

Esto no parece funcionar ya que el Introspector todavía está devolviendo un método de lectura de tipo Objeto. Además, esto parece ser un método de puente generado (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102) lo que significa que podría encontrar http://bugs.sun.com/view_bug.do ? bug_id = 6788525 si quiero acceder a las anotaciones en este método. –

+0

@ Jörn Horstmann - Gracias por la actualización. Lamento que no funcionó para ti, parecía que debería hacerlo, pero dada la información sobre los métodos de puente generados por el compilador, puedo ver la confusión. La única otra sugerencia que puedo pensar es implementar una propiedad de alias, p. 'public String getFooItem() {return super.getFoo(); } 'le da a' SubBean' una propiedad distintiva 'FooItem' que es realmente un alias para' Foo'. –

+1

Gracias, anular los métodos definitivamente valió la pena intentarlo y ayudó a comprender algunos otros aspectos de la implementación de genéricos. Pero mi objetivo realmente es utilizar esto en una biblioteca para introspectir beans arbitrarios sin tener que modificarlos primero. –

3

Encontré una solución para el caso donde hay una jerarquía con una única clase super (aparte de Object) que también funciona cuando hay varios parámetros de tipo en la superclase.

Todavía no funcionaría para jerarquías más profundas o al implementar interfaces genéricas . También me gustaría una confirmación de que esto está de hecho documentado y se supone que funciona.

public static class SuperBean<F, B, Q> { 
    // getters and setters 
} 

public static class SubBean<X> extends SuperBean<String, Integer, X> { 
} 

...

   Type returnType = readMethod.getGenericReturnType(); 

       Type superClass = SubBean.class.getGenericSuperclass(); 
       GenericDeclaration genericDecl = ((TypeVariable) returnType).getGenericDeclaration(); 
       TypeVariable[] parameters = genericDecl.getTypeParameters(); 
       Type[]   actualArgs = ((ParameterizedType) superClass).getActualTypeArguments(); 

       for (int i=0; i<parameters.length; i++) { 
        //System.out.println(parameters[i] + " " + actualArgs[i]); 
        if (returnType == parameters[i]) { 
         System.out.println("Match : " + parameters[i] + " : " + actualArgs[i]); 
        } 
       } 

Salida:

barra de la clase java.lang.Object
partido: B: Clase java.lang.Integer
foo de la clase java.lang.Object
Coincidencia: F: clase java.lang.String
qux de la clase java.lang.Objeto
partido: Q: X

tengo que escribir más pruebas para determinar qué hacer con la última último caso :)