1. Problem: nə üçün adi Stream həmişə səmərəli deyil
Java-da rəqəm kolleksiyaları ilə işləyərkən və adi stream-lərdən (Stream<Integer>, Stream<Double>) istifadə edərkən, pərdəarxasında primitiv dəyərlərin obyekt qabıqlarına (Integer, Double və s.) “qablaşdırılması” (boxing) və “açılması” (unboxing) baş verir. Bu rahatdır, amma həmişə səmərəli deyil:
- Boxing — primitivin (int) obyektə (Integer) çevrilməsidir.
- Unboxing — əks əməliyyat: obyektdən primitivə.
Problem:
Boxing/unboxing əlavə əməliyyatlardır və yaddaş sərf edir. Proqramın “isti” (tez‑tez çağırılan) hissələrində bu, xüsusilə böyük ədədlər massivlərini emal edərkən, performansın nəzərəçarpacaq dərəcədə enməsinə səbəb ola bilər.
Nümunə:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);
Burada hər bir ədəd — bu, Integer obyektidir, primitiv int deyil.
2. Primitiv stream-lər: IntStream, LongStream, DoubleStream
Artıq boxing/unboxing-dən qaçmaq üçün Java-da primitiv stream-lər var:
- IntStream — int üçün
- LongStream — long üçün
- DoubleStream — double üçün
Onlar yalnız primitivlərlə işləyir və əlavə obyekt‑qabıqlar yaratmır.
Primitiv stream necə yaradılır?
Massivdən:
int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);
range/rangeClosed ilə:
IntStream.range(0, 10) // 0..9
IntStream.rangeClosed(1, 5) // 1..5 daxil olmaqla
Təsadüfi ədədlərin generasiyası:
new Random().ints(5, 0, 100) // 0-dan 99-a qədər 5 təsadüfi int
Nümunə: massiv elementlərinin cəmlənməsi
int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15
3. Stream<T> ilə primitiv stream-lər arasında çevirmə
Bəzən sizdə adi stream olur, bəzən isə primitiv. Çevirmə üçün xüsusi metodlar istifadə olunur:
- mapToInt, mapToLong, mapToDouble — adi stream-i primitivə çevirir.
- boxed() — primitiv stream-i obyekt stream-inə geri çevirir.
Nümunə:
List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3
Əksinə:
IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();
Diqqət:
boxed() — bu, geri çevirmə əməliyyatıdır, yenidən obyekt‑qabıqlar (Integer, Double və s.) yaradır və əgər maksimal performans lazımdırsa — onu “isti” yerlərdə istifadə etməkdən qaçın.
4. Cəmləmə və xülasələr: sum, average, min, max, summaryStatistics
Primitiv stream-lərdə aqreqasiya üçün əlverişli metodlar var:
- sum() — bütün elementlərin cəmi
- average() — orta (qaytarır: OptionalDouble)
- min(), max() — minimum və maksimum (OptionalInt, OptionalLong, OptionalDouble)
- summaryStatistics() — tam statistika obyektini qaytarır (cəmi, orta, minimum, maksimum, say)
Nümunə:
int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMin()); // 1
System.out.println(stats.getMax()); // 5
System.out.println(stats.getCount()); // 5
Collectors ilə müqayisə:
Adi stream-lər üçün Collectors.summarizingInt istifadə etmək olar, lakin bu, çox vaxt primitiv stream metodlarından daha az səmərəlidir:
List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));
5. Autoboxing-dən qaçmaq: harada vacibdir və necə ölçmək olar
Harada bu kritikdir?
- Böyük həcmdə ədədləri emal edən dövrlərdə və stream-lərdə (məsələn, massivlərin emalı, statistika, riyaziyyat, verilənlərin pars edilməsi).
- “İsti” yerlərdə — tez‑tez çağırılan və performansa təsir edən kod.
Niyə vacibdir?
- Hər dəfə boxing baş verəndə yeni obyekt‑qabı (Integer, Double və s.) yaradılır.
- Bu, zibil toplayıcısına (GC) yükü artırır.
- Mikrobençmarlarda fərq bəzən dəfələrlə ola bilər!
Necə ölçmək olar?
Dəqiq müqayisə üçün JMH (Java Microbenchmark Harness) çərçivəsindən istifadə olunur. O, boxing ilə və onsuz kodun performansını düzgün müqayisə etməyə imkan verir.
Nümunə (psevdo‑kod):
@Benchmark
public int sumIntStream() {
return IntStream.range(0, 1_000_000).sum();
}
@Benchmark
public int sumStreamInteger() {
return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}
İkinci variant, Integer obyektlərinin daimi yaradılması səbəbindən daha yavaş işləyəcək.
Nəticə:
Əgər performans sizin üçün vacibdirsə — primitiv stream-lərdən istifadə edin!
6. Primitiv stream-lər həqiqətən nə vaxt kömək edir və nə vaxt mürəkkəbləşdirməyə dəyməz
Aşağıdakı hallarda primitiv stream-lərdən istifadə edin:
- Böyük ədəd massivləri/kolleksiyaları ilə işləyirsinizsə.
- Cəmi, ortanı, minimumu, maksimumu tez hesablamaq lazımdırsa.
- Hər millisekundun önəmli olduğu kod yazırsınızsa (məsələn, real vaxtda verilənlərin emalı).
Aşağıdakı hallarda isə özünüzü yormaya bilərsiniz:
- Kolleksiya kiçikdirsə (onlarla və ya daha az element).
- Tip çevirmələri səbəbindən kod həddən artıq mürəkkəbləşirsə.
- Performans kritik deyilsə (məsələn, istifadəçi daxilinin emalı).
Nümunə:
List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Olar, lakin adi reduce da uyğundur
Məsləhət: mikroskopik optimallaşdırma naminə kodu “cəngəlliyə” çevirməyin. Primitiv stream-ləri yalnız həqiqətən əsaslandırıldığı yerlərdə istifadə edin.
7. OptionalInt, OptionalDouble: nəticələrin təhlükəsiz əldə edilməsi
Primitiv stream-lərdə min(), max(), average() metodları sadəcə ədəd yox, “qabıq” — OptionalInt, OptionalDouble və s. qaytarır. Bu, boş stream-ləri təhlükəsiz idarə etmək üçün lazımdır (məsələn, massiv boşdursa).
Nümunə:
int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
System.out.println("Minimum: " + min.getAsInt());
} else {
System.out.println("Massiv boşdur!");
}
Adi Optional ilə müqayisə:
- OptionalInt — int üçün
- OptionalDouble — double üçün
- OptionalLong — long üçün
Niyə sadəcə 0 qaytarmayaq?
Çünki 0 etibarlı bir dəyər ola bilər, boş stream isə ayrıca vəziyyətdir.
average ilə nümunə:
double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // boşdursa — NaN qaytaracaq
8. Təcrübə: primitiv stream-lərin istifadəsinə nümunələr
Nümunə 1: 1‑dən 1000‑ə qədər ədədlərin kvadratlarının cəmi
int sum = IntStream.rangeClosed(1, 1000)
.map(x -> x * x)
.sum();
System.out.println(sum);
Nümunə 2: Cüt ədədlərin filtrasiyası və sayılması
int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
.filter(x -> x % 2 == 0)
.count();
System.out.println("Cüt ədədlərin sayı: " + count);
Nümunə 3: adi Stream<Integer> ilə müqayisə
List<Integer> list = IntStream.range(0, 1_000_000)
.boxed()
.collect(Collectors.toList());
long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");
t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");
Böyük həcmlərdə fərq nəzərə çarpacaq.
9. Primitiv stream-lərlə işləyərkən tipik səhvlər
Səhv №1: Tipləri çevrilərkən boxing-i unutmaq.
Əgər boxed() istifadə edirsinizsə, yadda saxlayın ki, bu, yenidən obyekt‑qabıqlar yaradır. Zərurət olmadan istifadə etməyin.
Səhv №2: Obyektlər üçün primitiv stream-lərdən istifadə.
IntStream yalnız int ilə işləyir, obyektlərlə yox. Obyektlərlə işləmək lazımdırsa — adi Stream<T> istifadə edin.
Səhv №3: OptionalInt/OptionalDouble-i nəzərə almamaq.
Əgər min(), max(), average() çağırırsınızsa — həmişə nəticənin mövcud olduğunu (isPresent()) yoxlayın, əks halda istisna alacaqsınız.
Səhv №4: Stream<T> ilə IntStream arasında həddən artıq mürəkkəb çevirmələr.
Əgər kod daimi mapToInt() → boxed() → mapToDouble() səbəbindən oxunmaz olur — bəlkə də məntiqi sadələşdirmək lazımdır.
Səhv №5: Kiçik kolleksiyalarda “möcüzəvi” sürətlənmə gözləmək.
Kiçik siyahılarda Stream ilə IntStream arasındakı fərq minimaldır. Mikroskopik qənaət naminə kodu mürəkkəbləşdirməyə dəyməz.
GO TO FULL VERSION