samedi 9 août 2014

Java - différent type de retour d'une méthode générique selon le lieu de l'invocation - Stack Overflow


I have the following method with generics that executes the getter of each item in the list it receives:


public static <T, S> List<S> getValues(List<T> list, String fieldName) {
List<S> ret = new ArrayList<S>();
String methodName = "get" + fieldName.substring(0, 1).toUpperCase()
+ fieldName.substring(1, fieldName.length());
try {
if (list != null && !list.isEmpty()) {
for (T t : list) {
ret.add((S) t.getClass().getMethod(methodName).invoke(t));
}
}
} catch (IllegalArgumentException e) {
} catch (SecurityException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
} catch (NoSuchMethodException e) {
}
return ret;
}

It works perfectly fine if I call it as this:


List<Integer> ids = getValues(List<MyDTO>, "id");
request.setListIds(ids);

But, it gives me a compile error if I do it in a single line:


request.setListIds(getValues(List<MyDTO>, "id"));

The error says:



The method setListIds(List-Integer-) in the type MyDTO is not applicable for the arguments (List-Object-)



So, when I try to directly set the list, it is casting the generic to Object instead of Integer. Why is that?




It is due to Java's quite weak type inference. It can infer the type when you directly assign to a variable, but it won't infer by the target argument type, which you need in the second example.


You can overcome this with this.<Integer>getValues...




Apparently the compiler is not correctly guessing the generic type variables. In the assigment, he correctly guesses S=Integer, while when passing the result as parameter, it is not taking into account the generic type of the method parameter.


This is because of type-erasure, since the signature of the method at runtime is setListIds(List), not setListIds(List<Integer>). By the way the same question was asked here, and the answer explains why the compiler behaves like that.




No cast took place in the actual compiled bytecode of the method, because of type erasure.


When generating bytecode, the compiler treats any variable of a parameter type as having the same type as that parameter type's upper bound, or Object if the type is unbounded. So if S in your method had the constraint <S extends Integer>, the compiler would have inserted a cast to Integer. However, as S is unbounded, any references to S are treated in the bytecode as being of type Object - thus, no cast.


With your method as written, you could get rid of the compile error by filling in the type parameters when invoking the method:


YourClass.<MyDTO, Integer>getValues(list, "id")

Though as this looks clunky, you'd probably do well to get rid of the type parameter T.




Change your method signature like:


public static <T, S> List<S> getValues(List<T> list, String fieldName, Class<S> fieldType) {
//Your code goes here
}

Then following code will work seamlessly:


request.setListIds(getValues(List<MyDTO>, "id", Integer.class));


I have the following method with generics that executes the getter of each item in the list it receives:


public static <T, S> List<S> getValues(List<T> list, String fieldName) {
List<S> ret = new ArrayList<S>();
String methodName = "get" + fieldName.substring(0, 1).toUpperCase()
+ fieldName.substring(1, fieldName.length());
try {
if (list != null && !list.isEmpty()) {
for (T t : list) {
ret.add((S) t.getClass().getMethod(methodName).invoke(t));
}
}
} catch (IllegalArgumentException e) {
} catch (SecurityException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
} catch (NoSuchMethodException e) {
}
return ret;
}

It works perfectly fine if I call it as this:


List<Integer> ids = getValues(List<MyDTO>, "id");
request.setListIds(ids);

But, it gives me a compile error if I do it in a single line:


request.setListIds(getValues(List<MyDTO>, "id"));

The error says:



The method setListIds(List-Integer-) in the type MyDTO is not applicable for the arguments (List-Object-)



So, when I try to directly set the list, it is casting the generic to Object instead of Integer. Why is that?



It is due to Java's quite weak type inference. It can infer the type when you directly assign to a variable, but it won't infer by the target argument type, which you need in the second example.


You can overcome this with this.<Integer>getValues...



Apparently the compiler is not correctly guessing the generic type variables. In the assigment, he correctly guesses S=Integer, while when passing the result as parameter, it is not taking into account the generic type of the method parameter.


This is because of type-erasure, since the signature of the method at runtime is setListIds(List), not setListIds(List<Integer>). By the way the same question was asked here, and the answer explains why the compiler behaves like that.



No cast took place in the actual compiled bytecode of the method, because of type erasure.


When generating bytecode, the compiler treats any variable of a parameter type as having the same type as that parameter type's upper bound, or Object if the type is unbounded. So if S in your method had the constraint <S extends Integer>, the compiler would have inserted a cast to Integer. However, as S is unbounded, any references to S are treated in the bytecode as being of type Object - thus, no cast.


With your method as written, you could get rid of the compile error by filling in the type parameters when invoking the method:


YourClass.<MyDTO, Integer>getValues(list, "id")

Though as this looks clunky, you'd probably do well to get rid of the type parameter T.



Change your method signature like:


public static <T, S> List<S> getValues(List<T> list, String fieldName, Class<S> fieldType) {
//Your code goes here
}

Then following code will work seamlessly:


request.setListIds(getValues(List<MyDTO>, "id", Integer.class));

0 commentaires:

Enregistrer un commentaire