3

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.

BeeOnRope
  • 51,419
  • 13
  • 149
  • 309
  • In the case of an `Optional`, do you consider `Optional.empty()` to be an instance of `Optional` or not? – Andy Turner Dec 05 '17 at 19:50
  • You can supply a `Function` to the filter method, which returns a casted reference if it is an instance, or null. In the case of `Optional`, `List` etc, you'd have to find some way to determine if it is safe to cast, e.g. `!opt.isPresent() || opt.get() instanceof Whatever`. – Andy Turner Dec 05 '17 at 19:52
  • @AndyTurner - yes, I think `Optional.empty()` is an `Optional` for any U. About your suggestion though, I'm not interesting in "looking into" the type `T` here: I just want to get a `List>` since for a generic type `Foo` I think it is always safe to cast to `Foo>` (and hence the raw type `Foo` can be safely case to `Foo>`). I'll live with having an unbounded wildcard in my type. – BeeOnRope Dec 05 '17 at 20:16

1 Answers1

4

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<?>.

Jorn Vernee
  • 26,917
  • 3
  • 67
  • 80
  • That's the trick! Bonus question: this works if I let `T` be inferred as you suggested, but I always thought that was just a shortcut and I could explicitly specify the function type parameter if I wanted. However if I change your last line to `List> result = Class.>filterByClass(l, Optional.class);` it fails to compile (complaining that `Optional.class` doesn't match `Class extends Optional>>`). What's up with that? Is there any type parameter I can use, or is type inference doing something "magic" here? Note `Class` is the class this function appears in. – BeeOnRope Dec 05 '17 at 20:09
  • 2
    Hmm, I say that but `javac` doesn't really seem to like it: https://ideone.com/SDmtSS Incidentally the 2 lines of test code I used/posted only work in eclipse. Adding an explicit type hint of `>` also breaks the example. I'll wiki this answer so people can still see the suggestion. ( @BeeOnRope Looks like I didn't test enough, this seems to be an eclipse peculiarity) – Jorn Vernee Dec 05 '17 at 20:09
  • This `javac` vs Eclipse discrepancy was interesting enough that I [asked over here](https://stackoverflow.com/q/47662493/149138). I'm guessing `javac` is right here since some tests with the same conversion (without argument or return type inference) also fail on eclipse. In general it doesn't seem legal to convert a `P1` to a `P1 extends P2>>`, but let's hope some language lawyer types weigh in to confirm. – BeeOnRope Dec 05 '17 at 20:55