1

Folks I have been using CompletableFuture in my project and I experienced a strange behaviour. I would like to understand the behaviour. Kindly help

Scenario 1: In the below code the output is I am from supply I am the first thenApply I am the second thenApply .. As expected

public void callAll(){
        String calculatedDistance = listBuildComp().stream()
                .map(CompletableFuture::join)
                .collect(Collectors.joining());
        System.out.println(calculatedDistance);
    }
    private List<CompletableFuture<String>> listBuildComp(){
        List<CompletableFuture<String>> result = new ArrayList<>();
        result.add(buildComp());
        return result;
    }

    private CompletableFuture<String> buildComp(){
        CompletableFuture<String> workFlowWithServices =
                CompletableFuture.supplyAsync( () -> "I am from supply ")
        .thenApply( x -> {
            return x.concat(" I am the first thenApply ");
        })
        .thenApply( x -> {
            return x.concat(" I am the second thenApply ");
        });
        return workFlowWithServices;
    }

Scenario 2: When the below method is changed then the output is I am from supply . Upon further investigation I see that the rest two thenApply runs in their own thread

private CompletableFuture<String> buildComp(){
        CompletableFuture<String> workFlowWithServices =
                CompletableFuture.supplyAsync( () -> "I am from supply ");
        
        workFlowWithServices.thenApply( x -> {
            return x.concat(" I am the first thenApply ");
        });
        
        workFlowWithServices.thenApply( x -> {
            return x.concat(" I am the second thenApply ");
        });
        return workFlowWithServices;
    }

The reason I am interested with Scenario 2 is imagine you are chaining 2 TASKS then Scenario 1 is okay but imagine you want to chain 50 TASKS then the method will get too big. In this case I wanted to extract each calls in to a method to begin with but eventually extract in to a class if required but I cannot do all these cause of Scenario 2.

Want to know the concept or idea about why scenario 2 behaves in a different way and if there is anyway to make it behave like scenario 1. Kindly share your knowledge. Thank you.

Eugene
  • 117,005
  • 15
  • 201
  • 306
Hari Rao
  • 2,990
  • 3
  • 21
  • 34
  • 1
    What is so surprising to the fact that ignoring return values leads to loss of the return values? When you write `string1.concat(" I am the first thenApply "); string1.concat(" I am the second thenApply ");` it’s exactly the the same, the `string1` variable still references the original string. – Holger Feb 24 '21 at 16:51
  • yes @Holger it does refer to the original one but this works in Scenario 1 as they all run one after the other while in scenario 2 all the three supplyAsync, thenApply and thenApply run in parallel. – Hari Rao Feb 24 '21 at 17:02
  • 1
    Scenario 1 is like `String s = "a".concat("b").concat("c");` it is *using* the result of the method invocations. `CompletableFuture f = CompletableFuture.supplyAsync(…) .thenApply(…).thenApply(…);` is not different. Scenario 2 is like `String s = "a"; s.concat("b") s.concat("c");` storing the first object and ignoring the other two, though the other two objects also are different, as it does matter on which object you invoke a method. – Holger Feb 25 '21 at 06:53

1 Answers1

2

First of all, you have no guarantee which thread will execute those thenApply, it could easily be main.

Then in your example, you build a CompletableFuture:

CompletableFuture<String> workFlowWithServices =
       CompletableFuture.supplyAsync( () -> "I am from supply ");

chain some actions :

    workFlowWithServices.thenApply( x -> {
        System.out.println("executing");
        return x.concat(" I am the first thenApply ");
    })

   ...

but you ignore the result of that thenApply (which is a CompletableFuture<String> too). When you join, you join on workFlowWithServices which, when it's done, will return "I am from supply ". Done. You do not query (you ignore entirely) the result of subsequent actions in thenApply, thus they do execute, but the result is gone.

I do not get what exactly stops you to build something like this, for example:

 private static CompletableFuture<String> buildComp2(){
    CompletableFuture<String> one =
            CompletableFuture.supplyAsync( () -> "I am from supply ");

    CompletableFuture<String> two = one.thenApply( x -> {
        System.out.println("executing");
        return x.concat(" I am the first thenApply ");
    });

    CompletableFuture<String> three = two.thenApply( x -> {
        return x.concat(" I am the second thenApply ");
    });
    return three;
}
 
Eugene
  • 117,005
  • 15
  • 201
  • 306