واجهة التدفّقات

sorted و distinct و limit و skip

15 دقيقة الدرس 6 من 13

sorted و distinct و limit و skip

رأيت سابقًا العمليات الوسيطة عديمة الحالة — filter وmap تعالج كل عنصر باستقلالية تامة دون الاطلاع على العناصر الأخرى. يركّز هذا الدرس على العمليات الوسيطة الأربع ذات الحالة ويُعرّفك بمفهوم الدوائر القصيرة (short-circuiting)، وكلاهما مهم للأداء والصحة.

عمليات ذات حالة مقابل عمليات عديمة الحالة

العملية عديمة الحالة لا تحتاج سوى العنصر الحالي. أما العملية ذات الحالة فتضطر إلى تجميع عدة عناصر أو فحصها قبل أن تُنتج أي مخرج. تمنحك واجهة Streams API أربع عمليات وسيطة ذات حالة:

  • sorted() — ترتيب جميع العناصر
  • distinct() — إزالة المكررات
  • limit(n) — الاحتفاظ بأول n عناصر فقط
  • skip(n) — تجاهل أول n عناصر
لماذا تهمّ الحالة؟ عند تشغيل stream بشكل متوازٍ، تُجبر العمليات ذات الحالة الخيوط على المزامنة فيما بينها — وهي أكثر تكلفةً بكثير من العمليات عديمة الحالة. فضّل العمليات عديمة الحالة في المسارات المتوازية الساخنة، وضع limit وskip في أبكر وقت ممكن حتى يصل عدد أقل من العناصر إلى العمليات الثقيلة لاحقًا.

sorted

sorted() بدون معاملات يرتّب بالترتيب الطبيعي (يجب أن تُنفّذ العناصر Comparable). مرّر Comparator للترتيب بأي مفتاح:

import java.util.List; import java.util.Comparator; List<String> words = List.of("banana", "apple", "cherry", "apricot"); // الترتيب الأبجدي الطبيعي words.stream() .sorted() .forEach(System.out::println); // apple, apricot, banana, cherry // ترتيب تنازلي بالطول ثم أبجدياً عند التساوي words.stream() .sorted(Comparator.comparingInt(String::length) .reversed() .thenComparing(Comparator.naturalOrder())) .forEach(System.out::println); // apricot, banana, cherry, apple
sorted() يخزّن الstream بأكمله في الذاكرة. لا يستطيع إصدار عنصر واحد حتى يرى جميع العناصر — لذا على stream لا نهائي ستدور إلى الأبد (أو حتى نفاد الذاكرة). اقرن دائمًا المصادر اللانهائية بـ limit قبل sorted.

distinct

distinct() يُزيل العناصر المكررة باستخدام equals وhashCode، وهو يحافظ على الترتيب في streams التسلسلية (يفوز أول ظهور):

import java.util.List; List<Integer> numbers = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5, 3); numbers.stream() .distinct() .forEach(System.out::println); // 3, 1, 4, 5, 9, 2, 6 (أول ظهور لكل قيمة)

يكون ذلك مفيدًا بشكل خاص عند تسطيح مجموعات تظهر فيها القيمة ذاتها في قوائم فرعية متعددة.

distinct() يعتمد على equals/hashCode. عند استخدامه على stream من كائنات مخصصة، تأكد من وجود تطبيق صحيح لـ equals وhashCode، وإلا سيمرّ كائنان متطابقان منطقيًا كلاهما.

limit و skip — عمليات الدائرة القصيرة

limit(n) يحتفظ بأول n عناصر على الأكثر ثم يوقف المسار. skip(n) يتجاهل أول n عناصر ويمرّر الباقي. معًا يُمكّنان من ترقيم صفحات stream:

import java.util.List; import java.util.stream.Collectors; List<String> items = List.of("a","b","c","d","e","f","g","h"); int page = 1; // رقم الصفحة (يبدأ من صفر) int pageSize = 3; List<String> page1 = items.stream() .skip((long) page * pageSize) // تجاوز أول 3 .limit(pageSize) // احتفظ بالـ 3 التالية .collect(Collectors.toList()); System.out.println(page1); // [d, e, f]

مصطلح الدائرة القصيرة يعني أن المسار لا يحتاج إلى معالجة كل عناصر المصدر. حالما يُصدر limit حصته يُشير للمصدر بالتوقف. هذا نفس مفهوم && الذي يتوقف مبكرًا في التعبيرات المنطقية:

import java.util.stream.Stream; // بدون limit ستعمل إلى ما لا نهاية. // مع limit(5) يتوقف الstream بعد إنتاج 5 عناصر. Stream.iterate(0, n -> n + 1) // لانهائي: 0، 1، 2، 3 ... .limit(5) .forEach(System.out::println); // 0, 1, 2, 3, 4
توجد أيضًا عمليات نهائية بدائرة قصيرة. findFirst() وfindAny() وanyMatch() وnoneMatch() وallMatch() جميعها تستطيع إيقاف المسار مبكرًا. ستراها في درس لاحق عن Optional مع Streams.

الجمع بين العمليات الأربع في مسار حقيقي

سيناريو واقعي: من قائمة سطور سجل (logs)، ابحث عن أفضل 5 رسائل خطأ فريدة مرتبة أبجديًا، مع تخطي الأولى (لغرض الترقيم):

import java.util.List; import java.util.stream.Collectors; List<String> logs = List.of( "ERROR: null pointer", "INFO: started", "ERROR: timeout", "ERROR: null pointer", // مكرر "ERROR: disk full", "ERROR: timeout", // مكرر "ERROR: out of memory", "ERROR: connection refused" ); List<String> result = logs.stream() .filter(line -> line.startsWith("ERROR")) // عديمة الحالة .map(line -> line.substring(7)) // عديمة الحالة: حذف "ERROR: " .distinct() // ذات حالة: إزالة المكررات .sorted() // ذات حالة: ترتيب أبجدي .skip(1) // دائرة قصيرة: تجاهل الأولى .limit(5) // دائرة قصيرة: احتفظ بأقصاه 5 .collect(Collectors.toList()); System.out.println(result); // [null pointer, out of memory, timeout] (connection refused تم تجاوزها بـ skip(1))

نصائح للأداء

  • ضع filter قبل sorted وdistinct لتقليل عدد العناصر التي تحتاج العمليات ذات الحالة إلى تخزينها.
  • ضع limit في أبكر وقت يسمح به المنطق — كل عنصر يُقطع قبل عملية ثقيلة يوفّر عملًا.
  • تجنّب sorted على streams متوازية كبيرة ما لم يكن ضروريًا حقًا؛ إذ يُدخل خطوة دمج قد تُلغي فائدة التوازي.

الخلاصة

sorted وdistinct ذاتا حالة — يجب أن يريا جميع العناصر (أو كثيرًا منها) قبل إنتاج المخرج. limit وskip ذواتا دائرة قصيرة — تتوقف أو تتخطى مبكرًا مما يجعل streams اللانهائية عملية. وضع العمليات الثقيلة ذات الحالة في النهاية والعمليات ذات الدائرة القصيرة في البداية قاعدة بسيطة تُبقي المسارات فعّالة.