Изменение объектов в потоке в Java8 во время итерации


Ответы:


101

Да, вы можете изменять состояние объектов внутри вашего потока, но чаще всего вам следует избегать изменения состояния источника потока. Из раздела о невмешательстве в документацию потокового пакета мы можем прочитать, что:

Для большинства источников данных предотвращение помех означает гарантию того, что источник данных вообще не будет изменен во время выполнения потокового конвейера. Заметным исключением из этого правила являются потоки, источниками которых являются параллельные коллекции, специально разработанные для обработки одновременных изменений. Источники параллельного потока - это те, чьи Spliteratorотчеты имеют CONCURRENTхарактеристику.

Так что это нормально

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

но это в большинстве случаев не

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

и может вызывать ConcurrentModificationExceptionили даже другие неожиданные исключения, такие как 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

4
Какие есть решения, если вы хотите изменить пользователей?
Augustas

2
@Augustas Все зависит от того, как вы хотите изменить этот список. Но, как правило, вам следует избегать того, Iteratorчто предотвращает изменение списка (удаление / добавление новых элементов) во время итерации, и оба потока и циклы for-each используют его. Вы можете попробовать использовать простой цикл, например, в for(int i=0; ..; ..)котором нет этой проблемы (но это не остановит вас, когда другой поток изменит ваш список). Вы можете также использовать такие методы , как list.removeAll(Collection), list.removeIf(Predicate). Также у java.util.Collectionsкласса есть несколько методов, которые могут быть полезны, например addAll(CollectionOfNewElements,list).
Pshemo

@Pshemo, одним из решений этого является создание нового экземпляра коллекции, например ArrayList, с элементами внутри вашего основного списка; перебрать новый список и выполнить операцию с основным списком: new ArrayList <> (users) .stream.forEach (u -> users.remove (u));
MNZ,

2
@Blauhirn Решить что? Нам не разрешено изменять источник потока при использовании потока, но разрешено изменять состояние его элементов. Не имеет значения, используем ли мы map, foreach или другие методы потока.
Pshemo

1
Вместо изменения коллекции я бы предложил использоватьfilter()
jontro 06

5

Функциональный способ imho будет:

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]
    }
}

В этом решении исходный список не изменяется, но должен содержать ожидаемый результат в новом списке, который доступен под той же переменной, что и старый.


3

Для структурной модификации на источнике потока, как и Pshemo отметил в своем ответе, одно решения , чтобы создать новый экземпляр , Collectionкак ArrayListс предметами внутри основного списка; перебирать новый список и выполнять операции с основным списком.

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

1

Чтобы избавиться от ConcurrentModificationException, используйте CopyOnWriteArrayList


2
На самом деле это правильно, но: это зависит от конкретного класса, который здесь используется. Но когда вы знаете только, что у вас есть List<Something>- вы понятия не имеете, является ли это CopyOnWriteArrayList. Так что это больше похоже на посредственный комментарий, но не на настоящий ответ.
GhostCat

Если бы он разместил это как комментарий, кто-то, скорее всего, сказал бы ему, что он не должен публиковать ответы в виде комментариев.
Bill K

1

Вместо того, чтобы создавать странные вещи, вы можете просто filter()и потом получить map()свой результат.

Это гораздо более читабельно и надежно. Потоки сделают это всего за один цикл.


1

Да, вы можете изменить или обновить значения объектов в списке в вашем случае аналогичным образом:

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

Однако приведенный выше оператор обновит исходные объекты. Что в большинстве случаев может быть неприемлемым.

К счастью, у нас есть другой способ:

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

Что возвращает обновленный список обратно, не мешая старому.


0

Как уже упоминалось ранее, вы не можете изменять исходный список, но можете транслировать, изменять и собирать элементы в новый список. Вот простой пример изменения строкового элемента.

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]
    }
}

0

Вы можете использовать removeIfдля условного удаления данных из списка.

Например: - Если вы хотите удалить все четные числа из списка, вы можете сделать это следующим образом.

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

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

-1

Это может быть немного поздно. Но вот один из вариантов использования. Это для подсчета количества файлов.

Создайте указатель на память (в данном случае новый объект) и измените свойство объекта. Поток Java 8 не позволяет изменять сам указатель, и, следовательно, если вы объявите просто count как переменную и попытаетесь увеличить в потоке, он никогда не будет работать и в первую очередь вызовет исключение компилятора

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 + "]";
        }



    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.