4

I am playing around with streams and lambdas in Java 8, as I have never used them before and was trying to convert the ages of everyone who is 20 to 19 and the printing out their names, but I get the following error

Cannot invoke map(Person::getName) on the primitive type void

Here is my code

            System.out.println(
                people.stream()
                .filter(person -> person.getAge() == 20)
                .forEach(person -> person.setAge(19))
                .map(Person::getName)); 

If someone could tell me why this is happening or let me know how to improve or amend this code, it would be greatly appreciated.

user6248190
  • 1,233
  • 2
  • 16
  • 31

2 Answers2

9

forEach is a terminal operation and will not return the stream it works on.

In general, you should avoid using forEach to modify the stream, even as a final operation. Instead, you should use map to modify the items of your stream.

Here is an example of how to do it, which includes a legitimate use of forEach :

people.stream()
      .filter(person -> person.getAge() == 20)
      .map(person -> new Person(person.getName(), person.getAge() -1 /*, ...*/))
      .forEach(System.out::println);

A few notes on that code :

  • map(...) only transforms the stream, not the datasource (the people Collection). If you want to use the transformed result later, you will want to .collect() the Stream into a new Collection with an appropriate Collector (e.g. Collectors.toList())

  • if you follow that road, your new terminal operation is collect(), and you can't use .forEach() anymore. One solution would be to use the .forEach() method of the new Collection (no need for a stream, forEach() is implemented on Iterable since 1.8), another would be to use .peek() on the stream where you map() as a non-terminal equivalent to .forEach()

  • About the use of peek(), note that the non-terminal operations are driven by the terminal operation : if this one only returns a single element of the stream (like .findFirst()), non-terminal operations will only be executed for this element, and you shouldn't expect otherwise.

Aaron
  • 24,009
  • 2
  • 33
  • 57
  • Can you also add something about how this code can be made to work or why it isn't a good idea to do this at all? – biziclop Oct 27 '16 at 09:09
  • You're right for forEach. map isn't the solution though, because person.setAge(19) probably returns void. – Eric Duminil Oct 27 '16 at 09:16
  • Please don't recreate every Person just to setAge. – Eric Duminil Oct 27 '16 at 09:16
  • 1
    I'm divided here. On one hand I agree recreating a new object just to change one of its properties is wasteful. On the other hand, I'm pretty sure `peek` isn't intended to modify a stream. – Aaron Oct 27 '16 at 09:19
  • @Aaron thanks for you answer it worked, but instead of creating a new person in map i just did person -> person.getName and it worked perfectly – user6248190 Oct 27 '16 at 09:21
  • It doesn't necessarily modify a stream. It's just here to perform an action (could be println(), could be setAge()) while returning the original stream. – Eric Duminil Oct 27 '16 at 09:22
  • @user6248190 : but then, how do you setAge(19) ? – Eric Duminil Oct 27 '16 at 09:23
  • 2
    @user6248190 that's not the same thing : instead of mapping your original `Person` stream to a stream of `Person` aged one less year, you mapped your stream to a stream of `String` which are the names of the `Person` with still the same age – Aaron Oct 27 '16 at 09:23
  • @EricDuminil when it doesn't modify the stream I'm all for `peek` as an intermediate operation equivalent to `forEach`. However, I believe neither `forEach` nor `peek` should modify the stream as a good practice. I'm not sure where this belief comes from, so maybe it's not justified. – Aaron Oct 27 '16 at 09:25
  • @Aaron oh right I get you, using your solution actually changes the ages of the people where as my one just prints out people who are 20 – user6248190 Oct 27 '16 at 09:26
  • 2
    I believe peek should not be interfering : that is, modifying the elements is fine, but modifying the stream itself isn't. – Eric Duminil Oct 27 '16 at 09:28
  • 1
    @EricDuminil You're right that peek and forEach should be non-interfering, but I'm not sure about your interpretation of what it means. The [javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#NonInterference) says "For most data sources, preventing interference means ensuring that the data source is not modified at all during the execution of the stream pipeline." – Aaron Oct 27 '16 at 09:32
  • 1
    "data source" : for example a file, but not the lines inside it. – Eric Duminil Oct 27 '16 at 09:34
  • 2
    That makes sense, thanks for your feedback! I'm still not totally convinced, but I'll make sure to further research this point. – Aaron Oct 27 '16 at 09:36
  • 2
    Okay. I think I found what the biggest problem with peek is : it is only called if some terminal operation comes after it. Without forEach after, it won't get called. I updated my answer with a forEach example, without having to create a new Person. – Eric Duminil Oct 27 '16 at 09:50
  • 3
    As long as you can prove that the same `Person` instance will never be twice in the stream, calling `setAge` in one action has no interference (otherwise, you’re modifying the same property that the `filter` accesses). Still, you should consider [“… is peek really only for debugging?”](http://stackoverflow.com/q/33635717/2711488) – Holger Oct 27 '16 at 09:51
  • 1
    Thanks @Holger. The accepted answer merely supports my opinion, but the most upvoted one really is enlightening! – Aaron Oct 27 '16 at 09:58
  • 1
    Excellent point Holger. I stand corrected. Peek might be sometimes more elegant, but what good is it if it doesn't always work? I'll update my answer with only forEach – Eric Duminil Oct 27 '16 at 10:03
  • map has the same problem as peek here : its execution depends on what comes after. Without forEach after, it won't get called. And I still think it's wrong to create a new Person. It might be fine in this example, but this pattern could lead to big problems/bugs with other classes. – Eric Duminil Oct 27 '16 at 11:39
  • @EricDuminil I agree, I have to revise my answer. I don't have the time right now but I should be able un a few hours, I'll notify you once it's done – Aaron Oct 27 '16 at 11:59
  • @Aaron: a few months later ... :) – Eric Duminil Feb 05 '17 at 16:22
  • @EricDuminil hey, thanks for reminding me ! I've had some difficulty finding how to integrate the result of our interesting conversation in the answer without going off-topic, please tell me if you think anything can be added ! – Aaron Feb 06 '17 at 16:31
3

You can write multiple statements inside of forEach :

public class Person
{
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return name + "(" + age + ")";
    }
}

public class LambdaPeople
{
    public static void main(String[] args) {
        Person andy = new Person("Andy", 19);
        Person bob = new Person("Bob", 21);
        Person caroline = new Person("Caroline", 20);

        List<Person> people = new ArrayList<>();
        people.add(caroline);
        people.add(andy);
        people.add(bob);

        people.stream()
                .filter(person -> person.getAge() == 20)
                .forEach(person -> {
                    person.setAge(19);
                    System.out.println(person);
                });
    }
}

It returns :

Caroline(19)

The two next methods are for documentation purpose. peek and map are intermediate operations. Without forEach at the end, they wouldn't be executed at all.

If you want to use map :

    people.stream()
            .filter(person -> person.getAge() == 20)
            .map(person -> {
                person.setAge(19);
                return person;
            })
            .forEach(System.out::println);

If you want to use peek :

    people.stream()
            .filter(person -> person.getAge() == 20)
            .peek(person -> person.setAge(19))
            .forEach(System.out::println);
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124