Return CompletableFuture<Void> or CompletableFuture<?>?

I want to write an asynchronous method that returns a CompletableFuture. The only purpose of the future is to track when the method is complete, not its result. Would it be better to return CompletableFuture<Void> or CompletableFuture<?>? Is there a reason to prefer one or the other, or are they interchangeable?

  • CompletableFuture itself returns CompletableFuture<Void> from many of its methods.
  • java.nio has a Future<Void> in AsynchronousSocketChannel: Future<Void> connect(SocketAddress remote).
  • On the other hand, java.util.concurrent classes like ExecutorService and ScheduledExecutorService return Future<?>: for instance, with Future<?> submit(Runnable task).

Note that I'm only asking about return types, not parameter lists, variable declarations, or other contexts.


Solution 1:

It is best to use CompletableFuture<Void>.

According to this answer found by Sotirios Delimanolis, Future<?> is a minor API flaw. In Java 6 the submit() method used a Future<Object> internally, and so its return type was set to Future<?>. In Java 7 the implementation changed to use Future<Void> internally, but it was too late to change the API so the return value stayed as Future<?>.

Newer Java APIs use Future<Void> and CompletableFuture<Void>. Those are the examples we should follow.

Solution 2:

Would it be better to return CompletableFuture<Void> or CompletableFuture<?>

Is there a reason to prefer one or the other, or are they interchangeable?

There are three contexts which code may affect:

  • Runtime - generics have no implication on it.
  • Compile - I can't imagine a case where some method will accept Future<Void> but won't accept Future<?>.
  • Development - if Future's result have no meaning, then it is a good practice to say about that to the users through the declaration.

So Future<Void> is more preferable.

Solution 3:

Looking at the CompletableFuture API you will find that CompletableFuture<Void> is used with side effects kind of methods where the result can't be obtained (because it doesn't exist), ex:

CompletableFuture.runAsync(Runnable runnable);

returning a CompletableFuture<Object> here would be confusing because there no result really, we only care about the completion. Methods that take Consumers and Runnables return CompletableFuture<Void>, ex : thenAccept, thenAcceptAsync. Consumer and Runnable are used for side effects in general.

Another use case for Void is when you really don't know the result. For example: CompletableFuture.allOf, the passed list might be a CompletableFuture originated from a Runnable, so we can't get the result.

Having said all of that, CompletableFuture<Void> is only good if you don't have another option, if you can return the result then please go for it, the caller might choose to discard if they are not interested. You said that you are only interested in the completion, then yes, CompletableFuture<Void> would do the job, but your API users would hate you if they know that CompletableFuture<T> was an option and you just decided on their behalf that they will never need the result.

Solution 4:

The suitable type depends on its semantic. All listed options promise to signal completion and may asynchronously return exceptions.

  • CompletableFuture<Void>: The Void tells the user there is no result to be expected.
  • CompletableFuture<?> The ? means the type of the contains value is undefined in the sense that any value could be delivered.

The CompletableFuture class inherits several convenience methods from CompletionStage. But it does also allow the caller of your method to trigger completion of the future which seems wrong because your method is responsible to signal its completion itself. There is also a cancel(...) method which is pretty pointless in the default implementation of CompletableFuture as it does not cancel the execution.

  • Future<Void>: The Void tells the user there is no result to be expected.
  • Future<?> The ? means the type of the contains value is undefined in the sense that any value could be delivered.

Future lacks the convenience methods from CompletionStage. It does not allow to trigger completion of the future but the execution can be cancelled.

Next option is CompletionStage<Void>:

  • CompletionStage<Void>: The Void tells the user there is no result to be expected. The convenience methods to bind handlers are present but the cancel(...) method is not. The caller of your method cannot trigger the completion of a CompletionStage.
  • <CancellableFuture extends Future<Void> & CompletionStage<Void>>: Set of methods from Future<Void> and CompletionStage<Void>. It tells that there is no result, convenience methods are present as well as the option to cancel. The caller of your method cannot trigger the completion of a CompletionStage.

The absence of the cancel(...) method might fit your scenario or not. Therefore I suggest to go with CompletionStage<Void> if you don't need cancellation and use <CancellableFuture extends Future<Void> & CompletionStage<Void>> if you require the option to cancel the execution. If you chose <CancellableFuture extends Future<Void> & CompletionStage<Void>> you probably want to create an interface yourself that inherits from Future<Void> and CompletionStage<Void> to be used as return type instead of putting the long type intersection directly in your method declaration.

You should avoid returning with a declared return type CompletableFuture because of the possibility for the caller to trigger completion of the future. Doing so deliberately leads to confusing code and surprising hangs because it is not clear which code is responsible to trigger completion anymore. Use one of the mentioned more restricted types to let the type system prevent unintentional completion triggering by the caller of your method.