Введение: почему 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 с индексом — доли микросекунды на миллион элементов. Но есть два случая,