在 Java8中迭代时修改流中的对象

在 Java8流中,我是否可以在其中修改/更新对象? 例如:

users.stream().forEach(u -> u.setProperty("value"))
185571 次浏览

Yes, you can modify state of objects inside your stream, but most often you should avoid modifying state of source of stream. From non-interference section of stream package documentation we can read that:

For most data sources, preventing interference means ensuring that the data source is not modified at all during the execution of the stream pipeline. The notable exception to this are streams whose sources are concurrent collections, which are specifically designed to handle concurrent modification. Concurrent stream sources are those whose Spliterator reports the CONCURRENT characteristic.

So this is OK

  List<User> users = getUsers();
users.stream().forEach(u -> u.setProperty(value));
//                       ^    ^^^^^^^^^^^^^
//                        \__/

but this in most cases is not

  users.stream().forEach(u -> users.remove(u));
//^^^^^                       ^^^^^^^^^^^^
//     \_____________________/

and may throw ConcurrentModificationException or even other unexpected exceptions like NPE:

List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());


list.stream()
.filter(i -> i > 5)
.forEach(i -> list.remove(i));  //throws NullPointerException

To do structural modification on the source of the stream, as Pshemo mentioned in his answer, one solution is to create a new instance of a Collection like ArrayList with the items inside your primary list; iterate over the new list, and do the operations on the primary list.

new ArrayList<>(users).stream().forEach(u -> users.remove(u));

To get rid from ConcurrentModificationException Use CopyOnWriteArrayList

The functional way would imho be:

import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;


public class PredicateTestRun {


public static void main(String[] args) {


List<String> lines = Arrays.asList("a", "b", "c");
System.out.println(lines); // [a, b, c]
Predicate<? super String> predicate = value -> "b".equals(value);
lines = lines.stream().filter(predicate.negate()).collect(toList());


System.out.println(lines); // [a, c]
}
}

In this solution the original list is not modified, but should contain your expected result in a new list that is accessible under the same variable as the old one

Instead of creating strange things, you can just filter() and then map() your result.

This is much more readable and sure. Streams will make it in only one loop.

As it was mentioned before - you can't modify original list, but you can stream, modify and collect items into new list. Here is simple example how to modify string element.

public class StreamTest {


@Test
public void replaceInsideStream()  {
List<String> list = Arrays.asList("test1", "test2_attr", "test3");
List<String> output = list.stream().map(value -> value.replace("_attr", "")).collect(Collectors.toList());
System.out.println("Output: " + output); // Output: [test1, test2, test3]
}
}

You can make use of the removeIf to remove data from a list conditionally.

Eg:- If you want to remove all even numbers from a list, you can do it as follows.

    final List<Integer> list = IntStream.range(1,100).boxed().collect(Collectors.toList());


list.removeIf(number -> number % 2 == 0);

This might be a little late. But here is one of the usage. This to get the count of the number of files.

Create a pointer to memory (a new obj in this case) and have the property of the object modified. Java 8 stream doesn't allow to modify the pointer itself and hence if you declare just count as a variable and try to increment within the stream it will never work and throw a compiler exception in the first place

Path path = Paths.get("/Users/XXXX/static/test.txt");






Count c = new Count();
c.setCount(0);
Files.lines(path).forEach(item -> {
c.setCount(c.getCount()+1);
System.out.println(item);});
System.out.println("line count,"+c);


public static class Count{
private int count;


public int getCount() {
return count;
}


public void setCount(int count) {
this.count = count;
}


@Override
public String toString() {
return "Count [count=" + count + "]";
}






}

Yes, you can modify or update the values of objects in the list in your case likewise:

users.stream().forEach(u -> u.setProperty("some_value"))

However, the above statement will make updates on the source objects. Which may not be acceptable in most cases.

Luckily, we do have another way like:

List<Users> updatedUsers = users.stream().map(u -> u.setProperty("some_value")).collect(Collectors.toList());

Which returns an updated list back, without hampering the old one.

.peek() is the answer.

users.stream().peek(u -> u.setProperty("value")).foreach(i->{
...
...
});

for new list

users.stream().peek(u -> u.setProperty("value")).collect(Collectors.toList());