1. Tənbəl işləmə nədir?
Tənbəl işləmə, yaxud “tənbəl qiymətləndirmə” (lazy evaluation), nəticə həqiqətən lazım olana qədər verilənlər üzərindəki əməliyyatların təxirə salınması prinsipidir. Stream API kontekstində bu o deməkdir: kolleksiya üzərində çevrilmələr zənciri yazsanız belə, Java onları dərhal icra etmir — terminal əməliyyatı çağırılana qədər gözləyir. Və yalnız bundan sonra bütün zəncir hesablanır.
Bu nə üçün lazımdır? Birincisi, resurslara qənaət olunur: sonda lazım olmayacaq elementlər ümumiyyətlə işlənmir. İkincisi, performans artır — çoxsaylı aralıq kolleksiyalar yaratmadan uzun zəncirlər qura bilərsiniz. Nəhayət, “qısa qiymətləndirmə” mümkündür: ilk uyğun element tapılan kimi sonrakı işləmə dayandırılır.
Bənzətmə: tənbəl bir ofisiant təsəvvür edin. Siz deyirsiniz: “Menyunu gətir, sonra qəhvə, sonra da piroq”. O başı ilə təsdiq edir, amma heç nə etmir… siz “İndi isə həqiqətən gətir” deməyənədək. Bax onda sifarişi yerinə yetirməyə gedir — və əgər piroqlar qalmayıbsa, təkcə qəhvəni gətirə bilər. Stream-lərdə tənbəl işləmə də təxminən belə işləyir.
2. Aralıq və terminal əməliyyatlar
Aralıq (intermediate):
- filter
- map
- sorted
- distinct
- peek (debug üçün)
- və başqaları
Aralıq əməliyyatlar yeni Stream qaytarır, lakin hesablamanı başlatmır. Onlar yalnız işləmənin “planını qururlar”.
Terminal (terminal):
- collect
- forEach
- reduce
- count
- findFirst, findAny
- anyMatch, allMatch, noneMatch
- və başqaları
Yalnız terminal əməliyyatı bütün zəncirin icrasını başladır.
Nümunə: terminal əməliyyatı olmadan heç nə baş vermir
List<String> names = List.of("Alisa", "Bob", "Vasya");
names.stream()
.filter(name -> {
System.out.println("Filtrləyirəm " + name);
return name.startsWith("A");
});
// Heç bir mesaj olmayacaq! Yuxarıdakı kod sadəcə zənciri "qurur".
İndi terminal əməliyyatı əlavə edək:
names.stream()
.filter(name -> {
System.out.println("Filtrləyirəm " + name);
return name.startsWith("A");
})
.forEach(System.out::println);
// İndi konsolda çıxışı görəcəyik!
Nəticə:
Filtrləyirəm Alisa
Filtrləyirəm Bob
Filtrləyirəm Vasya
Alisa
3. Tənbəl işləmənin üstünlükləri
Resurslara qənaət
Tənbəl işləmə lazımsız elementlərə vaxt və yaddaş sərf etməməyə imkan verir. Məsələn, ilk uyğun obyekti axtarırsınızsa, işləmə ilk uyğunluqda dayandırılacaq.
List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");
String firstA = names.stream()
.filter(name -> {
System.out.println("Yoxlayıram: " + name);
return name.startsWith("A");
})
.findFirst()
.orElse("Tapılmadı");
System.out.println("Nəticə: " + firstA);
Çıxış:
Yoxlayıram: Alisa
Nəticə: Alisa
Diqqət: digər elementlər heç yoxlanılmır!
Aralıq kolleksiyalar olmadan uzun zəncirlər
filter, map, sorted və s. kimi bir çox əməliyyatı hər addımda kolleksiyalar yaratmadan birləşdirmək olar.
List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList(); // Java 16+, əvvəllər — .collect(Collectors.toList())
Qısa qiymətləndirmə
“Elementlər arasında uyğun varmı?” sualının cavabı kifayətdirsə, qalanları yoxlanmayacaq:
boolean hasLongName = names.stream()
.anyMatch(name -> {
System.out.println("Yoxlayıram: " + name);
return name.length() > 10;
});
// İlk element uzundursa — qalanlar yoxlanmayacaq!
4. Nümunələr: tənbəl işləmə necə işləyir
Nümunə 1: terminal əməliyyatı olmadan heç nə baş vermir
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream()
.filter(n -> {
System.out.println("Filtrləyirəm " + n);
return n % 2 == 0;
});
// Heç bir çıxış yoxdur!
Nümunə 2: terminal əməliyyatlı zəncir
numbers.stream()
.filter(n -> {
System.out.println("Filtrləyirəm " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("Vururam " + n);
return n * 10;
})
.forEach(System.out::println);
Çıxış:
Filtrləyirəm 1
Filtrləyirəm 2
Vururam 2
20
Filtrləyirəm 3
Filtrləyirəm 4
Vururam 4
40
Filtrləyirəm 5
Vacib qeyd: əməliyyatlar element-əsaslı yerinə yetirilir: əvvəl filter, sonra map, daha sonra forEach — hər element üçün növbə ilə. Bu, “öncə hamısını filtrlə, sonra hamısını çevril” şəklində iki ayrı keçid deyil.
Nümunə 3: peek ilə debug
numbers.stream()
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("Filtri keçdi: " + n))
.map(n -> n * 10)
.peek(n -> System.out.println("map-dən sonra: " + n))
.forEach(System.out::println);
5. Faydalı nüanslar
Yan təsirlərlə stream-lərdən istifadə etməyin
Tənbəllik, dərhal icraya güvəndikdə xoşagəlməz sürprizlər yarada bilər. map, filter və ya peek içindəki yan təsirlər (fayla yazmaq, xarici vəziyyəti dəyişmək) gözlənilən sırada icra olunmaya, bütün elementlər üçün işləməyə və ya terminal əməliyyatı olmadan ümumiyyətlə icra edilməyə bilər.
Mümkün qədər tez filtr edin
Artıq elementləri daha tez kəsmək və sonrakı işi azaltmaq üçün filter əməliyyatını zəncirin əvvəlinə yaxın yerləşdirin.
Yalnız ilk nəticə lazımdır? Müvafiq terminallardan istifadə edin
Əgər ilk uyğun element lazımdırsa — findFirst və ya findAny çağırın. Bu, stream-in nəticə tapılar-tapılmaz dayanmasına imkan verir.
Stream-lər ilkin kolleksiyanı dəyişdirmək üçün deyil
Stream-lər ilkin kolleksiyaya element əlavə/çıxarmaq üçün nəzərdə tutulmayıb. Kolleksiyanın strukturunu dəyişmək üçün digər mexanizmlərdən istifadə edin.
Tənbəl stream-lərin işinin vizuallaşdırılması
List<String> words = List.of("cat", "dog", "elephant", "fox", "giraffe");
words.stream()
.filter(w -> w.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println);
Bu belə baş verir:
| Mərhələ | cat | dog | elephant | fox | giraffe |
|---|---|---|---|---|---|
|
✗ | ✗ | ✓ | ✗ | ✓ |
|
— | — | ELEPHANT | — | GIRAFFE |
|
— | — | çap | — | çap |
Cədvəl: eager və lazy yanaşmaların müqayisəsi
| Yanaşma | İşləmə nə vaxt icra olunur? | Yaddaş istifadəsi | Performans |
|---|---|---|---|
| Eager (acgöz) | Çağırışla dərhal | Çox ola bilər | Bəzən yavaşdır |
| Lazy (tənbəl) | Yalnız ehtiyac olduqda | Minimum | Adətən daha sürətlidir |
Eager yanaşma — məsələn, kolleksiya üzərindən bir neçə keçidi əl ilə təşkil edərək aralıq siyahılar yaratdığınız haldır.
Tənbəl yanaşma — stream-lərdir: nəticə lazım olmayana qədər heç nə edilmir.
6. Tənbəl stream-lərlə işləyərkən tipik səhvlər
Səhv №1: dərhal nəticə gözləmək. Yeni başlayanlar düşünür ki, filter və ya map çağırışları dərhal icra olunur. Amma terminal olmadan (məsələn, collect, forEach) heç nə baş verməyəcək — nəticədə “debug işləmir”, “heç nə çıxarmır”.
Səhv №2: aralıq əməliyyatlardakı yan təsirlər. Fayla yazmaq, xarici dəyişənləri dəyişmək kimi işlər map/filter/peek daxilində pis təcrübədir. Tənbəllik və optimizasiyalar səbəbilə bu cür hərəkətlər tam yerinə yetirilməyə, gözlənilən sırada olmaya və ya ümumiyyətlə icra edilməyə bilər.
Səhv №3: terminal əməliyyatını çağırmağı unutmaq. Stream zənciri yazılıb, ancaq collect, forEach və s. ilə tamamlanmayıb. Nəticə — “sükut”.
Səhv №4: bütün elementlərin işlənəcəyini gözləmək. findFirst və ya anyMatch kimi əməliyyatlar ilk nəticədə konveyeri dayandırır. Qalan elementlər işlənmir — buna görə “nə üçün mənim println hamısı üçün işləmədi?” təəccübü yaranır.
Səhv №5: stream-lərdən ilkin kolleksiyanı dəyişmək üçün istifadə etmək. Stream-lər ilkin kolleksiyaları modifikasiya etmək (element əlavə/çıxarmaq) üçün nəzərdə tutulmayıb. Kolleksiyaların ixtisaslaşmış metodlarından və ya iteratorlardan istifadə edin.
GO TO FULL VERSION