Как работает for-each в Java: разбор на примерах от новичка до профи

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему for-each стал "королём" циклов в Java?

Если вы писали на Java хотя бы неделю, вы точно встречали конструкцию for (Type var : collection). Этот лаконичный синтаксис называют циклом for-each (или enhanced for loop). Он появился ещё в Java 5 (2004 год) и быстро стал любимцем разработчиков.

Но знаете ли вы, что происходит под капотом, когда JVM встречает эту конструкцию? Почему for-each может быть медленнее обычного for для массивов? И почему он иногда выкидывает ConcurrentModificationException, даже если вы ничего не удаляете?

В этой статье мы разберём как работает for-each в Java на уровне байт-кода, сравним с классическими циклами и научимся избегать типичных ловушек. Поехали!

Синтаксис и первое знакомство

Базовый синтаксис выглядит так:

for (ТипЭлемента переменная : коллекция_или_массив) {
    // тело цикла
}

Пример с ArrayList:

List<String> names = Arrays.asList("Анна", "Борис", "Виктор");

for (String name : names) {
    System.out.println("Привет, " + name + "!");
}

Вывод:

Привет, Анна!
Привет, Борис!
Привет, Виктор!

Всё просто? Да. Но за этой простотой скрывается два совершенно разных механизма — для массивов и для коллекций.

Как for-each работает с массивами?

Когда вы пишете for-each для массива, компилятор Java превращает его в обычный for с индексом. Никакой магии.

Ваш код:

int[] numbers = {10, 20, 30};

for (int num : numbers) {
    System.out.println(num);
}

После компиляции (декомпилируйте байт-код — убедитесь):

int[] numbers = new int[]{10, 20, 30};
for (int i = 0; i < numbers.length; i++) {
    int num = numbers[i];
    System.out.println(num);
}

Важный вывод: для массивов for-each и обычный for — одно и то же по производительности. Никаких накладных расходов на итераторы.

Как for-each работает с коллекциями (List, Set)?

А вот тут начинается самое интересное. Для коллекций for-each использует итератор.

Ваш код:

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");

for (String lang : list) {
    System.out.println(lang);
}

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

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");

for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
    String lang = it.next();
    System.out.println(lang);
}

То есть:

  • Вызывается list.iterator() — получаем итератор.
  • В условии — it.hasNext().
  • В теле — it.next().
  • Переменная lang инициализируется результатом next().

Что это значит на практике?

  • Нельзя удалять элементы внутри for-each через list.remove() — вы получите ConcurrentModificationException. Используйте it.remove() или обычный for.
  • Нельзя изменять коллекцию (добавлять/удалять) во время итерации — та же ошибка.
  • Производительность: для ArrayList разница с обычным for минимальна (создание объекта итератора). Для LinkedList — for-each гораздо быстрее, чем for (int i = 0; i < list.size(); i++), потому что get(i) у LinkedList — это O(n) каждый раз!

Что насчёт Map? Итерация по парам ключ-значение

Map не реализует Iterable напрямую, поэтому for-each по Map не работает. Но можно итерировать по entrySet():

Map<String, Integer> scores = new HashMap<>();
scores.put("Алиса", 95);
scores.put("Боб", 87);

for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

Это тоже использует итератор entrySet().

Продвинутые нюансы: что вы могли не знать

1. For-each и null-коллекции

Если коллекция равна null, for-each выбросит NullPointerException. Всегда проверяйте на null перед итерацией:

List<String> list = null;
// for (String s : list) { } // NPE!

if (list != null) {
    for (String s : list) { /*...*/ }
}

2. For-each и примитивные типы

Работает отлично с int, double и т.д. — никакого autoboxing.

3. Разница между for-each и stream().forEach()

С Java 8 появился list.stream().forEach(...). Чем отличается?

  • For-each — внешняя итерация, вы управляете порядком.
  • Stream.forEach — внутренняя итерация, порядок не гарантирован (если не sequential).
  • Stream.forEach может работать параллельно (parallelStream).
  • For-each не позволяет использовать лямбды с non-final переменными.

Пример:

List<String> list = Arrays.asList("a", "b", "c");

// For-each
for (String s : list) {
    System.out.println(s);
}

// Stream forEach
list.stream().forEach(s -> System.out.println(s));

4. For-each с собственными классами (Iterable)

Вы можете сделать свой класс "итерируемым", реализовав интерфейс Iterable<T>:

class MyCollection implements Iterable<String> {
    private String[] data = {"один", "два", "три"};

    @Override
    public Iterator<String> iterator() {
        return Arrays.asList(data).iterator();
    }
}

// Теперь можно:
MyCollection mc = new MyCollection();
for (String s : mc) {
    System.out.println(s);
}

Производительность: когда for-each может быть медленнее?

Для ArrayList разница между for-each и обычным for с индексом — доли микросекунды на миллион элементов. Но есть два случая,

Блоги

Рекомендаций по книгам