Return value directly from CompletableFuture.thenAccept

13,922

Solution 1

There is a fundamental contradiction in your goal. You can have only either, get() returning a complete, directly usable list or “return the result in an async way”.

If the method List<Provider> get() is supposed to return a List which can be used by the caller without restrictions, it can’t stay an asynchronous operation, as the operation must have been completed when get() returns. This can be achieved as simple as calling join(), to wait for the completion and get the result:

public List<Provider> get() {
    CompletableFuture<List<Provider>> providersResponse = getSomeData();
    return providersResponse.join();
}

or just

public List<Provider> get() {
    return getSomeData().join();
}

This effectively turns the potentially asynchronous operation of getSomeData() into a synchronous operation.

This answer, using

public List<Provider> get() {
    List<Provider> providers = new ArrayList<>();
    CompletableFuture<List<Provider>> providersResponse = getSomeData();
    providersResponse.thenAccept(providers::addAll);
    return providers;
}

does not wait for the completion of the operation, but returns a new ArrayList after scheduling an addAll operation, whose execution is entirely out of control of this method.

This is an asynchronous operation, so when get() returns, the List might still be empty, but get updated at a later time by an arbitrary thread. Since ArrayList is not thread safe, the perceived state doesn’t have to be either, empty or completed, it may be an arbitrary in-between state, not even required to be consistent. Be prepared for strange exceptions, impossible-looking situations or other surprises when using that code.

When you fix that code by using a thread safe List implementation, you still have the problem that the returned List might be empty when queried and get filled at an arbitrary point of time outside of the caller’s control. That’s not fixable, as said at the beginning, it’s the fundamental problem of wanting an asynchronous operation but returning a List.

The CompletableFuture is already an encapsulation of a potentially asynchronous operation, so if you want the operation to stay asynchronous, just return the CompletableFuture<List<Provider>> to the caller, to allow to gain control. Otherwise, if you want the caller to receive a List which can be used without restrictions, wait for the completion before returning the result, e.g. via join().

Solution 2

Try this:

public List<Provider> get() {
    List<Provider> providers = Collections.synchronizedList(new ArrayList<>());
    CompletableFuture<List<Provider>> providersResponse = getSomeData();
    providersResponse.thenAccept(providers::addAll);
    return providers;
}
Share:
13,922
span
Author by

span

SOreadytohelp

Updated on June 04, 2022

Comments

  • span
    span almost 2 years

    I am trying to return a list from my CompletableFuture like this:

    public List<Provider> get() {
        CompletableFuture<List<Provider>> providersResponse = getSomeData();
        return providersResponse.thenAccept((List<Provider> providers) -> {
            return providers;
        });
    }
    

    It fails with "unexpected return type though. How can I return the result in an async way?

    • Lii
      Lii about 7 years
      Note that you can write the argument to thenAccept like this shorter and clearer: .thenAccept(providers -> providers)
    • Holger
      Holger about 7 years
      And how is the caller of get() supposed to deal with the spurious asynchronous List modification?
    • span
      span about 7 years
      @Holger Could you elaborate? I do not understand what spurious asynchronous List modification is.
  • Holger
    Holger about 7 years
    This is broken as the caller can get an empty ArrayList on which another thread might call addAll at an arbitrary time. ArrayList is not thread safe.
  • span
    span about 7 years
    CompletableFuture.get() is blocking so it would lock the thread no?
  • Darshan Mehta
    Darshan Mehta about 7 years
    Updated the answer.
  • Didier L
    Didier L about 7 years
    Even with the synchronized list, it remains very inconvenient for the caller as he will have no means to know when the list will be filled in – in particular when getSomeData() fails or returns an empty list.
  • msayag
    msayag about 7 years
    You must decide if you want your get() to block or not. It can be async and return a CompletableFuture<List<Provider>> or be blocking and return List<Provider>. it can not be async and return a plain List...
  • AJW
    AJW about 3 years
    I have a similar use case but am new to CompletableFuture. I am trying to replace AsyncTask [also ExecutorService.submit() with .get()] to load a List of CardViews from Room database to the UI. Your comment about returning an empty List has me nervous about even using CompletableFuture. Would appreciate any thoughts or ideas here: stackoverflow.com/questions/67294345/…