Avatar ·

Как на самом деле работает цикл for-each в Java? Подробное объяснение с примерами

Всем привет! Я начал изучать Java недавно и столкнулся с циклом for-each. Вроде бы всё понятно: for (int x : numbers) { ... } — просто перебирает элементы массива или коллекции. Но когда я начал копать глубже, возникло много вопросов. Например, я написал такой код:
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String s : list) {
    if (s.equals("B")) {
        list.remove(s); // ConcurrentModificationException!
    }
}
И получил ConcurrentModificationException. Почему? Ведь в обычном for (int i = 0; i < list.size(); i++) такого не происходит. Другой пример — я попытался изменить элементы массива через for-each:
int[] arr = {1, 2, 3};
for (int x : arr) {
    x = x * 2; // Не работает! Массив не изменился
}
Почему x не ссылается на элемент массива? И что вообще происходит "под капотом"? Может ли for-each работать с любыми типами данных? Как компилятор обрабатывает этот синтаксис? В интернете много ответов, но хотелось бы получить полное и понятное объяснение с примерами. Спасибо! <<>>

Как работает for-each: два разных механизма под одной обёрткой

Цикл for-each (также известный как enhanced for loop) — это синтаксический сахар, который компилятор Java преобразует в разные конструкции в зависимости от того, с чем вы работаете: с массивом или с объектом, реализующим Iterable.

1. For-each с массивами

Когда вы пишете:

int[] arr = {1, 2, 3};
for (int x : arr) {
    System.out.println(x);
}

Компилятор преобразует это в обычный цикл for с индексом:

int[] arr = {1, 2, 3};
for (int i = 0; i < arr.length; i++) {
    int x = arr[i];
    System.out.println(x);
}

Ключевой момент: переменная x — это копия значения из массива. Поэтому изменение x не влияет на исходный массив. Если вы работаете с массивом объектов, то x будет ссылкой на объект, и вы можете изменить свойства объекта, но не саму ссылку в массиве.

2. For-each с Iterable (коллекции, списки, множества)

Если вы пишете:

List<String> list = Arrays.asList("A", "B", "C");
for (String s : list) {
    System.out.println(s);
}

Компилятор преобразует это в:

List<String> list = Arrays.asList("A", "B", "C");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
    String s = it.next();
    System.out.println(s);
}

То есть for-each неявно создаёт итератор и использует его методы hasNext() и next().

Почему возникает ConcurrentModificationException?

Вернёмся к вашему первому примеру:

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String s : list) {
    if (s.equals("B")) {
        list.remove(s); // Проблема!
    }
}

После преобразования компилятором это выглядит так:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next(); // Здесь проверяется модификация
    if (s.equals("B")) {
        list.remove(s); // Изменяем список напрямую, а не через итератор
    }
}

Итератор ArrayList при вызове next() проверяет, не изменился ли список с момента создания итератора (через счётчик modCount). Когда вы вызываете list.remove(), счётчик увеличивается, итератор об этом не знает — и выбрасывает исключение. Правильный способ удалять элементы:

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("B")) {
        it.remove(); // Удаление через итератор — безопасно
    }
}

Что можно и нельзя делать в for-each

  • Можно: читать элементы, изменять свойства объектов (не сами ссылки), использовать с любым Iterable
  • Нельзя: изменять коллекцию (добавлять/удалять элементы), изменять значения в массиве примитивов, работать с несколькими коллекциями одновременно (нужен явный итератор)
  • Ограничение: нет доступа к индексу элемента (если нужен — используйте обычный for)

Важное замечание про производительность

Для ArrayList for-each и обычный for с индексом работают одинаково быстро. Но для LinkedList обычный for с get(i) работает медленнее (O(n²)), так как каждый раз проходится от начала списка. For-each с LinkedList использует итератор и работает за O(n).

Можно ли использовать for-each с собственными классами?

Да! Ваш класс должен реализовать интерфейс Iterable<T>:

public class MyCollection<T> implements Iterable<T> {
    private T[] items;
    
    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            private int index = 0;
            
            @Override
            public boolean hasNext() {
                return index < items.length;
            }
            
            @Override
            public T next() {
                return items[index++];
            }
        };
    }
}

Теперь можно писать: for (T item : myCollection) { ... }

<<>>

Альтернативный взгляд: for-each как инструмент функционального стиля

Хотя технически for-each — это просто синтаксический сахар, с точки зрения проектирования кода он поощряет более декларативный подход. Вместо того чтобы думать "как перебрать элементы" (индекс, счётчик, условие выхода), вы думаете "что сделать с каждым элементом".

Начиная с Java 8, у нас есть ещё более выразительные инструменты:

// Старый for-each
for (String s : list) {
    if (s.startsWith("A")) {
        System.out.println(s.toUpperCase());
    }
}

// Функциональный стиль
list.stream()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println);

Stream API решает многие проблемы for-each:

  • Безопасное изменение: можно использовать collect(Collectors.toList()) вместо удаления элементов
  • Параллельная обработка: parallelStream()
  • Ленивые вычисления: цепочки операций не выполняются, пока не понадобится результат

Однако for-each остаётся лучшим выбором для простых итераций, когда не нужны трансформации данных. Он понятнее для новичков и не создаёт лишних объектов (в отличие от stream, где каждый раз создаётся новый стрим).

Совет: если ваш for-each становится длиннее 5-7 строк с вложенными условиями — подумайте о рефакторинге в stream или вынесите логику в отдельный метод.