CodeGym /Kurslar /JAVA 25 SELF /Tənbəl işləmə (lazy evaluation) Stream API-də

Tənbəl işləmə (lazy evaluation) Stream API-də

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

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
filter
map
ELEPHANT GIRAFFE
forEach
ç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.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION