Как работает метод indexOf() при замене внутри ArrayList?

Я проверяю код из пособия

ArrayList<Integer> numbers = new ArrayList<Integer>();

numbers.add(100);
numbers.add(101);
numbers.add(102);
numbers.add(103);
numbers.add(100);
numbers.add(105);

System.out.println("numbers before:\n" + numbers.toString());

numbers.forEach(number -> {
  numbers.set(numbers.indexOf(number), number * 2);
});

System.out.println("numbers after:\n" + numbers.toString());

Он работает, значения списка удваиваются. Но у меня возник вопрос: когда внутри forEach() я перебираю элементы и ищу индекс элемента по пришедшему значению что будет, если таких элементов два и более? У меня есть два элемента со значеним 100 (0 и 4 индекс), но как будет работать моя замена:

  • на первом шаге я получил 100, нашел его индекс (0) и заменил 100 на 200?
  • на пятом шаге я получил 100, нашел его индекс (4) и заменил 100 на 200?

Почему я спрашиваю? AI-помощник на сайте replit.com утверждает, что моя замена вообще не сработает, Java заменит только первое вхождение (0-ой индекс) и дальше при просмотре списка всегда будет находить 0-ой индекс как 100. Это как-то странно... Можно еще переформулировать вопрос: замена значений списка будет происходить постепенно элемент за элементом или как-то разово в конце цикла? Я думаю, что постепенно, но хотел спросить профессионалов.


Ответы (3 шт):

Автор решения: Grundy

после первой замены, в ArrayList останется только одна 100 - которая и будет заменена второй.

Проблема будет, если удвоенное значение будет равно одному из следующих.

Как привели пример в комментариях: [50, 100]

В этом случае, сначала 50 заменится на 100 на второй итерации indexOf найдет 100, бывшие 50 - и заменит их на 200

В итоге вместо [100, 200] получится [200, 100]

→ Ссылка
Автор решения: Nowhere Man

AI-помощник в данном случае не прав, так как после удвоения первого значения 100 по индексу 0 в списке останется только одно другое значение 100 по индексу 4, которое будет благополучно удвоено, когда до него дойдёт очередь в "цикле" forEach.

Однако, если бы входные данные были несколько другие, например, после удвоения более раннего числа получалось бы число, записанное "правее", то данный код не смог бы удвоить каждое значение исходного списка, так как List::indexOf всегда ищет с начала списка (индекса 0).

Пример:

ArrayList<Integer> numbers = new ArrayList<Integer>();

numbers.add(100);
numbers.add(200);
numbers.add(400);
numbers.add(800);
numbers.add(1600);
numbers.add(3200);

System.out.println("numbers before:\n" + numbers.toString());

numbers.forEach(number -> {
  numbers.set(numbers.indexOf(number), number * 2);
});

System.out.println("numbers after:\n" + numbers.toString());        

Результат:

numbers before:
[100, 200, 400, 800, 1600, 3200]
numbers after:
[6400, 200, 400, 800, 1600, 3200]

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


Если задача заключалась в том, чтобы удвоить значения именно в исходном списке, наиболее лаконичное решение -- вызвать функцию List::replaceAll, принимающую некоторую лямбда-функцию, которая будет применяться к каждому значению:

numbers.replaceAll(n -> n * 2);

Также можно было бы применить классический цикл с прохождением по индексам без привлечения indexOf:

for (int i = 0, n = numbers.size(); i < n; i++) {
    numbers.set(i, 2 * numbers.get(i));
}

или же аналогично использовать "поток" индексов:

IntStream.range(0, numbers.size()) // IntStream
    .forEach(i -> numbers.set(i, 2 * numbers.get(i)));

Касаемо вопроса:

Можно еще переформулировать вопрос: замена значений списка будет происходить постепенно элемент за элементом или как-то разово в конце цикла?

вы сами ответили на него верно: элементы будут заменяться постепенно, даже в случае упомянутой функции removeAll, которая эквивалентна циклу с экземпляром ListIterator'а, как описано в документации по вышеприведённой ссылке:

default void removeAll(UnaryOperator<E> operator) {
    final ListIterator<E> li = list.listIterator();
    while (li.hasNext()) {
        li.set(operator.apply(li.next()));
    }
}

Следует заметить, что более быстрый алгоритм применяется для условного удаления элементов списка ArrayList при помощи метода ArrayList::removeIf(Predicate<? super E> filter), оптимизированного в Java 9 (см. ответ на соответствующий вопрос)

→ Ссылка
Автор решения: Sandro

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

На будущее - чтобы самостоятельно отвечать на такие вопросы, тебе нужно точно знать, как ведет себя тот или иной метод. Для этого можно заглянуть в его документацию, а при необходимости - посмотреть его реализацию (в любой среде разработки есть возможность посмотреть исходный код любого класса из стандартной бибилиотеки. Например в IDEA можешь найти исходники класса используя комбинацию клавшь ctrl + N или просто зажать ctrl и кликнуть по имени класса в любом месте кода).

Поведение всех трех методов (indexOf, set, forEach) - можно посмотреть здесь: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/ArrayList.html#set(int,E)

→ Ссылка