2011-02-02 9 views
8

Dato il seguente codice di scheletro, è possibile determinare che la proprietà foo sia in realtà di tipo String?Generics e 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]); 
       } 
      } 
     } 
    } 
} 

L'uscita effettiva è

foo di classe java.lang.Object
TypeVariable: T classe java.lang.Object

Edit: avrei dovuto mentionend che conosco la cancellazione dei tipi e che il metodo restituisca di fatto un oggetto a livello di bytecode. Tuttavia, i metadati relativi ai tipi generici sono disponibili nel file di classe e possono essere interrogati per riflessione come nel codice di esempio. Ecco un altro frammento che mostra che SubBean infatti ha un parametro di tipo di tipo String:

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

uscita:

classe java.lang.String

La domanda rimane quindi, come relaziono questo argomento di tipo effettivo con la variabile type? Se so che esiste un solo parametro di tipo, questo è semplice, ma vorrei che questo codice funzioni anche per i bean che hanno più parametri di tipo generico.

+1

La libreria java ClassMate descritta all'indirizzo http://www.cowtowncoder.com/blog/archives/2012/04/entry_471.html sembra supportare in modo soddisfacente la risoluzione di tipi generici. Il progetto è disponibile su github: https://github.com/cowtowncoder/java-classmate –

+0

A partire da Java 1.7.0_80 l'output è 'foo di classe java.lang.String TypeVariable: classe T java.lang.Object' –

risposta

5

Finché classe runtime dell'oggetto determina il valore del parametro type , è possibile dedurre il suo valore effettivo, sostituendo in modo ricorsivo parametri di tipo formale da quelli effettivi ottenuti da Class.getGenericSuperClass(): codice

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

prova:

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

Se invece la classe non determina il valore del parametro type, sarà necessario estendere il bean per mantenere l'oggetto classe per il parametro di tipo corrente in fase di esecuzione con altri mezzi, ad es. facendo:

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

Generazione di tipi di errore generici Java durante la compilazione. Al momento dell'esecuzione, non è possibile determinare il tipo di T che era presente durante la compilazione.

Questo è il link: type erasure

+1

Ho paura che sia uno dei casi in cui il tutorial di Java non dice tutta la verità. Quello che intendono dire è che i parametri di tipo non esistono nel bytecode. I tipi dichiarati, anche se sono generici, sono presenti nel file di classe e possono essere ispezionati utilizzando il reflection. Nell'esempio di codice della domanda, tale informazione è sufficiente per ricostruire il parametro type in fase di esecuzione. – meriton

0

Purtroppo, no:

Generics sono implementati per tipologia di cancellazione: informazioni di tipo generico è presente solo in fase di compilazione, dopo di che si è cancellato dal compilatore. Il vantaggio principale di questo approccio è che fornisce un'interoperabilità totale tra codice generico e codice legacy che utilizza tipi non parametrizzati (che sono tecnicamente noti come tipi non elaborati). Gli svantaggi principali sono che le informazioni sul tipo di parametro non sono disponibili in fase di esecuzione e che i cast generati automaticamente possono fallire quando si interagisce con codice legacy mal funzionante. Esiste, tuttavia, un modo per ottenere la sicurezza del tipo di runtime garantita per le raccolte generiche anche quando si interagisce con codice legacy mal funzionante.

come citato da http://download.oracle.com/javase/1.5.0/docs/guide/language/generics.html

+2

Ho paura che sia uno dei casi in cui il tutorial di Java non dice tutta la verità. Quello che intendono dire è che i parametri di tipo non esistono nel bytecode. I tipi dichiarati, anche se sono generici, sono presenti nel file di classe e possono essere ispezionati utilizzando il reflection. Nell'esempio di codice della domanda, tale informazione è sufficiente per ricostruire il parametro type in fase di esecuzione. – meriton

2

È possibile ottenere il tipo di runtime di generici per mezzo di this hack. Codice estratto dal link.

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

Ecco il codice di byte di 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 
} 

Come si può vedere, sia il getter e setter sono del tipo java.lang.Object. Introspector usa Getter e Setter per generare PropertyDescriptor (i campi vengono ignorati), quindi non è possibile che la Proprietà conosca il tipo generico di T.

+0

L'oggetto oggetto di ispezione è di tipo 'SubBean', non' SuperBean'. Il file di classe di 'SubBean' contiene la clausola extends completa, che rivela che' T' sta per String per le istanze di 'SubBean'. Questa informazione è accessibile in fase di esecuzione. – meriton

+0

@meriton Vero (credo di non aver dato abbastanza un'occhiata alla domanda). Tuttavia, i metodi sono definiti nella superclasse e Introspector utilizza quelle definizioni e ignora le informazioni di tipo generico del bambino. –

1

Sfortunatamente, il tipo di cancellazione è in vigore.

Anche se sembra che il SubBean debba avere un tipo fisso di String per quel ivar e quei metodi perché il parametro type per SuperBean è noto in fase di compilazione, sfortunatamente, non è il modo in cui funziona. Il compilatore non crea una versione -ified String di SuperBean a tempo di compilazione per SubBean derivare da - c'è un solo (tipo cancellata) SuperBean

Uno possibilmente brutta soluzione che viene in mente è però che SubBean può essere in grado di eseguire l'override del metodo della superclasse con la versione specifica per tipo e poi BeanInfo può restituire ciò che ci si aspetta per i metodi:

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

Aggiornamento: Quanto sopra non funziona. Nota questa informazione che @ Jörn Horstmann post nei commenti:

Questo non sembra funzionare in quanto l'Introspector sta ancora restituendo un metodo di lettura di tipo Oggetto. Inoltre, questo sembra essere un metodo bridge generato (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102) il che significa che potrei incorrere in bugs.sun.com/view_bug.do?bug_id=6788525 se voglio accedere alle annotazioni su questo metodo.

Un'altra brutta variante della soluzione precedente è quello di alias proprietà:

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 ora ha una struttura distinta FooItem che è un alias per la struttura SuperBeanFoo originale.

+1

Questo non sembra funzionare poiché Introspector sta ancora restituendo un metodo di lettura di tipo Object. Inoltre, questo sembra essere un metodo bridge generato (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102), il che significa che potrei incontrare http://bugs.sun.com/view_bug.do ? bug_id = 6788525 se voglio accedere alle annotazioni su questo metodo. –

+0

@ Jörn Horstmann - Grazie per l'aggiornamento. Mi dispiace che non abbia funzionato per te - sembrava come dovrebbe, ma date le informazioni sui metodi di bridge generati dal compilatore, posso vedere la confusione. L'unico altro suggerimento che posso pensare è implementare una proprietà alias, ad es. 'public String getFooItem() {return super.getFoo(); } 'dà a' SubBean' una distinta proprietà 'FooItem' che è in realtà un alias per' Foo'. –

+1

Grazie, la valutazione dei metodi è stata sicuramente degna di essere provata e ha aiutato a comprendere alcuni altri aspetti dell'implementazione dei generici. Ma il mio obiettivo è proprio quello di usarlo in una libreria per introspettare i bean arbitrari senza doverli prima modificare. –

3

Ho trovato una soluzione per il caso in cui esiste una gerarchia con una singola classe super (oltre a Object) che funziona anche quando ci sono più parametri di tipo sulla super classe.

Ancora non funzionerebbe per gerarchie più profonde o quando si implementano interfacce generiche . Vorrei anche una conferma che questo è in realtà documentato e dovrebbe funzionare.

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

uscita:

bar di classe java.lang.Object
Partita: B: classe java.lang.Integer
foo di classe java.lang.Object
Corrispondenza: F: classe java.lang.String
qux di classe java.lang.Oggetto
Partita: D: X

ho bisogno di scrivere qualche altro test per determinare che cosa fare con lo scorso ultimo caso :)