Рубріки: Теория

Цикл for-each и метод forEach в Java

Андрій Денисенко

В Java, кроме цикла for, для итерации по коллекциям используется цикл for-each и методы forEach, forEachOrdered и forEachRemaining.

Цикл for-each

Цикл for-each осуществляет проход по элементам массива или коллекции. Его синтаксис таков:

for (тип имяПеременной : имяМассива) {
    // блок кода
}

Следующий пример выводит все элементы массива fruits в цикле for-each.

public class ForEachLoop {
    public static void main(String[] args) {
        String[] fruits = {"apple", "orange", "banana", "mango"};
        
        System.out.println("=== Цикл for-each ===");
        for(String fruit: fruits){
            System.out.println(fruit);
        }
    }
}

Ограничения цикла for-each

Цикл for-each нецелесообразно использовать, если нужно изменить коллекцию. Параметр цикла лишь получает значение очередного элемента коллекции, поэтому его изменение не приводит к изменению этого элемента.

import java.util.ArrayList;
import java.util.List;
public class ForEachNoChange {
    public static void main(String[] args) {
        List fruits = new ArrayList();
        fruits.add("apple");
        fruits.add("orange");
        fruits.add("banana");
        fruits.add("mango");
        
        for(String fruit: fruits){
            System.out.println("fruit = " + fruit);
            fruit = "grapes";
            System.out.println("Now fruit = \"" + fruit + "\"");
        }
        System.out.println();
        System.out.println("The list after attempting to change it:");
        for(String fruit: fruits){
            System.out.println(fruit);
        }
    }
}

Цикл for-each не отслеживает индекс элемента массива, поэтому его невозможно использовать, чтобы получить индекс, к которому прикреплен элемент.

for(String fruit: fruits){
    if(fruit == "banana"){
         // Мы знаем только значение элемента, но не индекс
    }
}

Для определения индекса нужно использовать цикл for.

for(int i = 0; i < fruits.length; i++){
    if(fruits[i] == "banana"){
         index = i;
         System.out.println(i);
    }
}

Цикл for-each проходит по циклу в прямом направлении, но не в обратном, с шагом в один элемент. Цикл for позволяет перебирать элементы в прямом и в обратном порядке с произвольным шагом. Например, следующий код выведет в консоли “mango” и “orange”, перебирая элементы от конца к началу с шагом 2:

String[] fruits = {"apple", "orange", "banana", "mango"};

for(int i = fruits.length - 1; i > 0; i-=2){
    System.out.println(fruits[i]);
}

В цикле for-each невозможно обработать два принятия решения одновременно:

for(int i = 0; i < numbers.length; i++){
    if(numbers[i] == arr[i]){
         // Непросто реализовать в цикле for-each
    }
}

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

Введенный в Java 8 метод forEach интерфейсов Iterable и Stream также имеет несколько преимуществ перед традиционным циклом. Например, итерацию можно выполнять параллельно, используя параллельный поток вместо обычного. Поскольку операции выполняются над потоком, можно фильтровать элементы и применять к ним функцию map. После этого можно применить метод forEach для итерации по элементам. В методе forEach можно использовать либо ссылку на метод, либо лямбда-выражение для получения чистого и краткого кода.

Рассмотрим метод forEach подробно.

Метод forEach

В Java 8 для итерации по элементам коллекций введен метод forEach. Он определен для интерфейсов Iterable и Stream.

Для интерфейса Iterable это метод по умолчанию. Классы коллекций, расширяющие интерфейс Iterable, могут использовать цикл forEach для итерации по элементам.

Сигнатура метода forEach:

void forEach(Consumer<? super T> action)

Интерфейс Consumer — это функциональный интерфейс (интерфейс с единственным абстрактным методом). Он принимает входной параметр и не возвращает результата.

Вот его определение:

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

Поэтому аргументу forEach можно передать любую реализацию, например такую, которая печатает строку:

Consumer printConsumer = new Consumer() {
    public void accept(String s) {
        System.out.println(s);
    };
};

Это не единственный способ создать действие с помощью Consumer и API forEach. Рассмотрим три самых распространенных способа использования метода forEach.

Анонимная реализация Consumer, пример которой приведен выше. Если проанализировать его, становится ясно, что полезная часть находится внутри метода accept. Чтобы код было удобно читать, чаще используют два следующих способа.

Лямбда-выражение. Большое преимущество функциональных интерфейсов Java 8 — возможность использовать лямбда-выражения для создания их экземпляров вместо написания громоздких реализаций анонимных классов. Поскольку интерфейс Consumer является функциональным, можно представить его в виде лямбда-выражения: (argument) -> { //body }

Метод printConsumer упрощается:

name -> System.out.println(name)

Теперь его можно передать методу forEach:

s.forEach(s -> System.out.println(s));

После внедрения лямбда-выражений в Java 8 это, вероятно, самый распространенный способ использования метода forEach.

Ссылка на метод. Вместо обычного синтаксиса лямбда-выражений можно использовать синтаксис ссылки на метод, если существует метод, который выполняет операцию над классом.

s.forEach(System.out::println);

Использование forEach для итерации по List

Для списков метод forEach используется следующим образом:

import java.util.ArrayList;
import java.util.List;
public class ForEachList {
    public static void main(String[] args) {
        List fruits = new ArrayList();
        fruits.add("apple");
        fruits.add("orange");
        fruits.add("banana");
        fruits.add("mango");
        
        System.out.println("=== Метод forEach для List ===");
        System.out.println();

        fruits.forEach(fruitList -> System.out.println(fruitList));
        System.out.println();
    }  
}

В этом примере кода метод forEach вызывается с передачей лямбда-выражения:

fruits.forEach(fruitList -> System.out.println(fruitList));

Вывод:

=== Метод forEach для List ===

apple
orange
banana
mango

Использование forEach для итерации по Set

В приведенном ниже примере показано, как метод forEach применяется к Set и вызывается с передачей ссылки на метод.

import java.util.HashSet;
import java.util.Set;

public class ForEachSet {

    public static void main(String[] args) {
        
        Set fruits = new HashSet<>();
        
        fruits.add("apple");
        fruits.add("orange");
        fruits.add("banana");
        fruits.add("mango");
        
        System.out.println("=== Метод forEach для Set ===");
        
        fruits.forEach((e) -> { System.out.println(e); });
    }
}

Вывод:

=== Метод forEach для Set ===
orange
banana
apple
mango

Использование forEach для итерации по Map

В этом примере метод forEach применяется к Map:

import java.util.HashMap;
import java.util.Map;

public class ForEachMap {

    public static void main(String[] args) {

        Map<Integer, String> fruits = new HashMap<>();

        fruits.put(1, "apple");
        fruits.put(2, "orange");
        fruits.put(3, "banana");
        fruits.put(4, "mango");
        
        System.out.println("=== Метод forEach для Map ===");

        fruits.forEach((k, v) -> {
            System.out.printf("%d: %s%n", k, v);
        });
    }
}

Вывод:

=== Метод forEach для Map ===
1: apple
2: orange
3: banana
4: mango

forEach с условием

Если нужно выбрать из коллекции определенные элементы, можно преобразовать ее в поток, а затем применить к нему метод filter. После этого можно провести итерацию только по отобранным элементам.

import java.util.ArrayList;
import java.util.List;

public class ForEachFilter {

    public static void main(String[] args) {

        List fruits = new ArrayList<>();

        fruits.add("apple");
        fruits.add("orange");
        fruits.add("banana");
        fruits.add("mango");
        
        System.out.println("=== Метод forEach с фильтрацией ===");
        fruits.stream().filter(fruit -> (fruit.length() == 5)).forEach(System.out::println);
    }
}

Вывод:

=== Метод forEach с фильтрацией ===
apple
mango

Итерация по Map с использованием entrySet и фильтрацией

В приведенном ниже примере кода из HashMap с рейтингом языков программирования за октябрь 2022 года выбираются записи с рейтингом больше семи.

import java.util.HashMap;
import java.util.Map;

public class JavaForEachExample {

    public static void main(String[] args) {

        Map<String, Double> languages = new HashMap<>();

        languages.put("Abap", 0.58);
        languages.put("Ada", 0.95);
        languages.put("C#", 7.79);
        languages.put("C/C++", 7.01);
        languages.put("Cobol", 0.34);
        languages.put("Dart", 0.64);
        languages.put("Delphi/Pascal", 0.16);
        languages.put("Go", 1.48);
        languages.put("Groovy", 0.48);
        languages.put("Haskell", 0.29);
        languages.put("Java", 17.64);
        languages.put("JavaScript", 9.21);
        languages.put("Julia", 0.41);
        languages.put("Kotlin", 1.57);
        languages.put("Lua", 0.51);
        languages.put("Matlab", 1.71);
        languages.put("Objective-C", 2.21);
        languages.put("Perl", 0.44);
        languages.put("PHP", 5.27);
        languages.put("Python", 27.61);
        languages.put("R", 4.26);
        languages.put("Ruby", 1.1);
        languages.put("Rust", 1.29);
        languages.put("Scala", 0.73);
        languages.put("Swift", 2.17);
        languages.put("TypeScript", 2.43);
        languages.put("VBA", 1.07);
        languages.put("Visual Basic", 0.65);
        
        languages.entrySet().stream().filter(x -> (x.getValue() > 7)).forEach(System.out::println);
    }
}

Сначала HashMap преобразуется во множество записей с помощью метода entrySet. Затем это множество преобразуется в поток методом stream. Далее методом filter отфильтровываются записи со значением больше 7. Отфильтрованные записи выводятся в консоль с помощью метода forEach.

Вывод:

C#=7.79
C/C++=7.01
JavaScript=9.21
Python=27.61
Java=17.64

Итерация по коллекции с использованием Stream.forEachOrdered


Если для перебора коллекции используется метод forEach, то не гарантируется, что он каждый раз будет выводить элементы в исходном порядке. Это хорошо видно на примере параллельного потока.

import java.util.ArrayList;
import java.util.List;

public class ForEachOrdered {
    public static void main(String[] args) {
        List fruits = new ArrayList();
        fruits.add("apple");
        fruits.add("orange");
        fruits.add("banana");
        fruits.add("mango");
        

        for(int i = 0; i < 3; i++){
            System.out.printf("Iteration %d:%n", i+1);
            fruits.stream().parallel().forEach(s->System.out.println(s));
            System.out.println();
        }
    }
}

Вывод может быть таким:

Iteration 1:
banana
mango
orange
apple

Iteration 2:
orange
banana
apple
mango

Iteration 3:
orange
apple
mango
banana

Элементы параллельного потока при использовании метода forEach выводятся в разном порядке.

Заменим метод forEach на forEachOrdered:

fruits.stream().parallel().forEachOrdered(s->System.out.println(s));

Для метода forEachOrdered вывод всегда будет совершаться в порядке вхождения элементов:

Iteration 1:
apple
orange
banana
mango

Iteration 2:
apple
orange
banana
mango

Iteration 3:
apple
orange
banana
mango

Этот метод имеет следующую сигнатуру:

void forEachOrdered(Consumer<? super T> action)

Здесь Consumer — это функциональный интерфейс, от которого ожидается, что он будет работать посредством побочных эффектов, а T — тип элементов потока.

Итерация с помощью Iterator.forEachRemaining

Наконец, итерацию по коллекции можно совершать с помощью метода forEachRemaining интерфейса Iterator.

import java.util.ArrayList;
import java.util.List;

public class ForEachRemaining {

    public static void main(String[] args) {
        
        List fruitList = new ArrayList();
        fruitList.add("apple");
        fruitList.add("orange");
        fruitList.add("banana");
        fruitList.add("mango");

        fruitList.iterator().forEachRemaining(E -> System.out.println(E));
    }
}

Заключение

В этой статье мы рассмотрели использование метода forEach для итерации по коллекциям и сравнили его с другими способами итерации. Мы узнали, как работает метод forEach и какими способами можно реализовать его аргумент. Также мы применили другие методы итерации по коллекции (forEachOrdered, forEachRemaining) и провели итерацию по отфильтрованным элементам коллекции.

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

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023