The following test renders fine in my IDE (Eclipse), but fails to compile when building via Maven.
The compiler error is shown in the comment line in the code block below.
It looks like the compiler is unable to determine the type of the 'o' input to the lambda. And if I cast o to the MyObj class, then it compiles fine.
I realize that this is a somewhat convoluted situation (we really do need this complexiy, though). And there really should be enough type info here for the compiler to determine the type (and the built-in compiler in Eclipse does so).
Am I doing something wrong with the generics declarations?
JDK is 21.0.5
public class AnotherTestClass {
public static class MyObj{
private final String arg1;
public MyObj(String arg1) {
this.arg1 = arg1;
}
public String getArg1() { return arg1; }
}
public static class MyFunctionHolder<T, R>{
Function<T, R> f;
public MyFunctionHolder(Function<T, R> f) {
this.f = f;
}
}
public static <C extends Collection<E>, E, R> MyFunctionHolder<C, R> forCollectionOfType(Class<? extends C> collectionClass, Class<E> elementClass, Function<C, R> function){
return new MyFunctionHolder<C, R>(function);
}
@Test
public void testMultiLevelStreams() {
List<MyObj> list = Arrays.asList(new MyObj("one"), new MyObj("two"));
MyFunctionHolder<List<MyObj>, List<String>> fh = forCollectionOfType( List.class,
MyObj.class,
l -> l.stream()
.map(o -> o.getArg1()) // compile error "cannot find symbol\n symbol: method getArg1()\n variable o of type java.lang.Object"
.toList()
);
List<String> rslt = fh.f.apply(list);
}
}
The objective is to have the static forCollectionOfType method accept the class of a collection, the class of the elements in the collection, and a function to apply to the collection itself.
One potentially useful data point is that when I do this, I get compile errors in Eclipse as well - so I'm really thinking I must be doing something wrong with the generics:
Function<List<MyObj>, List<String>> listFunction = l -> l.stream().map(o -> o.getArg1()).toList();
MyFunctionHolder<List<MyObj>, List<String>> fh2 = forCollectionOfType( List.class,
MyObj.class,
listFunction
);
Update
After reading this: Difference between <? super T> and <? extends T> in Java
I wound up making a small change to the generics in the method declaration:
Instead of Class<? extends C> collectionClass, I changed it to Class<? super C> collectionClass
The compiler error is now gone. However, I can now pass Object.class to the static method - and there is no compile time check to make sure the function generic parameters are consistent with the collection and element classes we pass in:
MyFunctionHolder<List<MyObj>, List<String>> fh = forCollectionOfType( Object.class, // Whoa - that's not a Collection!
MyObj.class,
l -> l.stream()
.map(o -> o.getArg1())
.toList()
It actually seems like I could remove the collection type entirely (the generic type checking is going to come from the function type).
However, we have downstream reasons for needing the collection class - but it looks like there is no way to ensure that the collection type is consistent with the function.
Let me know if I'm wrong or missing something!
The compiler must infer the type parameter E to be Object. E cannot be inferred as MyObj because then List.class cannot be passed to a parameter expecting a Class<? extends List<MyObj>>.
The simplest workaround is to just cast List.class to the correct type before passing it
(Class<List<MyObj>>)(Object)List.class
Of course, this means you would be writing List and MyObj two times each.
If you need both Class objects for later and don't like repeating yourself, one way I can think of is taking a "type token" object, such as com.google.common.reflect.TypeToken.
// The 'E' type parameter isn't actually necessary anymore
public static <C extends Collection<E>, E, R> MyFunctionHolder<C, R> forCollectionOfType(
TypeToken<C> collectionType,
Function<C, R> function
){
// you can get the two Class objects this way
Class<? super C> collectionClass = collectionType.getRawType();
Class<?> elementClass = collectionType.resolveType(Collection.class.getTypeParameters()[0]).getRawType();
System.out.println(collectionClass);
System.out.println(elementClass);
return new MyFunctionHolder<>(function);
}
Caller:
MyFunctionHolder<List<MyObj>, List<String>> fh = forCollectionOfType(
// the type in <...> can be inferred from the type of 'fn'!
new TypeToken<>() {},
l -> l.stream()
.map(o -> o.getArg1())
.toList()
);
A notable difference between taking a TypeToken vs Class is that you have moved some compile time checks to runtime. For example, you are not allowed to pass in a type variable in either case,
public static <T extends Collection<MyObj>> void someMethod() {
forCollectionOfType(
new TypeToken<T>() {}, // you cannot do this
l -> l.stream()
.map(o -> o.getArg1())
.toList()
);
}
While T.class will produce a compile time error, new TypeToken<T>() {} would throw at runtime.
If you don't want to use external libraries, you can write something like TypeToken yourself if you only have a limited number of cases to handle. For example,
public static abstract class CollectionTypeToken<C extends Collection<E>, E> {
private final Class<?> collectionClass;
private final Class<?> elementClass;
public CollectionTypeToken() {
var superType = (ParameterizedType)getClass().getGenericSuperclass();
collectionClass = switch(superType.getActualTypeArguments()[0]) {
case Class<?> x -> x;
case ParameterizedType x -> (Class<?>)x.getRawType();
default -> throw new UnsupportedOperationException();
};
elementClass = switch(superType.getActualTypeArguments()[1]) {
case Class<?> x -> x;
case ParameterizedType x -> (Class<?>)x.getRawType();
default -> throw new UnsupportedOperationException();
};
}
public Class<?> getCollectionClass() {
return collectionClass;
}
public Class<?> getElementClass() {
return elementClass;
}
}
This implementation only handles cases where C and E are parameterised types or simple classes. It does not handle the case where they are wildcards or generic arrays. A more proper implementation would involve traversing the Type tree. See the implementation of Guava's TypeToken for other cases.