Модифікація об’єктів у потоці в Java8 під час ітерації


87

Чи можна в потоках Java8 змінювати / оновлювати об'єкти всередині? Наприклад, List<User> users:

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

Відповіді:


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
Що таке рішення, якщо ви хочете змінити користувачів?
Август

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

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

2
@Blauhirn Що вирішити? Нам не дозволено змінювати джерело потоку під час використання потоку, але нам дозволено змінювати стан його елементів. Не має значення, використовуємо ми карту, foreach чи інші потокові методи.
Пшемо

1
Замість модифікації колекції я б запропонував використовуватиfilter()
jontro

5

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

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

Для структурної модифікації джерела потоку, як згадав Пшемо у своїй відповіді, одним із рішень є створення нового екземпляра Collectionподібного ArrayListз елементами у вашому первинному списку; переглядайте новий список і виконуйте операції з первинним списком.

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

1

Щоб позбутися від ConcurrentModificationException, використовуйте CopyOnWriteArrayList


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

Якби він розмістив це як коментар, хтось, швидше за все, сказав би йому, що не повинен розміщувати відповіді як коментарі.
Білл К

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

Це може бути трохи пізно. Але ось одне з використання. Це для отримання підрахунку кількості файлів.

Створіть вказівник на пам’ять (у цьому випадку новий obj) і змініть властивість об’єкта. Потік 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.