9

As discussed in Bounding generics with 'super' keyword the Java type system is broken/incomplete when it comes to lower bounds on method generics. Since Optional is now part of the JDK, I'm starting to use it more and the problems that Guava encountered with their Optional are starting to become a pain for me. I came up with a decent work around, but I'm not sure its safe. First, let me setup the example:

public class A {}
public class B extends A {}

I would like to be able to declare a method like:

public class Cache {
   private final Map<String, B> cache;
   public <T super B> Optional<T> find(String s) {
       return Optional<T>.ofNullable(cache.get(s));
   }
}

So that both of the following work:

A a = cache.find("A").orElse(new A())
B b = cache.find("B").orElse(new B())

As a workaround, I have a static utility method as follows:

public static <S, T extends S> Optional<S> convertOptional(Optional<T> optional) {
    return (Optional<S>)optional;
} 

So my final question, is this as type-safe as the 'ideal' code above?

A a = OptionalUtil.<A,B>convertOptional(cache.find("A")).orElse(new A());
Community
  • 1
  • 1
MattWallace
  • 1,015
  • 6
  • 12

3 Answers3

5

You're effectively trying to view the Optional<B> returned as an Optional<A> without changing the return type (since you can't use the super). I would just map the identity function.

A a = cache.find("A").map(Function.<A> identity()).orElse(new A());
// or shorter
A a = cache.find("A").<A> map(x -> x).orElse(new A());

I don't see anything wrong with your approach though.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
2

Yes, your code is type safe because you are casting Optional<T> to Optional<S>, and T is always an S. You can indicate this to the compiler by using the @SuppressWarnings("unchecked") annotation on your convertOptional utility method.

As well as the other excellent answer, you could do it this way. Note the lack of generics compared to your first version.

package com.company;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

public class Cache {
    private static final Function<A,A> CAST_TO_A = Function.<A>identity();
    private final Map<String, B> cache = new HashMap<>();

    public Optional<B> find(String s) {
        return Optional.ofNullable(cache.get(s));
    }

    public static void main(String[] args) {
        Cache cache = new Cache();

        Optional<B> potentialA = cache.find("A");

        A a = potentialA.isPresent() ? potentialA.get() : new A();

        A anotherWay = cache.find("A").map(CAST_TO_A).orElse(new A());

        B b = cache.find("B").orElse(new B());
    }

    public static class A {}
    public static class B extends A {}
}
user1886323
  • 1,179
  • 7
  • 15
1

In practice, I think it is type-safe, because the Optional class is immutable and T is subtype of S.

Beware though that T is subtype of S does NOT mean that Optional<T> is subtype of Optional<S>, so theoretically that cast is not correct. And in some cases, it is not safe to do such kind of casts and it can be problematic at run-time (as it happens with List).

So my suggestion is to avoid casts whenever we can, and I would rather define a method like the getOrElse. Also, for the sake of completeness, the generic types in your method convertOptional could be simplified as below.

class OptionalUtil {
    public static <S> S getOrElse(Optional<? extends S> optional, Supplier<? extends S> other) {
        return optional.map(Function.<S>identity()).orElseGet(other);
    }

    public static <S> Optional<S> convertOptional(Optional<? extends S> optional) {
        return (Optional<S>)optional;
    }
}

And they could be use like that:

A a1 = OptionalUtil.getOrElse(cache.find("A"), A::new);
A a2 = OptionalUtil.<A>convertOptional(cache.find("A")).orElseGet(A::new);

[EDIT] I replaced the method orElse by orElseGet, because with the former a new A object will be created even if the Optional is present.

Helder Pereira
  • 5,522
  • 2
  • 35
  • 52