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

الاختزال والتجميع

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

الاختزال والتجميع

أظهرت الدروس السابقة كيفية تصفية عناصر المجرى وتحويلها. تلك العمليات تنتج مجرىً جديدًا — لكنك في نهاية المطاف تحتاج إلى إجابة ملموسة: رقم واحد، أو قائمة، أو خريطة. هذا ما تفعله العمليات الختامية. يغطي هذا الدرس أهم ثلاث منها: reduce وcount وتجميع النتائج بـ Collectors.toList() (وبديلها الحديث).

لماذا تهمّنا العمليات الختامية؟

المجرى كسول بطبعه. الخطوات الوسيطة (filter وmap وغيرها) لا تُنفَّذ حتى تُستدعى عملية ختامية. reduce وcollect هما أقوى العمليات الختامية: الأولى تطوي المجرى في قيمة واحدة، والثانية تسكب العناصر في حاوية قابلة للتعديل كـList أو Map.

count — أبسط عملية ختامية

count() ترجع long — عدد العناصر التي نجت من خط الأنابيب.

import java.util.List; List<String> names = List.of("Alice", "Bob", "Charlie", "Dave", "Eve"); long shortNames = names.stream() .filter(n -> n.length() <= 3) .count(); // 2 (Bob, Eve) System.out.println(shortNames); // 2

count() مكافئة مفاهيميًا لـ reduce(0L, (acc, e) -> acc + 1) — وهذا يقودنا إلى العملية الأعم.

reduce — طيّ المجرى في قيمة واحدة

reduce تطبّق مؤثّرًا ثنائيًا بشكل متكرر حتى ينضب المجرى. فكّر فيها كطيّ على قائمة: تبدأ بقيمة هوية وتدمجها مع كل عنصر واحدًا تلو الآخر.

الشكل الأكثر شيوعًا يأخذ قيمة هوية وBinaryOperator<T>:

// جمع كل الأعداد الصحيحة List<Integer> numbers = List.of(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, Integer::sum); // هوية=0، ثم 0+1+2+3+4+5 = 15 System.out.println(sum); // 15

قيمة الهوية يجب أن تكون هوية حقيقية للعملية — إضافة الصفر لا تغير النتيجة أبدًا، لذا 0 هو هوية الجمع. وللضرب تكون الهوية 1.

يمكنك كتابة التعبير اللامبدي صراحةً لتوضيح الآلية:

int product = numbers.stream() .reduce(1, (accumulator, element) -> accumulator * element); System.out.println(product); // 120

reduce بدون هوية — Optional

حين لا تُعطى قيمة هوية، يرجع reduce Optional<T> لأن المجرى قد يكون فارغًا ولن تكون هناك نتيجة ذات معنى ترجعها.

import java.util.Optional; Optional<Integer> max = numbers.stream() .reduce((a, b) -> a > b ? a : b); max.ifPresent(v -> System.out.println("Max: " + v)); // Max: 5
متى تستخدم الشكلين: استخدم الشكل ذا الوسيطتين (مع الهوية) حين تريد دائمًا نتيجة ملموسة حتى على مجرى فارغ. استخدم الشكل ذا الوسيطة الواحدة حين يكون المجرى الفارغ احتمالًا حقيقيًا تريد التعامل معه صراحةً عبر Optional.

تجميع النتائج بـ Collectors.toList()

reduce رائعة لحساب قيمة عددية واحدة. لكن كثيرًا ما تريد مجموعة جديدة. هذا ما تفعله collect — تجمع عناصر المجرى في حاوية قابلة للتعديل.

أكثر مُجمِّع شيوعًا هو الذي ينتج List:

import java.util.List; import java.util.stream.Collectors; List<String> filtered = names.stream() .filter(n -> n.startsWith("A") || n.startsWith("E")) .collect(Collectors.toList()); System.out.println(filtered); // [Alice, Eve]

منذ Java 16 يوجد بديل أكثر إيجازًا: Stream.toList(). يرجع قائمة غير قابلة للتعديل، وهو ما تريده في الغالب:

// Java 16+ — مفضّل في الكود الحديث List<String> filtered = names.stream() .filter(n -> n.startsWith("A") || n.startsWith("E")) .toList(); // قائمة غير قابلة للتعديل
فضّل .toList() على Collectors.toList() في كود Java 16+ . الشكل المختصر أنظف ويشير إلى الثبات من الوهلة الأولى. استخدم Collectors.toList() حين تحتاج التوافق مع Java 11 أو Java 8.

التجميع في حاويات أخرى

تقدّم فئة Collectors مُجمِّعات كثيرة — ستستكشفها بعمق في الدرس السابع. في الوقت الحالي أكثر مُجمِّعَين فائدةً بعد toList() هما:

  • Collectors.toSet() — ينتج Set مُزيلًا المكرّرات تلقائيًا.
  • Collectors.joining(delimiter) — يسلسل عناصر مجرى String في سلسلة نصية واحدة.
import java.util.Set; import java.util.stream.Collectors; // إزالة المكرّرات List<Integer> dupes = List.of(1, 2, 2, 3, 3, 3); Set<Integer> unique = dupes.stream().collect(Collectors.toSet()); System.out.println(unique); // [1, 2, 3] (الترتيب غير مضمون) // ربط السلاسل النصية String csv = names.stream() .collect(Collectors.joining(", ")); System.out.println(csv); // Alice, Bob, Charlie, Dave, Eve

الجمع بين reduce وmap — مثال عملي

نمط عملي شائع هو تحويل الكائنات إلى خاصية عددية ثم الاختزال للحصول على إحصائية ملخّصة:

record Product(String name, double price) {} List<Product> cart = List.of( new Product("Keyboard", 79.99), new Product("Mouse", 29.99), new Product("Monitor", 349.99) ); double total = cart.stream() .mapToDouble(Product::price) .sum(); // 459.97 System.out.println("Total: $" + total);
لا تستخدم reduce للآثار الجانبية. التعبير اللامبدي الذي تمرّره إلى reduce يجب أن يكون عديم الحالة وغير متداخل وترابطيًا (حتى تعطي المجاري المتوازية النفس النتيجة). تعديل الحالة الخارجية داخل اللامبدا خطأ شائع يكسر المجاري المتوازية بصمت.

الخلاصة

count() أبسط طريقة لعدّ العناصر بعد التصفية. reduce تسمح لك بطيّ أي مجرى في قيمة واحدة — استخدم شكل الهوية حين يجب أن يُرجع المجرى الفارغ نتيجة محايدة، وشكل Optional حين يكون المجرى الفارغ احتمالًا حقيقيًا. collect(Collectors.toList()) — أو .toList() الحديثة — تسكب المجرى في مجموعة ملموسة. هذه العمليات الثلاث تغطّي الغالبية العظمى من احتياجات المجاري الختامية؛ الدروس التالية ستبني عليها.