Suppose there's a long IO operation that may result in some return value — but doesn't have to. Completion with no return value is different from the operation being pending.
Do I have better options other than returning CompletableFuture<Optional<MyType>>
from my asynchronous method? It's a bit of a mouthful, especially if MyType
is generic itself. If I need to return a list of some entities, it can get as ugly as CompletableFuture<Optional<List<MyType<MyOtherType>>>>
(remember, an empty list is different from no list, I can't skip the Optional
layer).
In my case it's a bit different, though, Optional<CompletableFuture<List<MyType<MyOtherType>>>>
. A cache class may have a list of entities that you may fetch by a key, but you may also need to wait for the loading to finish (or consume the result asynchronously). Thus, cacheable, cached, not cacheable are three distinct states.
Java 8.
4 Answers 4
Whenever combinations of nested generic types become ugly, I would recommend to apply the same measures we apply whenever combinations of nested loops or control structures become ugly: refactor the innermost pieces out to something which groups them in a sensible way.
In case of control structures, we refactor to new functions. In case of nested generic types, we refactor to new types (which in Java means new classes).
Now I cannot tell you whether it is better to create a new type / class for
MyType<MyOtherType>
or
List<MyType<MyOtherType>>
or
Optional<List<MyType<MyOtherType>>>
since meaningless names like MyOtherType
or MyType
are not well suited to give you a more meaningful answer. You need to analyse what semantics these type combinations have in your context, then you may probably be able to build an abstraction by naming them accordingly.
remember, an empty list is different from no list, I can't skip the Optional layer
Oh sure you can.
Optional is just a collection that contains 0 to 1 elements. It's hardly the only way to model something not existing yet.
What would be nice is to be able to write code against this result without needing to know the result. You know, polymorphism.
Well there are ugly ways to do this when returning the result. But you can also try not using return.
Instead of asking the code to return, pass a port to the code you want to complete. That port will allow that code to call one method on success, and another on error or timeout. And now you don't have to sit around waiting. When that's done it'll call what it needs to call. You go find something else to do.
-
1Thank you. But I want to use
CompletableFuture
exactly because I want to pass success/error handlers and then "find something else to do"Sergey Zolotarev– Sergey Zolotarev02/26/2025 03:27:42Commented Feb 26 at 3:27
I think someone might have something SE relevant things to say.
In my experience there are some rare cases that fit. But it mostly - as far as I remember - made sense to wrap classes around the raw data. With methods like hasResults
- your Option/List semantics. Even a simple toString
already convinced the developer forcing an abstract layer.
Where Lists are concerned, you could also have a class with header data & detailed list that could be passed.
So in short: maybe it is a code smell. When things cannot be stated succinctly.
I guess you have to ask yourself if its more or less ugly than
var r = longIoOperation()
if(r == null) ...
Or
io.OnComplete = getResultFunction
io.longIoOperation()
Or
public class MyListType : List
{
@Override
public boolean add(E e) { ...
}
Or a combination thereof
var
can spare you many of these declarations, but proper type aliases are not in java yet.