1. Imperativ yanaşmanın problemi
Gəlin klassik bir tapşırıqdan başlayaq. Tutaq ki, bizdə istifadəçilərin siyahısı var:
List<User> users = ...; // artıq doldurulub
Tutaq ki, bizə bütün yetkin istifadəçilərin (18 yaşdan yuxarı) adlarının siyahısını almaq lazımdır. Bunu əvvəllər necə edirdik?
List<String> names = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
names.add(user.getName());
}
}
İlk baxışda hər şey sadədir, amma “texniki” addımlara fikir verin: nəticə üçün yeni siyahı yaratmaq, bütün elementlər üzərindən keçmək, şərti yoxlamaq, nəticəni əlavə etmək.
Əgər tapşırıq mürəkkəbləşirsə — məsələn, adı “A” hərfi ilə başlayan bütün istifadəçilərin unikal e‑poçt ünvanlarının əlifba sırası ilə sıralanmış siyahısını almaq lazımdırsa — kod sürətlə böyüyür: filtrasiya, sahələrin çıxarılması, dublikatların silinməsi, sıralama, bəlkə də e‑poçt formatının çevrilməsi və s. Nəticədə qısa və aydın məntiq əvəzinə oxumaq və dəstəkləmək çətin olan bir yığın boilerplate əldə edirsiniz.
Bu yanaşmanın mənfi cəhətləri qısa desək: çox təkrarlanan kod, səhv etmə ehtimalının yüksək olması (sıralamanı unutmaq və ya dublikatları səhv emal etmək), əməliyyatları birləşdirməyin çətinliyi. Buna imperativ üslub deyilir: siz kompüterə mərhələ‑mərhələ tapşırığın necə yerinə yetiriləcəyini izah edirsiniz, nəyi almaq istədiyinizi deyil.
2. Stream API — deklarativ üslub
Java 8 ilə birlikdə Stream API yarandı — kolleksiyalarla “nə etmək lazımdır”, yoxsa “tam olaraq necə etmək lazımdır” üslubunda işləməyə imkan verən alət. Bu yanaşma deklarativ adlanır.
Bəs ümumiyyətlə Stream nədir?
Bu ayrıca bir kolleksiya deyil, daha çox məlumat axınıdır: elementlər ardıcıllığı əməliyyatlar zəncirindən keçir — filtrasiya, çevrilmə, sıralama, nəticənin toplanması və s. Axın heç nə saxlamır, o, məlumatı sanki konveyerdən “keçirir”. Əməliyyatları demək olar ki, LEGO kubları kimi birləşdirmək olar: zənciri yığıb işə salırsınız.
Nümunə:
List<String> names = users.stream() // istifadəçilər siyahısından stream yaradırıq
.filter(u -> u.getAge() >= 18) // yalnız yaşı 18 və daha çox olanları saxlayırıq
.map(User::getName) // User → String çevrilməsi (adı götürürük)
.collect(Collectors.toList()); // nəticəni siyahıya toplayırıq
Və bu qədər — bir sətirdə bütün tapşırığı təsvir etdik: “istifadəçiləri götür, yaşa görə filtrlə, adları çıxart, siyahıya yığ”. Döngələri əl ilə yazmağa, müvəqqəti dəyişənlər yaratmağa və heç nəyi unutmadığınıza nəzarət etməyə ehtiyac yoxdur.
Stream API zavod konveyeri kimi işləyir: siz emal mərhələlərini təyin edirsiniz, məlumat isə özü onlardan keçir. Gözəl, yığcam və xeyli daha oxunaqlıdır.
3. Stream API-nin üstünlükləri
- Qısalıq və oxunaqlılıq. Eyni tapşırıq üzərində iki yanaşmanı — imperativ və deklarativ — müqayisə edin. Hansının oxuma və dəstəkləmə baxımından daha asan olduğu dərhal görünür.
- Əməliyyatların asan kompozisiyası. Əməliyyatları zəncirə “yığmaq” asandır: filter, map, sorted, collect — və bunların hamısı bir bütöv ardıcıllıqda.
- Daha az səhv. Stream API rutindən xilas edir: aralıq kolleksiyaları və döngələri əl ilə idarə etməyə ehtiyac yoxdur — addımı unutmaq və ya indekslə əlaqəli səhv etmək ehtimalı azalır.
- Paralelləşdirmə imkanı. Emalı bir neçə nüvəyə asanlıqla ölçüləndirmək olar — sadəcə parallelStream() əvəzinə stream() çağırın. Təfərrüatlar — sonra.
- Müasir proqramlaşdırma üslubu. Funksional proqramlaşdırma ideyaları (funksiyaların kompozisiyası, açıq döngələrin olmaması) ifadəliliyi artırır və bazarda yüksək qiymətləndirilir.
Stream API-nin qısa tarixi
Stream API Java 8-də (2014) ortaya çıxdı və dil üçün ciddi keyfiyyət sıçrayışı oldu. Buna qədər kolleksiyalar üzərində hər hansı qeyri‑trivial əməliyyat çoxlu köməkçi koda ehtiyac duyurdu, halbuki digər platformalar artıq deklarativ yanaşmaları (map, filter, reduce və s.) təklif edirdi.
O vaxtdan bəri Stream API — Java-da kolleksiya emalı üçün de‑fakto standartdır. Müasir Java kodu yazmaq istəyirsinizsə — onsuz mümkün deyil.
4. Stream API-nin tətbiq sahələri
- Filtrləmə: yalnız lazım olan elementləri seçmək (məsələn, 18 yaşdan yuxarı bütün istifadəçilər).
- Transformasiya: sahəni çıxarmaq və ya yeni obyekt yaratmaq (məsələn, istifadəçi adlarının siyahısı).
- Aqreqasiya: cəmi, orta, say, maksimum/minimumu hesablamaq.
- Sıralama: elementləri lazım olan əlamətə görə düzənləmək.
- Qruplaşdırma: elementləri kateqoriyalara bölmək.
- Nəticənin toplanması: siyahı, çoxluq, xəritə, sətir və s. şəklində toplamaq.
Tapşırıq nümunələri:
- Adı “A” ilə başlayan bütün istifadəçilərin e‑poçt siyahısını almaq.
- İstifadəçilərin orta yaşını hesablamaq.
- Müəyyən e‑poçt ünvanına malik ilk istifadəçini tapmaq.
- Bütün unikal yaşayış şəhərlərini bir çoxluqda toplamaq.
5. Faydalı incəliklər
Stream necə işləyir
[User1] --\
[User2] ---|--> [ filter ] --> [ map ] --> [ collect ] --> List<String>
[User3] --/
1. filter — şərtə cavab verən istifadəçiləri ötürür.
2. map — istifadəçini, məsələn, e‑poçta çevirir.
3. collect — e‑poçtu lazım olan kolleksiyaya toplayır.
Sintaksis: stream necə yaradılır
- Kolleksiyadan: list.stream() — kolleksiya elementlərinin stream-i.
- Massivdən: Arrays.stream(array)
- Ayrı dəyərlərdən: Stream.of("a", "b", "c")
6. Stream API-yə keçiddə tipik səhvlər
Səhv №1: kolleksiyanı stream daxilində dəyişməyə cəhd. Stream API ilkin kolleksiyanı modifikasiya etmək üçün nəzərdə tutulmayıb (məsələn, forEach daxilində list.remove() etmək olmaz). Silmələr üçün, məsələn, emaldan əvvəl və ya sonra kolleksiyanın özündə removeIf istifadə edin.
Səhv №2: kolleksiya ilə stream-i qarışdırmaq. Stream — kolleksiya deyil! Stream elementlər üzərindən bir dəfə “keçir”, sonra bağlanır. Məlumatı yenidən emal etmək lazımdırsa — yeni stream() yaradın.
Səhv №3: həddən artıq mürəkkəb zəncirlər. İfadəni onlarla əməliyyatdan ibarət “canavara” çevirməyin. Zəncir çox uzanırsa — oxunaqlılıq və sazlama üçün onu aralıq dəyişənlərlə bir neçə mərhələyə bölün.
GO TO FULL VERSION