john
john

Reputation: 1133

why java8 streams consumer andThen method?

I am new to Java 8, just want to ask what is the difference in performance between the two code snippets below.

I know both work, but I am wondering why the Java team created Consumer::andThen method when I can use the normal approach.

List<String> list = Arrays.asList("aaa","cccc","bbbb");
List<String> list2 = new ArrayList<>();

//Approach 1

list.stream().forEach(x -> {
            list2.add(x);
            System.out.println(x);
        });

//Approach 2

Consumer<String> c1 = s -> list2.add(s);
Consumer<String> c2 = s -> System.out.println(s);

list.stream().forEach(c1.andThen(c2));

IMO, approach1 is better, why approach2 again? If both are same, then why andThen() method is created? I guess there must be reason they've created.

In other words, when exactly is the andThen() method really helpful?

Upvotes: 5

Views: 5436

Answers (4)

WesternGun
WesternGun

Reputation: 12738

There are two questions in the original post:

  • is it better regarding performance?
  • why introducing Consumer and other functional interface?

Performance-wise, I don't think it would be better; considering the time classic foreach loop have existed, optimisation of memory usage and other aspects may long have been there; and the overhead of creating other references in Consumer, so no, it's not faster/lighter.

So why? It's not about if it's faster, but about a paradigm shift, marking one new phase of Java when you can start thinking from the functional way. Before, functional perspective/way of programming or thinking in Java is not natively supported, but now it is. A "pipeline stream" way of processing data now is syntactically possible and readable. So it's an effort to achieve syntax and semantics consistency, so you can write in functional way, and others can understand because it can be read in a fluent way, with proper keyword assembling corresponding meaning. To know that how you read affects how you think, and if you cannot write/read it in a functional way, you cannot think so in Java. So it's a step towards making Java a multi-paradigm language.

(Apache Camel does pipeline processing in its concept even before that, even without this; but that's a rare example in a language like Java where functional was not there from the beginning).

Imagine now you can write code:

// functional and easy to read
readInput().forEach(convertToUpperCase().andThen().store());

vs.

// faster but not functional
readInput().forEach(word -> {
    String upperCase = convertToUpperCase();
    store(upperCase);
}

That's just one simple example so the difference is small; but I imagine there could be code much easier to read in functional way and you could understand the whole process just by reading it, vs. traditional Java non-functional way where it's obfuscate to understand what is happening without fully diving into code across several places.

Not knowing much about functional programming but the style is simple and beautiful even with my current shallow experience: one element being transformed/joined with others/grouped, among other processing, then result.

Upvotes: 0

Stadmina
Stadmina

Reputation: 129

The most obvious use case seems to the ability to combine your consumers as you wish while making each one of them re-usable. It does just make the code cleaner and more "functional ready". It will not be much useful if you don't need to re-use the same consumer logic elsewhere.

Upvotes: -1

Mafor
Mafor

Reputation: 10681

Just as a side note, there is another approach possible:

Approach 3

list.stream()
        .peek(System.out::println)
        .forEach(list2::add);

or even better:

List<String> list2 = list.stream()
        .peek(System.out::println)
        .collect(Collectors.toList());

Performance-wise all the approaches are comparable, but the last one is the most "functional". The problem with the previous ones is the insertion into the second list inside the stream. This is a side-effect, which is considered a bad practice. From this perspective, the approach with the two consumers is the worst in my opinion, since the side effect is the most hidden.

Upvotes: 3

Eran
Eran

Reputation: 393841

I agree that is makes less sense to create a separate Consumer<String> instance for each statement just for the sake of using andThen.

However, if you already have two Consumer<String> instances (c1 and c2), and you need to execute both, you could either write:

list.forEach(x -> {
        c1.accept(x);
        c2.accept(x);
    });

or

list.forEach(c1.andThen(c2));

In this case, the latter is clearly cleaner.

EDIT:

I'd like to expand on the case when you create your own Consumers (i.e. not re-using pre-existing ones). When you assign a lambda expression to a Consumer variable, you can use that Consumer instance in multiple places instead of duplicating the lambda expression. That is true for any functional interface.

Once you decide to use a Consumer variable, the decision of whether to have a single Consumer implementing complex logic or 2 simpler Consumers, each implementing part of the logic, depends on whether there is a need to be able to execute just part of the logic in some cases (or changing the order in which parts of the logic are executed).

Having c1 and c2 allows you either of the following options:

list.forEach(c1);
list.forEach(c2);
list.forEach(c1.andThen(c2));
list.forEach(c2.andThen(c1));

If you had a single consumer that does the logic of both c1 and c2, you have less flexibility.

Upvotes: 13

Related Questions