Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mutate elements in a Stream

Is there a 'best practice' for mutating elements within a Stream? I'm specifically referring to elements within the stream pipeline, not outside of it.

For example, consider the case where I want to get a list of Users, set a default value for a null property and print it to the console.

Assuming the User class:

class User {
    String name;

    static User next(int i) {
        User u = new User();
        if (i % 3 != 0) {
            u.name = "user " + i;
        }
        return u;
    }
}

In java 7 it'd be something along the lines of:

for (int i = 0; i < 7; i++) {
    User user = User.next(i);
    if(user.name == null) {
        user.name = "defaultName";
    }
    System.out.println(user.name);
}

In java 8 it would seem like I'd use .map() and return a reference to the mutated object:

IntStream.range(0, 7)
    .mapToObj(User::next)
    .map(user -> {
        if (user.name == null) {
            user.name = "defaultName";
        }
        return user;
    })
    //other non-terminal operations
    //before a terminal such as .forEach or .collect
    .forEach(it -> System.out.println(it.name));

Is there a better way to achieve this? Perhaps using .filter() to handle the null mutation and then concat the unfiltered stream and the filtered stream? Some clever use of Optional? The goal being the ability to use other non-terminal operations before the terminal .forEach().

In the 'spirit' of streams I'm trying to do this without intermediary collections and simple 'pure' operations that don't depend on side effects outside the pipeline.

Edit: The official Stream java doc states 'A small number of stream operations, such as forEach() and peek(), can operate only via side-effects; these should be used with care.' Given that this would be a non-interfering operation, what specifically makes it dangerous? The examples I've seen reach outside the pipeline, which is clearly sketchy.

like image 850
Adam Bickford Avatar asked Nov 17 '15 18:11

Adam Bickford


3 Answers

Don't mutate the object, map to the name directly:

IntStream.range(0, 7)
    .mapToObj(User::next)
    .map(user -> user.name)
    .map(name -> name == null ? "defaultName" : name)
    .forEach(System.out::println);
like image 117
assylias Avatar answered Sep 29 '22 20:09

assylias


It sounds like you're looking for peek:

.peek(user -> {
    if (user.name == null) {
        user.name = "defaultName";
    }
})

...though it's not clear that your operation actually requires modifying the stream elements instead of just passing through the field you want:

.map(user -> (user.name == null) ? "defaultName" : user.name)
like image 34
Louis Wasserman Avatar answered Sep 29 '22 18:09

Louis Wasserman


It would seem that Streams can't handle this in one pipeline. The 'best practice' would be to create multiple streams:

List<User> users = IntStream.range(0, 7)
    .mapToObj(User::next)
    .collect(Collectors.toList());

users.stream()
    .filter(it -> it.name == null)
    .forEach(it -> it.name = "defaultValue");

users.stream()
    //other non-terminal operations
    //before terminal operation
    .forEach(it -> System.out.println(it.name));
like image 37
Adam Bickford Avatar answered Sep 29 '22 20:09

Adam Bickford