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

Цикл 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 поступається звичайній ітерації з погляду продуктивності.

Метод forEach, який введено в Java 8 для інтерфейсів 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<String> fruits = new ArrayList<>();

        fruits.add("apple");
        fruits.add("orange");
        fruits.add("banana");
        fruits.add("mango");
        
        System.out.println("=== Метод forEach + filter ===");
        fruits.stream().filter(fruit -> (fruit.length() == 5)).forEach(System.out::println);
    }
}

Вивід:

=== Метод forEach + filter ===
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 ForEachOrdered2 {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<String>();
        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. Також він дає змогу писати легкий для читання код, хоча інколи поступається продуктивністю циклу. 

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

Українські програмісти створили Lağoda QT — гру-головоломку кримськотатарською мовою

Українські програмісти створили безплатну гру-головоломку Lağoda QT.  Кожен рівень — вірш одного з видатних кримськотатарських…

07.05.2024

В Copilot для Microsoft 365 додали українську мову

Корпорація Microsoft оголосила про підтримку української мови у Copilot для Microsoft 365. Українська мова входить…

07.05.2024

Google безплатно навчатиме створювати чат-боти за допомогою Gemini. Потрібно тільки знання Python

Корпорація Google запустила реєстрацію задля участі в безплатній програмі Startup School: Gen AI. Програма безплатна…

07.05.2024

Вакансій і наймів більше, а зарплати — менше: що відбувалося на ринку праці у квітні

В квітні на ринку праці збільшилася кількість вакансій для IT-фахівців. На DOU та Djinni спостерігались…

07.05.2024

І всього лише $300. Китайці представили ноутбук на базі RISC-V для ШІ-девелоперів

Китайський стартап SpacemiT представив MuseBook — ноутбук на базі восьмиядерного процесора K1 RISC-V, орієнтований на…

06.05.2024

Учасники Brave1 створили ШІ-платформу HARVESTER для органів держбезпеки

Учасники Brave1, українська команда MATHESIS, розробила для органів держбезпеки платформу HARVESTER на основі штучного інтелекту.…

06.05.2024