Menu
flag Ar iconالعربيةdown icon

تدفقات Java (Streams): شرح filter وmap وcollect وreduce

كيفية معالجة المجموعات باستخدام Stream API في Java - filter وmap وsorted وcollect وcount وreduce - ببناء سلاسل معالجة (pipelines) سهلة القراءة بدلاً من الحلقات اليدوية.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

سلاسل المعالجة بدلاً من الحلقات

الـ stream هو سلسلة معالجة (pipeline) لمعالجة تسلسل من القيم. تبدأ من مصدر - غالباً مجموعة - ثم تسلسل عمليات مثل "أبقِ هذه فقط"، و"حوّل كل عنصر"، و"رتّبها"، وتنهي بجمع النتيجة. وبدلاً من كتابة حلقة فيها قائمة مؤقتة وif بداخلها، تصف ما تريده وتترك للـ stream أمر المرور على العناصر.

والآن وقد أصبحت تتقن تعبيرات لامدا، يتّضح دور التدفقات: كل عملية تأخذ تعبير لامدا (أو مرجع دالة) يصف العمل المطلوب لعنصر واحد.

اقرأها من الأعلى إلى الأسفل: خذ الأسماء، أبقِ الطويلة منها، حوّلها إلى أحرف كبيرة، اجمعها في قائمة. قائمة names الأصلية لا تُعدَّل أبداً - فالـ stream يُنتج نتيجة جديدة ويترك المصدر كما هو.

تشريح الـ stream

كل سلسلة معالجة لها ثلاثة أجزاء:

  • مصدر - list.stream() أو Arrays.stream(array) أو Stream.of(a, b, c).
  • صفر أو أكثر من العمليات الوسيطة - filter وmap وsorted وdistinct وlimit. كل واحدة تُرجع stream آخر، فيمكنك تسلسلها.
  • عملية طرفية واحدة بالضبط - collect وcount وforEach وreduce وfindFirst. هذه هي التي تُطلق العمل وتُنتج نتيجة (أو أثراً جانبياً).

يهم هذا الفصل بسبب قاعدة واحدة تفاجئ الجميع في البداية.

العمليات الوسيطة كسولة

العمليات الوسيطة لا تفعل شيئاً بمفردها. إنها تسجّل فقط ما ينبغي أن يحدث. ولا تعمل سلسلة المعالجة إلا عندما تطلب عملية طرفية نتيجة.

شغّلها: تعبير لامدا الخاص بـ map لا يُنفَّذ أبداً. أضف عملية طرفية وستنبض السلسلة كلها بالحياة:

هذا الكسل ليس غرابة يجب محاربتها - بل يتيح للـ stream دمج العمليات وتخطّي العمل الذي لا يحتاجه. لكنه يعني أن سلسلة المعالجة من دون عملية طرفية لا تفعل شيئاً، وهذا خطأ شائع من نوع "لماذا لا يحدث أي شيء؟".

filter وmap: الإبقاء والتحويل

هاتان العمليتان تحملان معظم العبء. تأخذ filter كائن Predicate (تعبير لامدا يُرجع boolean) وتُبقي العناصر التي تجتاز الشرط. وتأخذ map كائن Function وتستبدل كل عنصر بنتيجة تطبيقها عليه.

لاحظ أن map يمكنها تغيير نوع العنصر: هنا يتحوّل stream من String إلى stream من Integer. وString::length هو مرجع دالة - اختصار لتعبير لامدا w -> w.length().

العمليات الطرفية تُنتج نتيجة

بعد أن تشكّل الـ stream، تحوّله عملية طرفية إلى شيء ملموس:

العمليات الطرفية الشائعة: collect (الجمع في قائمة/مجموعة/خريطة)، وcount، وforEach، وanyMatch / allMatch / noneMatch، وfindFirst، وmin / max، وreduce. بعد تنفيذ عملية طرفية، يصبح الـ stream مستهلَكاً - فلا يمكنك إعادة استخدامه. استدعِ list.stream() من جديد للحصول على سلسلة معالجة جديدة.

جمع النتائج

collect مع Collectors هو الأداة الأساسية لبناء النتيجة. والأكثر شيوعاً هو Collectors.toList():

يوفّر لك Collectors أيضاً toSet() وjoining(...) وgroupingBy(...) وcounting(). في Java 16 وما بعدها يمكنك استبدال collect(Collectors.toList()) بـ .toList() الأقصر (وهو يُرجع قائمة غير قابلة للتعديل):

List<String> result = names.stream().map(String::toUpperCase).toList();

sorted وdistinct وlimit

تعيد هذه العمليات الوسيطة تشكيل الـ stream قبل أن تجمعه:

تستخدم sorted() بلا وسيط الترتيب الطبيعي؛ ومرّر كائن Comparator (هنا تعبير لامدا) للحصول على ترتيب مخصص. وComparator.reverseOrder() هي الطريقة الأنظف للترتيب التنازلي.

reduce: طيّ الـ stream إلى قيمة واحدة

عندما تحتاج إلى دمج كل العناصر في نتيجة واحدة - مجموع، أو حاصل ضرب، أو أطول سلسلة نصية - فإن reduce هي الأداة العامة. تعطيها قيمة بداية ودالة تدمج قيمتين:

بالنسبة إلى المجاميع والمتوسطات البسيطة، تكون التدفقات المتخصصة أوضح: nums.stream().mapToInt(Integer::intValue).sum(). ولجأ إلى reduce حين لا يوجد مُجمِّع جاهز.

التدفقات لا تحل محل كل حلقة

تتألق التدفقات عندما تحوّل مجموعة إلى نتيجة. وهي ليست أسرع تلقائياً من الحلقة، وتصبح غير عملية حين تحتاج إلى تعديل حالة خارجية أو الخروج المبكر بطرق متشعّبة. وثمة قاعدة جيدة: إذا قُرئت سلسلة المعالجة كجملة واحدة واضحة، فاستخدم الـ stream؛ أما إذا كنت تلجأ إلى عدّاد مشترك أو إلى index، فحلقة for البسيطة أصدق وأنسب.

تذكّر أيضاً أن الـ stream يُستخدم مرة واحدة. هذا يرمي استثناءً:

Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count();   // IllegalStateException: stream has already been operated upon

التالي: Optional

بعض العمليات الطرفية في التدفقات - findFirst وmin وmax وreduce من دون قيمة محايدة - قد لا تجد شيئاً، لذا فهي لا تُرجع قيمة مجردة. إنها تُرجع كائن Optional، وهو حاوية Java لمعنى "ربما قيمة، وربما لا شيء"، الذي يمنحك أخيراً بديلاً نظيفاً عن إرجاع null. وذلك هو موضوع الصفحة التالية.

الأسئلة الشائعة

ما هو الـ stream في Java؟

الـ stream هو سلسلة معالجة (pipeline) لمعالجة تسلسل من العناصر - غالباً مصدرها مجموعة - عبر سلسلة من العمليات مثل filter وmap وsorted، تنتهي بعملية طرفية (terminal) مثل collect أو count. إنه لا يخزّن البيانات ولا يعدّل المصدر؛ بل يصف عملية حسابية. تُنشئ واحداً باستخدام list.stream()، وتقرأ السلسلة من الأعلى إلى الأسفل كأنها وصفة طبخ.

كيف تحوّل الـ stream مرة أخرى إلى قائمة في Java؟

أنهِ سلسلة المعالجة بمُجمِّع طرفي: list.stream().filter(...).collect(Collectors.toList()). في Java 16+ يمكنك استخدام .toList() الأقصر، وهو يُرجع قائمة غير قابلة للتعديل. من دون عملية طرفية لا يُنفَّذ أي شيء على الإطلاق، لأن العمليات الوسيطة مثل filter وmap كسولة (lazy).

متى ينبغي استخدام الـ stream بدلاً من حلقة for؟

لجأ إلى الـ stream عندما تحوّل مجموعة أو تُرشّحها للحصول على نتيجة - فتُقرأ سلسلة المعالجة كجملة واحدة واضحة ("خذ الأسماء، أبقِ الطويلة منها، حوّلها إلى أحرف كبيرة، اجمعها في قائمة"). والتزم بحلقة for بسيطة عندما تحتاج إلى تعديل حالة خارجية، أو الخروج المبكر بطرق معقّدة، أو حين يكون المنطق أبسط على هيئة خطوات إجرائية. التدفقات تتعلق بالوضوح، لا بالسرعة الخام.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن