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.

3
  • 1
    What’s the purpose of a trailing .map operation? Commented Oct 27, 2016 at 9:55
  • @Holger I wanted to print out the names of the people who's age had changed Commented Oct 27, 2016 at 10:11
  • 2
    Then, .forEach(person -> { person.setAge(19); System.out.println(person.getName()); }); is straight-forward… Commented Oct 27, 2016 at 10:16

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

22 Comments

@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
I believe peek should not be interfering : that is, modifying the elements is fine, but modifying the stream itself isn't.
That makes sense, thanks for your feedback! I'm still not totally convinced, but I'll make sure to further research this point.
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.
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?”
|
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);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.