1. Axın yaradırıq
Stream API-dən istifadə etmək üçün əvvəlcə hər hansı bir kolleksiyadan və ya massivdən axın əldə etmək lazımdır.
Axın yaratma nümunələri
// Siyahıdan
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// Massivdən
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// Ayrı-ayrı dəyərlərdən
Stream<String> letters = Stream.of("A", "B", "C");
Qısa olaraq:
- list.stream() — kolleksiyalar üçün
- Arrays.stream(array) — massivlər üçün
- Stream.of(...) — ayrı-ayrı dəyərlər üçün
Tətbiqimiz kontekstində nümunə
Tutaq ki, bizdə istifadəçilərin siyahısı var:
List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();
Aralıq və terminal əməliyyatlar
Vacib məqam: Stream API-də əməliyyatlar iki tipə bölünür.
- Aralıq əməliyyatlar (məsələn, filter, map, distinct) — işləmə mərhələlərini təsvir edir. Onlar yeni axın qaytarır, lakin öz-özlüyündə heç nəyi işə salmır.
- Terminal əməliyyatlar (məsələn, collect, forEach, count) — konveyeri işə salır və nəticə verir.
Axın “tənbəl” işləyir: terminal əməliyyat çağırılmayana qədər heç bir hesablamalar aparılmır. Məhz buna görə biz tez-tez zənciri collect(...) ilə bitiririk — məhz burada axın yenidən kolleksiyaya və ya digər nəticəyə çevrilir.
2. filter əməliyyatı: elementləri şərtə görə süzgəcdən keçirmək
filter — aralıq əməliyyatdır, verilmiş şərtə uyğun gələn elementləri buraxır.
İmza
Stream<T> filter(Predicate<? super T> predicate);
Predicate — elementi qəbul edib true (saxla) və ya false (kənar et) qaytaran funksional interfeysdir.
Nümunə: yalnız cüt ədədləri saxlamaq
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
Nə baş verir?
- n -> n % 2 == 0 — lambda ifadəsidir, ədədin 2-yə qalıqsız bölünüb-bölünmədiyini yoxlayır.
- filter yalnız cüt ədədləri saxlayır.
Nümunə: “A” ilə başlayan adları filtrləyirik
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
Vacib məqam: filter kolleksiyanı dəyişmir — o, yalnız lazım olan elementlərin olduğu yeni bir axın yaradır.
3. map əməliyyatı: elementi başqa bir şeyə çevirmək
map — çevirmə əməliyyatıdır. O, axının hər bir elementini götürür, ona funksiyanı tətbiq edir və yeni element qaytarır.
İmza
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function — elementi qəbul edib nəsə (başqa tip də ola bilər) qaytaran interfeysdir.
Nümunə: sətirlərin uzunluqlarını əldə etmək
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
Nə baş verir?
- map sətiri onun uzunluğuna çevirir (name -> name.length()).
- Nəticədə ədədlər axını alınır.
Nümunə: sətirləri böyük hərflərə çevirmək
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. collect əməliyyatı: nəticəni yenidən kolleksiyaya toplamaq
collect — terminal əməliyyatdır, yəni axının işini tamamlayır və nəticəni kolleksiyaya və ya başqa konteynerə toplayır.
İmza
<R, A> R collect(Collector<? super T, A, R> collector)
Qorxuducu imzadan çəkinməyin! Halların 99 %‑ində siz hazır kollektorları Collectors sinfindən istifadə edirsiniz.
Collectors — “toplayıcılar” dəstinə malik utilit sinifdir. O, axına nəticəni hansı formaya toplamaq lazım olduğunu bildirir: siyahı, çoxluq, sətir və s.
Nümunələr:
- Collectors.toList() — List-ə
- Collectors.toSet() — Set-ə
- Collectors.joining(", ") — vergüllə ayrılmış sətirə
Yəni Collectors — axın elementlərini yerləşdirdiyiniz müxtəlif formalı qutular dəsti kimidir.
Nümunə: nəticəni List-ə toplamaq
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
Nümunə: nəticəni Set-ə toplamaq
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
Nümunə: sətirləri vergüllə birləşdirmək
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. Əməliyyatlar zənciri: filtrləmə + çevirmə + nəticənin toplanması
Stream API-nin ən böyük gücü — əməliyyatları bir-birinin ardınca zəncirləmək imkanıdır.
Nümunə: “A” ilə başlayan adların uzunluqlarını əldə etmək
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
Addım-addım:
- .stream() — siyahıdan axın yaradırıq.
- .filter(name -> name.startsWith("A")) — yalnız adların "A" ilə başlayanlarını saxlayırıq.
- .map(String::length) — hər adı onun uzunluğuna çeviririk.
- .collect(Collectors.toList()) — nəticəni siyahıya toplayırıq.
Oxşar imperativ kod
Eyni şey “köhnə üsulla” belə görünərdi:
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
Müqayisə edin: Stream API — bir sətir və “nə edirik” kimi oxunur, “necə edirik” kimi yox.
6. Təcrübə: bir neçə qısa tapşırıq
Məşq edək! Bütün nümunələri bir faylda işə sala bilərsiniz — sadəcə verilənləri dəyişin.
Tapşırıq 1: yalnız tək ədədləri saxlayın və onları kvadrata yüksəldin
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
Tapşırıq 2: sətirlər siyahısından onların ilk hərflərinin siyahısını əldə etmək
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
Tapşırıq 3: uzunluğu 3-dən böyük olan sətirləri filtrləyin və onları Set-ə toplayın
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. filter, map, collect ilə işləyərkən tipik səhvlər
Səhv № 1: collect-i unutdunuz — nəticə yoxdur!
Stream API pəncərə kənarındakı pişik kimi tənbəldir: terminal əməliyyatını (məsələn, collect və ya forEach) çağırmayınca heç nə baş verməyəcək. Əgər yalnız users.stream().filter(...).map(...); yazsanız — bu, heç bir əməliyyat yerinə yetirməyəcək.
Səhv № 2: filter və map yerləri ilə səhv salınıb
Bəzən yeni başlayanlar əvvəlcə map, sonra filter edirlər. Məsələn, names.stream().map(String::length).filter(len -> len > 3) — bu, ədədlər verəcək, sətirlər yox. Əgər sizə uzunluğu 3-dən böyük olan sətirlər lazımdırsa, əvvəl filtrləyin, sonra çevirmə edin.
Səhv № 3: dəyişməzliyi unutmaq
Stream API əməliyyatları ilkin kolleksiyanı dəyişmir! Onlar yeni nəticə qaytarır. List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — bundan sonra names kolleksiyası əvvəlki kimi qalacaq.
Səhv № 4: xarici dəyişən siyahıdan istifadə cəhdi
Belə etmək lazım deyil:
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
Daha yaxşısı collect istifadə etməkdir — bu, daha təhlükəsiz və daha qısadır.
Səhv № 5: NullPointerException
Kolleksiyada null elementlər ola bilərsə, onda name.startsWith("A") çağırışı null üçün xəta verəcək. Mümkündürsə, null üçün filtr əlavə edin:
.filter(name -> name != null && name.startsWith("A"))
GO TO FULL VERSION