1. forEach metodu: elementlər üzərində son əməl
Detallara dalmazdan əvvəl gəlin bir daha xatırlayaq: axında terminal və aralıq əməliyyatlar nədir.
- Aralıq əməliyyatlar (məsələn, filter, map, distinct, peek) — yeni axın qaytarır və adətən terminal əməliyyat çağırılana qədər icra olunmur.
- Terminal əməliyyatlar (məsələn, forEach, collect, count, anyMatch) — axının elementlərinin emalını başladır və nəticə qaytarır (və ya heç nə qaytarmır, məsələn, forEach).
Terminal əməliyyatdan sonra axın bağlanmış sayılır və ona daha başqa əməliyyatlar tətbiq etmək olmaz. Bu, artıq yeyilmiş dondurmanı yenidən yeməyə cəhd etmək kimidir — alınmayacaq: axın artıq «istifadə olunub».
İndi yan təsirlərlə işləmək üçün iki vacib metodla tanış olaq: forEach və peek.
forEach nə edir?
forEach — axının terminal əməliyyatıdır və axının hər bir elementi üçün verilmiş əməli icra edir. Adətən ekrana çıxış, loqa yazma, statistika toplama və digər yan təsirlər (side effects) üçün istifadə olunur.
Metodun siqnaturası:
void forEach(Consumer<? super T> action)
Consumer<T> — bir arqument qəbul edən və heç nə qaytarmayan funksional interfeysdir (məsələn, System.out::println).
Nümunə: Siyahının bütün elementlərini çıxarmaq
Tutaq ki, istifadəçi adlarının siyahısı var:
List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");
Bütün istifadəçiləri Stream API ilə ekrana çıxarmaq çox asandır:
users.stream().forEach(System.out::println);
Nəticə:
Anna
Boris
Alex
Alina
Dmitry
Lambda ifadəsindən də istifadə etmək olar:
users.stream().forEach(name -> System.out.println("İstifadəçi: " + name));
Nəticə:
İstifadəçi: Anna
İstifadəçi: Boris
İstifadəçi: Alex
İstifadəçi: Alina
İstifadəçi: Dmitry
Vacibdir: forEach axını tamamlayır
forEach çağırıldıqdan sonra axın «bağlanır». Zənciri davam etdirmək olmaz:
users.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println)
.map(String::toUpperCase); // Səhv! Axın artıq bağlanıb.
forEach-dən sonra nəsə çağırmaq kompilasiya səhvinə gətirib çıxaracaq: terminal əməliyyat void qaytarır, yeni axın yox.
2. peek metodu: baxırıq, amma müdaxilə etmirik
peek — aralıq əməliyyatdır. O, emalın müəyyən mərhələsində hər bir element üçün hərəkət icra etməyə imkan verir, elementi dəyişmədən və axını tamamlamadan.
Metodun siqnaturası:
Stream<T> peek(Consumer<? super T> action)
- peek yeni axın qaytarır və bu axında hər element üçün action icra olunacaq.
- Adətən sazlama (debug), loqlama və ya axının vəziyyətini izləmək üçün istifadə olunur.
Nümunə: Filtrləmədən sonra loqlama
List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");
List<Integer> nameLengths = users.stream()
.filter(name -> name.startsWith("A"))
.peek(name -> System.out.println("Filtri keçdi: " + name))
.map(String::length)
.collect(Collectors.toList());
Konsoldakı nəticə:
Filtri keçdi: Anna
Filtri keçdi: Alex
Filtri keçdi: Alina
nameLengths-in məzmunu:
[4, 4, 5]
peek-i harada istifadə etmək uyğundur?
- Əməliyyatlar zəncirini sazlamaq üçün: hər mərhələdə nə baş verdiyini görmək.
- Statistika toplamaq üçün (məsələn, elementlərin sayını hesablamaq).
- Aralıq mərhələlərdə məlumatları loqa yazmaq üçün.
Vacibdir: peek axın elementlərini dəyişmək üçün istifadə olunmamalıdır. Transformasiyalar üçün map var. peek — «baxmaq» üçündür, «qarışmaq» üçün yox.
3. forEach vs peek: fərq nədir?
| Metod | Əməliyyat növü | Nə zaman tətbiq olunur | Zənciri davam etdirmək olar? | Nəyə daha uyğundur |
|---|---|---|---|---|
|
Terminal | Axının işlənməsinin sonunda | Xeyr | Son hərəkətlər (çıxış, loqlama, MB-yə yazma) |
|
Aralıq | Əməliyyatlar zəncirinin ortasında | Bəli | Sazlama, aralıq loqlama, sayma |
Nümunə: istifadədə fərq
// forEach nümunəsi
users.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println); // Burada axın tamamlanır
// peek nümunəsi
users.stream()
.filter(name -> name.startsWith("A"))
.peek(name -> System.out.println("Filtri keçdi: " + name))
.map(String::toUpperCase)
.collect(Collectors.toList()); // Zənciri davam etdirmək olar
Vacib məqamlar
- forEach — dönüş nöqtəsidir: ondan sonra axınla daha heç nə etmək olmur.
- peek terminal əməliyyat çağırılmasa, hərəkətlərin icrasını zəmanət etmir. Yalnız aralıq əməliyyatlardan ibarət zəncir yazsanız — heç nə baş verməyəcək.
4. Gözə görünməyən məqamlar: forEach həmişə ən yaxşı seçim deyil!
Niyə kolleksiyaları dəyişmək üçün forEach istifadə etmək olmaz?
Bir çox yeni başlayanlar forEach vasitəsilə kolleksiyanın elementlərini və ya kolleksiyanın özünü dəyişməyə (məsələn, elementləri silməyə) çalışırlar. Amma bu, pis təcrübədir: axınlar ilkin kolleksiyaları modifikasiya etmək üçün nəzərdə tutulmayıb.
Yanlış istifadəyə nümunə:
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // ConcurrentModificationException-a səbəb ola bilər!
}
});
Nəticə: icra zamanı xəta — axın üzərində keçid zamanı kolleksiyanı dəyişmək olmaz (ConcurrentModificationException).
Bəs forEach nə üçün istifadə olunmalıdır?
- Ekrana çıxış üçün (məsələn, hesabatın çapı).
- Loqa yazmaq üçün.
- Xarici servisləri çağırmaq üçün (məsələn, e-mail göndərmək).
- Statistika toplamaq üçün (məsələn, sayğacı artırmaq).
5. Bir daha peek haqqında: yalnız sazlama üçün!
Çox vaxt peek-dən elementləri dəyişmək üçün istifadə etmək istəyirlər, məsələn, istifadəçinin yaşını artırmaq:
users.stream()
.peek(user -> user.setAge(user.getAge() + 1)) // Pisdir!
.collect(Collectors.toList());
Niyə pisdir?
- Bu, Stream API-nin deklarativliyini və təmizliyini pozur.
- Belə kodu dəstəkləmək və test etmək çətinləşir.
- Aralıq əməliyyatda yan təsirlər gözlənilməz xətalara səbəb ola bilər.
Məlumatı çevirmək üçün map-dən istifadə etmək daha yaxşıdır:
List<User> olderUsers = users.stream()
.map(user -> new User(user.getName(), user.getAge() + 1))
.collect(Collectors.toList());
Sxem: forEach və peek arasındakı fərq
users.stream()
.filter(...) // aralıq əməliyyat
.peek(...) // aralıq əməliyyat, "baxırıq"
.map(...) // aralıq əməliyyat
.forEach(...) // terminal əməliyyat, "əməl edirik"
İzah:
— forEach-dən əvvəl olanların hamısını kombinə etmək, yerlərini dəyişmək, əlavə etmək olar.
— forEach-dən sonra axın bağlanır.
6. forEach və peek ilə işləyərkən tipik səhvlər
Səhv №1: peek-dən məlumatı dəyişmək üçün istifadə etmək. peek müşahidə üçündür, axın elementlərini dəyişmək üçün yox. Transformasiyalar üçün map-dən istifadə edin.
Səhv №2: peek-in həmişə icra olunacağını gözləmək. peek yalnız ondan sonra terminal əməliyyat olduqda icra olunur (collect, forEach, count və s.). Terminal əməliyyat olmadan heç nə baş verməyəcək.
Səhv №3: forEach-dən sonra axını davam etdirməyə çalışmaq. forEach — terminal əməliyyatdır. Ondan sonra axının digər metodlarını çağırmaq olmaz.
Səhv №4: forEach daxilində kolleksiyanı modifikasiya etmək. İlkin kolleksiyanı dəyişmək (elementləri silmək və ya əlavə etmək) forEach vasitəsilə keçid zamanı — birbaşa ConcurrentModificationException-a aparan yoldur.
Səhv №5: Nəticəni toplamaq üçün forEach-dən collect əvəzinə istifadə etmək. Elementləri yeni kolleksiyaya toplamaq istəyirsinizsə, collect(Collectors.toList())-dən istifadə edin, forEach ilə əl ilə əlavə etməyin. Bu, deklarativliyi pozur və çoxaxınlı ssenarilərdə xətalara səbəb ola bilər.
GO TO FULL VERSION