1. Xüsusi Collector-lar: nə vaxt və necə yazmalı
Java Stream API-də axını kolleksiyaya və ya aqreqata çevirmək üçün Collector interfeysindən istifadə olunur. Adətən siz Collectors sinfindəki hazır kollektorlardan (toList(), toMap(), groupingBy() və s.) istifadə edirsiniz, lakin bəzən xüsusi bir şey lazım olur — bu halda öz Collector-unuzu yaza bilərsiniz.
Collector — axının elementlərindən yekun nəticənin necə yığılacağını təsvir edən obyektidir. O, dörd (əslində beş) əsas komponenti müəyyənləşdirir:
- supplier — elementləri toplamaq üçün yeni konteyner yaradır (məsələn, yeni siyahı və ya xəritə).
- accumulator — növbəti elementi konteynerə əlavə edir.
- combiner — iki konteyneri birləşdirir (paralel stream-lər üçün vacibdir!).
- finisher — konteyneri yekun nəticəyə çevirir (məsələn, onu dəyişməz edir və ya başqa tipə çevirir).
- characteristics — kollektorun xüsusiyyətlərini təsvir edən flaqların toplusu (məsələn, paralelliyi dəstəkləyib-dəstəkləməməsi, nəticə tipini dəyişib-dəyişməməsi və s.).
İmza:
Collector<T, A, R>
- T — axın elementlərinin tipi,
- A — aralıq akkumulyatorun tipi,
- R — nəticənin tipi.
2. Nümunə: MultiMap (Map<K, List<V>>) üçün Collector
Tutaq ki, siz Pair<K, V> cütləri axınını Map<K, List<V>> (multi-xəritə) şəklində toplamaq istəyirsiniz; burada hər açara dəyərlər siyahısı uyğun gəlir.
Reallaşdırma nümunəsi:
public static <K, V> Collector<Pair<K, V>, ?, Map<K, List<V>>> toMultiMap() {
return Collector.of(
HashMap::new, // supplier
(map, pair) -> map.computeIfAbsent(pair.key(), k -> new ArrayList<>()).add(pair.value()), // accumulator
(map1, map2) -> { // combiner
map2.forEach((k, vList) -> map1.merge(k, vList, (l1, l2) -> { l1.addAll(l2); return l1; }));
return map1;
},
Function.identity(), // finisher
Collector.Characteristics.UNORDERED
);
}
İstifadə:
List<Pair<String, Integer>> pairs = List.of(
new Pair<>("a", 1), new Pair<>("b", 2), new Pair<>("a", 3)
);
Map<String, List<Integer>> multiMap = pairs.stream().collect(toMultiMap());
// multiMap: {a=[1, 3], b=[2]}
3. Nümunə: top-N elementlər üçün Collector
Tutaq ki, axını ən böyük N elementdən ibarət siyahıya toplamaq istəyirsiniz (məsələn, azalan sırada top-5).
Reallaşdırma:
public static <T> Collector<T, ?, List<T>> topN(int n, Comparator<? super T> comparator) {
return Collector.of(
() -> new PriorityQueue<>(n, comparator), // supplier
(pq, t) -> {
pq.offer(t);
if (pq.size() > n) pq.poll(); // ən kiçiyini çıxarırıq
},
(pq1, pq2) -> {
pq2.forEach(t -> {
pq1.offer(t);
if (pq1.size() > n) pq1.poll();
});
return pq1;
},
pq -> {
List<T> result = new ArrayList<>(pq);
result.sort(comparator.reversed()); // azalan sırada
return result;
},
Collector.Characteristics.UNORDERED
);
}
İstifadə:
List<Integer> top3 = Stream.of(5, 1, 9, 3, 7, 2).collect(topN(3, Comparator.naturalOrder()));
// top3: [9, 7, 5]
4. Nə vaxt öz Collector-unuzu yazmağa dəyməz
- Əgər tapşırığı standart kollektorların və downstream əməliyyatlarının (groupingBy, mapping, flatMapping, collectingAndThen və s.) kombinasiyası ilə ifadə etmək mümkündürsə, onları istifadə etmək daha yaxşıdır.
- Öz Collector yalnız həqiqətən qeyri-standart ssenarilər üçün lazımdır (xüsusi verilənlər strukturu, mürəkkəb aqreqasiya, top-N, multi-xəritələr və s.).
- Collector-u sadəcə Collector yazmaq xatirinə yazmağa dəyməz — bu, dəstək və testləşdirməni çətinləşdirir.
Nümunə:
// Map<K, Set<V>> üçün öz Collector-unuz əvəzinə:
.collect(Collectors.groupingBy(
Pair::key,
Collectors.mapping(Pair::value, Collectors.toSet())
))
5. Xüsusi Spliterator: nə üçün və necə
Spliterator — kolleksiyaları (və ya digər verilənlər mənbələrini) səmərəli şəkildə keçmək və hissələrə bölmək üçün xüsusi interfeysdir; xüsusilə paralel emal üçün uyğundur. Adi iteratordan fərqli olaraq, Spliterator kolleksiyanı paralel emal üçün müstəqil hissələrə “bölə” (split) bilər.
Açar metodlar:
- tryAdvance(Consumer<? super T> action) — növbəti elementi emal etmək.
- trySplit() — kolleksiyanı iki hissəyə bölməyə cəhd edir (hissələrdən biri üçün yeni Spliterator qaytarır).
- estimateSize() — qalan elementlərin sayının təxmini.
- characteristics() — xüsusiyyətlərin bit maskası (ORDERED, SIZED, SUBSIZED və s.).
trySplit: bölmə strategiyaları
Tarazlaşdırılmış bölmə — paralel stream-lər üçün vacibdir: trySplit təxmini eyni ölçülü hissələr qaytarmalıdır ki, iş yükü bərabər paylansın.
Bölmək mümkün deyilsə (məsələn, elementlər azdırsa) — null qaytarırıq.
Nümunə: faylı hissə-hissə oxumaq üçün Spliterator
Tutaq ki, böyük bir faylınız var və onu hər dəfə 1000 sətir olmaqla (hissələrlə) emal etmək istəyirsiniz ki, hamısını yaddaşda saxlamayasınız.
public class ChunkedLineSpliterator implements Spliterator<List<String>> {
private final BufferedReader reader;
private final int chunkSize;
public ChunkedLineSpliterator(BufferedReader reader, int chunkSize) {
this.reader = reader;
this.chunkSize = chunkSize;
}
@Override
public boolean tryAdvance(Consumer<? super List<String>> action) {
List<String> chunk = new ArrayList<>(chunkSize);
try {
String line;
for (int i = 0; i < chunkSize && (line = reader.readLine()) != null; i++) {
chunk.add(line);
}
if (chunk.isEmpty()) return false;
action.accept(chunk);
return true;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public Spliterator<List<String>> trySplit() {
// Fayldan axınla oxumaqda bölmənin mənası yoxdur — null qaytarırıq
return null;
}
@Override
public long estimateSize() {
return Long.MAX_VALUE; // əvvəlcədən məlum deyil
}
@Override
public int characteristics() {
return ORDERED | NONNULL;
}
}
İstifadə:
try (BufferedReader reader = Files.newBufferedReader(Path.of("big.txt"))) {
StreamSupport.stream(new ChunkedLineSpliterator(reader, 1000), false)
.forEach(chunk -> processChunk(chunk));
}
Spliterator-un xüsusiyyətləri
- ORDERED — elementlər müəyyən ardıcıllıqla gəlir (məsələn, siyahı).
- SIZED — elementlərin dəqiq sayı məlumdur.
- SUBSIZED — trySplit vasitəsilə alınan bütün Spliterator-lar da SIZED-dir.
- IMMUTABLE — mənbə keçid zamanı dəyişmir.
- CONCURRENT — mənbə təhlükəsiz paralel modifikasiyanı dəstəkləyir.
- DISTINCT, SORTED, NONNULL — əlavə xüsusiyyətlər.
Vacibdir: xüsusiyyətləri düzgün göstərmək lazımdır — bu, stream-lərin optimizasiyasına təsir edir.
6. Nümunələr
- Faylı hissələrlə (chunk-larla) oxuma — böyük faylları hissə-hissə emal etməyə imkan verir, hamısını yaddaşa yükləmədən.
- Artıq allokasiyalar olmadan parsinq — əgər siz bayt/simvol axınını parsinq edir və müvəqqəti obyektlərin yaradılmasını minimuma endirmək istəyirsinizsə, başlanğıc massivdən “pəncərə” və ya “dilimlər” verən Spliterator reallaşdıra bilərsiniz.
Nümunə: CSV-i sətir-sətir parsinq üçün Spliterator
public class CsvLineSpliterator implements Spliterator<String[]> {
private final BufferedReader reader;
public CsvLineSpliterator(BufferedReader reader) {
this.reader = reader;
}
@Override
public boolean tryAdvance(Consumer<? super String[]> action) {
try {
String line = reader.readLine();
if (line == null) return false;
action.accept(line.split(","));
return true;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public Spliterator<String[]> trySplit() {
return null; // ardıcıl parsinq
}
@Override
public long estimateSize() {
return Long.MAX_VALUE;
}
@Override
public int characteristics() {
return ORDERED | NONNULL;
}
}
7. parallel() ilə inteqrasiya — bunu necə təhlükəsiz etmək olar
- Əgər Spliterator-unuz paralel bölməni dəstəkləyirsə (trySplit null qaytarmır) və xüsusiyyətlərdə SIZED/SUBSIZED varsa, onda Stream API emalı effektiv şəkildə paralelləşdirə biləcək.
- Axın tipli mənbələr (fayllar, soketlər) üçün adətən bölmə dəstəklənmir — ardıcıl stream-lərdən istifadə edin.
- Kolleksiyalar və massivlər üçün — tarazlaşdırılmış bölmə reallaşdırın (məsələn, massivi iki yerə bölün).
Nümunə: massiv üçün Spliterator
public class ArraySpliterator<T> implements Spliterator<T> {
private final T[] array;
private int start, end;
public ArraySpliterator(T[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
public boolean tryAdvance(Consumer<? super T> action) {
if (start < end) {
action.accept(array[start++]);
return true;
}
return false;
}
@Override
public Spliterator<T> trySplit() {
int mid = (start + end) >>> 1;
if (mid == start) return null;
ArraySpliterator<T> split = new ArraySpliterator<>(array, start, mid);
start = mid;
return split;
}
@Override
public long estimateSize() {
return end - start;
}
@Override
public int characteristics() {
return ORDERED | SIZED | SUBSIZED | IMMUTABLE;
}
}
İstifadə:
String[] arr = {"a", "b", "c", "d"};
StreamSupport.stream(new ArraySpliterator<>(arr, 0, arr.length), true)
.forEach(System.out::println);
GO TO FULL VERSION