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

مقدّمة إلى Streams

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

مقدّمة إلى Streams

واجهة Streams API التي أُضيفت في Java 8 وتطوّرت منذ ذلك الحين تُعدّ من أبرز الإضافات إلى اللغة. تُتيح لك التعبير عن منطق معالجة البيانات كخط أنابيب تصريحي — تُخبر Java بـماذا تحسب لا بـكيفية التكرار. قبل المضي قُدُمًا، من الضروري تحديد معنى الـstream بدقة لأن الكلمة تُستخدم في سياقات متعددة في عالم الحوسبة.

ما هو الـStream؟

الـStream<T> في Java هو تسلسل من العناصر يدعم العمليات التجميعية المتسلسلة والمتوازية. ثلاثة أمور تميّزه عن المصفوفة أو القائمة العادية:

  • لا تخزين. الـstream لا يحتفظ بالبيانات. يقرأ من مصدر (مجموعة، مصفوفة، ملف، أو مولّد) ويمرّر العناصر عبر خط الأنابيب عند الحاجة.
  • طابع وظيفي. العمليات تنتج streams جديدة أو نتيجة نهائية؛ ولا تُعدّل المصدر أبدًا.
  • تقييم كسول. العمليات الوسيطة لا تُنفَّذ حتى تطلب عملية نهائية النتيجة.
Streams مقابل I/O Streams. java.util.stream.Stream لا علاقة له بـjava.io.InputStream أو java.io.OutputStream. كلاهما يُسمّى "stream" لكنهما يحلّان مشكلتين مختلفتين. هذا الدرس يتناول java.util.stream فقط.

Streams مقابل Collections — الفرق الجوهري

المجموعة (مثل ArrayList أو HashSet) هي بنية بيانات: تخزّن العناصر في الذاكرة، وتتيح إضافتها وحذفها، والتكرار عليها بأي ترتيب. أما الـstream فهو طريقة عرض لمعالجة تلك البيانات؛ يُستهلك مرة واحدة ثم يُهمَل.

لنتأمّل مثالًا ملموسًا: لديك قائمة بأسماء المنتجات وتريد الأسماء التي تبدأ بـ"A" محوّلة إلى أحرف كبيرة.

أسلوب المجموعات (إلزامي):

List<String> products = List.of("Apple", "Banana", "Avocado", "Cherry", "Apricot"); List<String> result = new ArrayList<>(); for (String name : products) { if (name.startsWith("A")) { result.add(name.toUpperCase()); } } // النتيجة: ["APPLE", "AVOCADO", "APRICOT"]

أسلوب الـStream (تصريحي):

List<String> products = List.of("Apple", "Banana", "Avocado", "Cherry", "Apricot"); List<String> result = products.stream() .filter(name -> name.startsWith("A")) .map(String::toUpperCase) .collect(Collectors.toList()); // النتيجة: ["APPLE", "AVOCADO", "APRICOT"]

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

القابلية للقراءة على نطاق واسع. تتعاظم ميزة أسلوب الـstream مع تزايد التعقيد. حين تُسلسل ثلاث أو أربع أو خمس عمليات تظل النية واضحة. الكود الإلزامي المكافئ يتراكم فيه الحلقات والمتغيرات المؤقتة التي تُخفي المنطق.

نموذج خط الأنابيب

كل تعبير stream يتبع البنية الثلاثية ذاتها:

  1. المصدر — من أين تأتي العناصر.
  2. العمليات الوسيطة — صفر أو أكثر من التحويلات التي ترجع stream جديدة.
  3. العملية النهائية — تُحفّز التنفيذ وتُنتج نتيجة (أو تأثيرًا جانبيًا).
// المصدر products.stream() // وسيطة (ترجع Stream، كسولة) .filter(name -> name.startsWith("A")) .map(String::toUpperCase) // نهائية (تُحفّز التنفيذ، ترجع نتيجة) .collect(Collectors.toList());

لا شيء يعمل حتى تُستدعى العملية النهائية. لو استدعيت filter(...).map(...) دون عملية نهائية، فإن Java لا تفعل شيئًا — لا تُستدعى أجسام lambdas مطلقًا. هذا الكسل مقصود: يتيح للمشغّل دمج العمليات وتجنّب إنشاء مجموعات وسيطة.

الكسل في التطبيق العملي

لرؤية الكسل بجلاء، أضف جملة طباعة داخل عملية وسيطة:

List<String> names = List.of("Alice", "Bob", "Charlie"); // لا مخرجات هنا — خط الأنابيب مبنيّ لكن غير منفَّذ Stream<String> pipeline = names.stream() .filter(n -> { System.out.println("filtering: " + n); return n.length() > 3; }); System.out.println("About to trigger..."); // العملية النهائية — الآن تعمل lambda الـfilter long count = pipeline.count(); System.out.println("Count: " + count);

المخرجات:

About to trigger... filtering: Alice filtering: Bob filtering: Charlie Count: 2

تظهر عبارة "About to trigger..." قبل أي تصفية، مما يُثبت أن العملية الوسيطة تأجّلت حتى استُدعيت count().

الـStream أحادي الاستخدام

بمجرد استدعاء عملية نهائية، يُستهلك الـstream. محاولة إعادة استخدامه تُطلق IllegalStateException.

Stream<String> s = List.of("a", "b").stream(); s.forEach(System.out::println); // صحيح s.forEach(System.out::println); // يُطلق IllegalStateException: stream has already been operated upon or closed
لا تُخزّن الـstreams. الـstream هو وصف خط أنابيب لمرة واحدة، وليس حاوية بيانات قابلة لإعادة الاستخدام. إن احتجت معالجة البيانات ذاتها مرتين، استدعِ .stream() على مجموعة المصدر مجددًا — وهو أمر غير مُكلف.

خلاصة سريعة

  • الـStream<T> هو خط أنابيب كسول أحادي الاستخدام فوق مصدر بيانات — لا يُخزّن شيئًا.
  • المجموعات تمتلك البيانات؛ الـstreams تعالجها.
  • كل stream له مصدر وصفر أو أكثر من العمليات الوسيطة وعملية نهائية واحدة بالضبط.
  • لا شيء ينفَّذ حتى تُستدعى العملية النهائية.

في الدرس التالي ستتعرّف على الطرق المتعددة التي تُتيحها Java لإنشاء stream — من المجموعات والمصفوفات والنطاقات والملفات والمولّدات.