javaspringspring-bootoopjava-stream

Understanding the Need for .map When Working with Optional and Interface Types in Spring Boot


I'm studying clean architecture building a simple Spring Boot application.

Some of my classes are:

public class EClient implements Serializable, EClientInterface {...}
public interface ClientRepository extends JpaRepository<EClient, String> {...}

And finnaly, the reason of my question:

@Override
public Optional<EClientInterface> findClient(String cpf) {
    return repository.findById(cpf).map(eClient -> eClient);
}

Basically, I was forced to put a .map(eClient -> eClient), because my error message was:

To sum up, my questions are:

  1. Why do I need the .map? Is it because Optinal only accpets the exact type and not implementations or subclasses?

  2. Why does this map actually works? It looks redundant to me


Solution

  • Generics are invariant.

    Outside of the <>, Java is covariant: A subtype is just as good:

    Integer i = 5;
    Number n = i; // this is fine!
    

    But inside the generics, Java is invariant - only the exact type is acceptable:

    List<Integer> is = new ArrayList<Integer>();
    List<Number> ns = is; // compiler error
    

    That's because the type system would be broken if it wasn't like that. After all, if the above is legal, I can asplode the universe:

    Double d = 5.5;
    Number n = d; // obviously, legal.
    ns.add(n); // this has to be legal too.
    Integer i = is.get(0); // oh.. oh dear. kaboom
    

    There's only one list here, and we added a double to it. Which means there is now a double in a list object that is referred to by a variable of type List<Integer>.

    You need to opt into covariance, and return an Optional<? extends EClientInterface>. You read the question mark as 'do not know'. As in, you read that as: "An Optional that holds.. I do not know. All I know is, whatever the choice was that I don't know, it's either EClientInterface or some subtype of it".

    The Java compiler will ensure that, whatever you do to such a thing, works for all possible choices one could make.

    As an example, you can do this:

    List<? extends Number> ns = someListOfIntegers;
    

    But, as the compiler knows what it means and knows it must ensure that anything you do to ns is fine regardless of what ? extends Number> actually ends up being (it could be Integer, Double, Number, or some type some library you haven't even written yet is going to create; Number is not final nor is it sealed, after all). Hence, ns.add() cannot be called because you need to pass an expression of type T, and T here is 'do not know', so, no can do. Except the null literal because that's all types at once, but list.add(null) is rather pointless, of course.

    If you for some bizarre reason MUST have Optional<EClientInterface> there, you're going to run into this problem a lot more, you can't always fix it so easily, and at least now you know why that map is 'necessary'. To work around the fact that generics just doesn't work the way you appear to think it does, and it doesn't work that way because the type system would be broken if it did.