BeeOnRope
BeeOnRope

Reputation: 64905

Getting collection of most general type from a GenericType<T>.class filter

I apologize in advance for the terrible title, suggestions for improvement are eagerly accepted.

Let's say I have a method which filters a List<T> of arbitrary type based on a class, returning a new List whose elements are those of the input list that are instances of the given class. Here's a straightforward implementation (yes, you can also do it in a 1-liner with streams):

public static <T> List<T> filterByClass(List<?> list, Class<T> clazz) {
    List<T> filtered = new ArrayList<>();
    for (Object o : list) {
        if (clazz.isInstance(o)) {
            filtered.add(clazz.cast(o));
        }
    }
    return filtered;
}

This works great if you pass it a list of a non-generic type like String as in (prints foo bar):

List<Object> l = Arrays.<Object>asList(1, "foo ", 2, "bar ");
filterByClass(l, String.class).stream().forEach(System.out::println);

Now I want to pass to filter on a class of a generic type, say Optional<T>:

List<Object> l = Arrays.<Object>asList(1, Optional.of("foo"), 2, Optional.of("bar"));
filterByClass(l, Optional.class).stream().forEach(System.out::println);

That works fine and prints:

Optional[foo]
Optional[bar]

The problem is there is a raw type hidden there. The return type of the filterByClass call above is List<Optional>, not List<Optional<...>>. Many uses will trigger warnings about raw types, etc.

Now I understand type erasure, and I know that a class object will never carry generic type information - there is no such Optional<String>.class or Optional<Integer>.class - there is only Optional.class.

However, there is still a better return value than the raw type: I would like instead the fully-generic wildcard version: List<Optional<?>>. This should be totally type safe, since any Optional is an Optional<?>, right?

Assignment doesn't work, since List<Optional> is not convertible to List<Optional<?>>

List<Optional<?>> r = filterByClass(l, Optional.class);  

A cast doesn't work either because the types aren't even castable (in the same way that Foo<T> can never be directly cast to Foo<U> no matter the relationship between different types T and U).

The only solution seems to be to cast all the way down to a raw List and then back up to the list with wildcard-paramaterized type-parameter:

List<Optional<?>> r = (List)filterByClass(l, Optional.class);  

Now obviously such casts aren't safe in the general case, and this would be totally unsafe if the type parameter in the assignment Optional<?> didn't match the class object passed to filterByClass - although I think they are safe in the specific case that the class matches the type parameter with unbounded wildcards.

Is there some way to do this without the potentially unsafe casts, either by changing the filterByClass method or some safe casting of the result?

Another possible answer would be that this is not safe (i.e., the result of filterByClass(..., Optional.class) cannot safely be converted to List<Optional<?>>, so the question is ill-formed.

Upvotes: 3

Views: 530

Answers (1)

Jorn Vernee
Jorn Vernee

Reputation: 33855

You could change the signature of filterByClass to:

public static <T> List<T> filterByClass(List<?> list, Class<? extends T> clazz) {...}

Then you can have T be inferred from the assignee:

List<Object> l = Arrays.<Object>asList(1, Optional.of("foo"), 2, Optional.of("bar"));
List<Optional<?>> result = filterByClass(l, Optional.class);

This works since a raw Optional is assignable to Optional<?> (which is safe), so Class<Optional> satisfies Class<? extends Optional<?>>, and you can add the Optional returned by cast to the list of Optional<?>.

Upvotes: 4

Related Questions