Generics and java.beans.Introspector
Given the following code skeleton, is it possible to determine that the property foo
is in fact of type String
?
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]);
}
}
}
}
}
The actual output is
foo of class java.lang.Object
TypeVariable : T class java.lang.Object
Edit: I should have mentionend that I know about type erasure and that the method is in fact returning an Object on the bytecode level. Nevertheless, the metadata about generic types is available in the class fi开发者_运维问答le and can be queried by reflection as in the sample code. Here is another snippet that shows that SubBean in fact has a type parameter of type String:
Type superClass = SubBean.class.getGenericSuperclass();
ParameterizedType pt = (ParameterizedType) superClass;
System.out.println(pt.getActualTypeArguments()[0]);
output:
class java.lang.String
The question then remains, how do I relate this actual type argument to the type variable? If I know that there is only one type parameter this is simple, but I would like this code to work also for beans having multiple generic type parameters.
As long as the object's runtime class determines the value of the type parameter, you can infer its actual value by recursively replacing formal type parameters by the actual ones obtained from Class.getGenericSuperClass():
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);
}
Test code:
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> { }
If on the other hand the class does not determine the value of the type parameter, you'll have to extend your bean to retain the class object for the actual type parameter at runtime by other means, e.g. by doing:
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;
}
}
I found a solution for the case where there is hierarchy with a single super class (apart from Object) which also works when there are multiple type parameters on the super class.
Still would not work for deeper hierarchies or when implementing generic interfaces. Also I would like a confirmation that this is in fact documented and supposed to work.
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]);
}
}
Output:
bar of class java.lang.Object
Match : B : class java.lang.Integer
foo of class java.lang.Object
Match : F : class java.lang.String
qux of class java.lang.Object
Match : Q : X
I need to write some more tests to determine what to do with last last case :)
You can get runtime type of generic by means of this hack . Code extracted from the 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));
}
}
Java generics experience type erasure during compilation. At runtime, it is not possible to determine the type of T that was present during compilation.
Here is a link: type erasure
Unfortunately, type erasure is in full force.
Though it seems like the SubBean should have a fixed type of String for that ivar and those methods because the type parameter for SuperBean
is known at compile-time, unfortunately, that not the way it works. The compiler does not create aString
-ified version of SuperBean
at compile-time for SubBean
to derive from - there is only one (type-erased) SuperBean
One possibly ugly workaround that occurs to me however is that SubBean
may be able to override the superclass method with the type-specific version and then BeanInfo may return what you expect for the methods:
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); }
}
Update: The above doesn't work. Note this info that @Jörn Horstmann posts in the comments:
This does not seem to work as the Introspector is still returning a read method of type Object. Additionally, this seems to be a generated bridge method (http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102) which means I might run into bugs.sun.com/view_bug.do?bug_id=6788525 if I want to access annotations on this method.
Another ugly variation of the above workaround is to alias the property:
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
now has a distinct property FooItem
that is an alias for the original SuperBean
property Foo
.
Unfortunately, no:
Generics are implemented by type erasure: generic type information is present only at compile time, after which it is erased by the compiler. The main advantage of this approach is that it provides total interoperability between generic code and legacy code that uses non-parameterized types (which are technically known as raw types). The main disadvantages are that parameter type information is not available at run time, and that automatically generated casts may fail when interoperating with ill-behaved legacy code. There is, however, a way to achieve guaranteed run-time type safety for generic collections even when interoperating with ill-behaved legacy code.
As quoted from http://download.oracle.com/javase/1.5.0/docs/guide/language/generics.html
Here's the byte code of 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
}
As you can see, both the getter and the setter are of the type java.lang.Object. Introspector uses Getters and Setters to generate the PropertyDescriptor (fields are ignored), so there is no way the Property could know the generic type of T.
精彩评论