Problem: heç kimin toxunmaq istəmədiyi pipeline
Hesabat metodu hər müştəri üzrə ödənilmiş sifarişləri toplayır. Beş sətirlik stream kodu kimi başlayıb. Üç dəyişiklik sorğusundan sonra içində iç-içə flatMap, repository çağıran filter, say sahəsini yeniləyən map və debugger olmadan heç kimin oxuya bilmədiyi collect var. Yeni developer onu dövr kimi yenidən yazır və səhv tapır: emaili olmayan müştərilərin sifarişləri səssizcə buraxılıb, çünki map null qaytarıb, sonrakı filter isə onu atıb.
Stream-lər ardıcıl datadan nə istədiyinizi təsvir etmək üçün yaxşı alətdir. Necə baş verdiyini gizlədəndə, xüsusilə yan təsirlər və sıra olanda, öhdəliyə çevrilir. Bu yazı pipeline-ı oxunaqlı saxlamağı və tanımağa dəyər səhvləri göstərir.
Yaxşı pipeline-ın forması
Stream-in mənbəyi, sıfır və ya daha çox aralıq əməliyyatı (filter, map, sorted, distinct, limit) və bir terminal əməliyyatı (collect, forEach, reduce, count, findFirst) var. Aralıq əməliyyatlar tənbəldir (lazy): terminal əməliyyat çağırılana qədər heç nə işləmir və mümkün olduqda elementlər bütün pipeline-dan bir-bir axır.
List<String> activeEmails = customers.stream() .filter(Customer::active) .map(Customer::email) .filter(Objects::nonNull) .distinct() .sorted() .toList();Yuxarıdan aşağıya oxumaq hekayəni danışır. Bunu belə saxlayan vərdişlər:
- Hər sətirdə bir əməliyyat, aydın olan yerdə method reference, əks halda qısa lambda.
- Mürəkkəb lambdanı adlı metoda çıxarın:
.filter(this::isEligibleForDiscount)beş sətirlik şərtdən çox şey izah edir. - Pipeline-ları qısa saxlayın. Nə baş verdiyini izah etmək üçün aralıq dəyişən lazımdırsa, pipeline-ı iki ifadəyə bölün.
- Rəqəmlər üçün primitiv stream-lərdən (
IntStream,LongStream,mapToInt) istifadə edin ki, boxing olmasın. toList()(Java 16+) dəyişdirilə bilməyən siyahı qaytarır;Collectors.toList()dəyişən siyahı qaytarır, amma dəqiq tipə söz vermir.
map və forEach-də yan təsirlər
Qayda: aralıq əməliyyatlar təmiz olmalıdır, yəni nəticələri yalnız girişdən asılı olmalı və paylaşılan vəziyyəti dəyişməməlidir. Müqayisə edin:
// Yanlış: pipeline daxilindən kənar siyahını dəyişməkList<String> names = new ArrayList<>();customers.stream() .filter(Customer::active) .forEach(c -> names.add(c.name())); // ardıcıl işləyir, paralel işləmir// Daha yaxşı: nəticəni stream özü yaratsınList<String> names = customers.stream() .filter(Customer::active) .map(Customer::name) .toList();Birinci variant işləyirmiş kimi görünür, amma onu anlamaq çətindir və kimsə .parallel() əlavə edən kimi təhlükəlidir: ArrayList thread-safe deyil. İkincisində paylaşılan vəziyyət ümumiyyətlə yoxdur.
Digər yan təsir tələləri:
map-i yalnız yan təsiri üçün (loglama və ya saxlama kimi) işlədib nəticəni görməzdən gəlmək.forEach, daha yaxşısı adi dövr işlədin.- Biznes məntiqi üçün
peek. Sənəd onu debug köməkçisi kimi təsvir edir və terminal əməliyyat short-circuit edə və ya elementləri keçə bilirsə, ümumiyyətlə işləməyə bilər. map-də uzaq servis və ya repository çağırmaq. Hər element görünməz yerdə I/O yaradır, bu da N+1 nümunəsini gizlədir.
Collector-lar: groupingBy və toMap
Collectors.groupingBy siyahılardan ibarət map və ya verdiyiniz downstream collector-un nəticəsini qurur.
Map<String, BigDecimal> totalByCustomer = orders.stream() .filter(o -> o.status() == Status.PAID) .collect(Collectors.groupingBy( Order::customerId, Collectors.reducing(BigDecimal.ZERO, Order::amount, BigDecimal::add)));Sürprizlər Collectors.toMap-dadır. İki element eyni açar yaradırsa, merge funksiyası olmayan toMap "Duplicate key" mesajı ilə IllegalStateException atır. Bu çox vaxt yaxşıdır, çünki yanlış fərziyyələri üzə çıxarır, amma data unikal sayılıbsa, real trafiki poza bilər.
// Müştəri iki dəfə görünəndə daha yeni ünvanı saxlaMap<String, Address> latest = addresses.stream() .collect(Collectors.toMap( Address::customerId, a -> a, (older, newer) -> newer.updatedAt().isAfter(older.updatedAt()) ? newer : older));Yadda saxlamağa dəyər digər faktlar: toMap null dəyərlərə icazə vermir (NullPointerException atır); dördüncü arqument kimi map factory verməsəniz, nəticə HashMap-dır; groupingBy də map factory qəbul edir, məsələn açarların sırası vacibdirsə TreeMap::new. İki qrupa bölmək üçün partitioningBy true və false açarlı map qaytarır. Açar bərabərliyi equals və hashCode-dan asılıdır, nəticə isə sadəcə map-dır, ona görə tipini kolleksiya bələdçisini nəzərə alaraq seçin.
Dövr nə vaxt daha aydındır
Stream-lər avtomatik olaraq daha yaxşı deyil. Adi dövr çox vaxt belə hallarda daha aydındır:
- Bir neçə şərtə görə erkən çıxmaq və ya lokal dəyişənləri dəyişmək lazımdır.
- Məntiqin ortaq aralıq dəyərləri olan bir neçə addımı var.
- Hər element üçün checked exception idarə etməlisiniz, çünki lambdalar onları sərbəst ata bilmir. Kompilyatoru razı salmaq üçün lazım olan sarğı oxunaqlılıq arqumentini puç edir; exception dizaynına baxın.
- Debugger sessiyası vacibdir. Uzun pipeline-da addım-addım getmək dövrdən çətindir.
// Dövr "ilk etibarsız sətirdə dayan" fikrini təbii ifadə edirfor (OrderLine line : lines) { if (!line.isValid()) { return Result.invalid(line.number()); } total = total.add(line.amount());}Stream də, dövr də yaxşıdır. Moda ilə yox, növbəti oxuyan şəxs üçün oxunaqlılığa görə seçin.
Paralel stream-lər: ehtiyatla
.parallel() çağırmaq pipeline-ı bütün JVM prosesi üçün paylaşılan ümumi ForkJoinPool-da işlədir. Bu bir neçə problem gətirir:
- Paralellik yalnız böyük data üzərində CPU-ya bağlı işdə kömək edir, orada bölmə və birləşdirmə xərci faydadan azdır. Kiçik kolleksiyalarda işi yavaşladır.
- Paralel stream daxilində bloklayan iş (HTTP çağırışı, verilənlər bazası sorğusu) paylaşılan pool thread-lərini tutur və tətbiqin əlaqəsiz hissələrini ac qoya bilər.
- Əməliyyatlar stateless, müdaxiləsiz və tələb olunan yerdə assosiativ olmalıdır; əks halda nəticələr proqnozlaşdırılmaz olur.
findFirst,limitvəforEachOrderedkimi sıraya həssas əməliyyatlar faydanın çoxunu itirir.- Mənbə vacibdir:
ArrayListvə massivlər yaxşı bölünür,LinkedListvəStream.iterateyox.
Təcrübə qaydası: real yüklə ölçmədən .parallel() əlavə etməyin, iş bloklayırsa isə ümumiyyətlə əlavə etməyin. I/O üçün paralellik lazımdırsa, concurrency əsaslarında təsvir olunan açıq executor işlədin.
Pipeline-ların test və debug edilməsi
Aralıq addımlar görünmədiyi üçün uğursuz pipeline-ı yoxlamaq dövrdən çətindir. İki vərdiş kömək edir. Birincisi, maraqlı lambdaları kiçik adlı metodlara çıxarın və onları birbaşa unit-test edin; pipeline-ın özü onları sadəcə birləşdirir. İkincisi, içindən nəyin axdığını görmək lazım olanda pipeline-ı müvəqqəti bölün, aralıq nəticəni siyahıya yığın, debugger-də baxın və sonra zənciri bərpa edin. Araşdırma zamanı sürətli log sətri üçün peek işlədin və sonra silin.
Həmçinin pipeline-ların səssiz keçdiyi kənar halları test edin: boş giriş, tək element, toMap üçün təkrar açarlar və null sahələri olan elementlər.
Tez-tez edilən səhvlər
- Stream-i təkrar işlətmək: stream yalnız bir dəfə istehlak olunur, ikinci terminal əməliyyat
IllegalStateExceptionatır. - Terminal əməliyyatı unutmaq; heç nə işləmir.
- Yoxlamadan
Optional.get()işlətmək;orElse,orElseGetvə yaorElseThrowdaha yaxşıdır. IntStream.rangekifayət edən yerdə isti dövrlərdə boxing.- Yalnız ilk elementi götürmək üçün böyük stream-i sort etmək; comparator ilə
minkifayətdir. - Mapper funksiyadan
nullqaytarıb pipeline-ın öhdəsindən gələcəyinə ümid etmək.
Güzəştlər
Stream-lər deklarativ, birləşdirilə bilən kod verir, bu da çevirmə və aqreqasiyalarda özünü doğruldur. Məntiq vəziyyətli (stateful) olanda oxunaqlılığa bir qədər zərər verir və nəsə uğursuz olanda sizinlə stack trace arasında əlavə qat yaradır. Paralel stream-lər dar bir məsələ sinfində potensial sürətlənmə üçün proqnozlaşdırıla bilənliyi qurban verir. Ağlabatan default: çevirmələr üçün ardıcıl stream-lər, idarəetmə axını üçün dövrlər, paralellik üçün açıq executor-lar.
Yoxlama siyahısı
- Hər lambda qısadır və ya adlı metoda çıxarılıb.
- Pipeline daxilindən kənar vəziyyət dəyişdirilmir.
- Təkrar mümkün olan hər yerdə
toMap-də merge funksiyası var. - Stream əməliyyatları daxilində bloklayan çağırış yoxdur.
.parallel()yalnız ölçmədən sonra və heç vaxt bloklayan işlə.- Pipeline-ı anlamaq üçün şərh lazımdırsa, dövrü düşünün.
Növbəti addım
Paralel stream xəbərdarlıqlarının arxasındakı thread-təhlükəsizlik qaydalarını görmək üçün concurrency əsaslarını oxuyun və ya cari səviyyənizi bacarıq qiymətləndirməsi ilə yoxlayın. Middle Java developer yol xəritəsi stream-ləri digər gündəlik alətlərin yanında yerləşdirir.