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

إنشاء التدفقات (Streams)

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

إنشاء التدفقات (Streams)

قبل أن تتمكن من معالجة البيانات باستخدام Streams API تحتاج أولًا إلى تدفق. تتيح لك Java عدة طرق مصنعية ومصادر بيانات مختلفة، كل منها مناسب لموقف معين. معرفة أي نقطة بداية تختار — ولماذا — تجعل كودك أنظف بكثير من البداية بدلًا من الإكثار من الحلقات اليدوية.

لماذا يهم المصدر؟

Stream<T> هو خط أنابيب كسول (lazy) للاستخدام مرة واحدة. إنه لا يحتفظ بالبيانات؛ بل يصف كيفية سحب البيانات من مصدر وتحويلها. المصدر هو الذي يحدد نوع العناصر وضمان الترتيب، وأحيانًا ما إذا كان بإمكان التدفق العمل بكفاءة بالتوازي. لذلك اختيار المصنع المناسب ليس مجرد تفضيل شكلي — بل يؤثر على صحة الكود وأدائه.

التدفقات من المجموعات (Collections)

المصدر الأكثر شيوعًا هو أي Collection (قائمة List أو مجموعة Set أو طابور Queue). كل مجموعة ترث stream() وparallelStream() من java.util.Collection:

import java.util.List; import java.util.Set; import java.util.stream.Stream; List<String> names = List.of("Alice", "Bob", "Carol"); // تدفق متسلسل — العناصر تصل بترتيب القائمة Stream<String> sequential = names.stream(); // تدفق متوازٍ — الترتيب غير مضمون Stream<String> parallel = names.parallelStream(); sequential.forEach(System.out::println); // Alice Bob Carol (بالترتيب)
ضمان الترتيب: List.stream() يحافظ على ترتيب الإدخال. Set.stream() لا يضمن ذلك — إذ لا يوجد ترتيب محدد لـ HashSet. إذا كان الترتيب مهمًا فضّل استخدام List أو LinkedHashSet.

التدفقات من المصفوفات (Arrays)

عندما تكون بياناتك في مصفوفة استخدم Arrays.stream(). وتقبل هذه الطريقة أيضًا نطاقًا (range) لتدفق جزء من المصفوفة دون نسخها:

import java.util.Arrays; int[] scores = {88, 72, 95, 60, 81}; // تدفق المصفوفة كاملة Arrays.stream(scores).forEach(System.out::println); // تدفق الفهارس من 1 إلى 3 (بداية شاملة، نهاية غير شاملة) Arrays.stream(scores, 1, 4).forEach(System.out::println); // 72 95 60

تُعيد Arrays.stream(int[]) تدفقًا من نوع IntStream لا Stream<Integer>. هذا مقصود — فتدفقات الأنواع الأصلية (primitive streams) تتجنب تكلفة التغليف (boxing)، وسيُغطّى هذا بالتفصيل في الدرس الثامن.

Stream.of — إنشاء تدفق من قيم مباشرة

تبني Stream.of() تدفقًا مباشرة من عدد من القيم دون الحاجة إلى إنشاء مجموعة أولًا. وهي مثالية للاختبارات والنماذج الأولية والحالات التي تملك فيها مراجع فردية:

import java.util.stream.Stream; // varargs — تعمل مع أي عدد من العناصر Stream<String> colours = Stream.of("red", "green", "blue"); colours.forEach(System.out::println); // عنصر واحد Stream<Integer> one = Stream.of(42); // تدفق فارغ (صالح ومفيد) Stream<String> empty = Stream.empty();
فضّل Stream.empty() على Stream.of() بدون وسيطات عندما تريد تدفقًا فارغًا. الطريقة المخصصة تجعل القصد واضحًا وتتجنب تحذير generic غير محدد النوع.

Stream.generate — تدفقات لانهائية بناءً على Supplier

تُنشئ Stream.generate(Supplier<T>) تدفقًا لانهائيًا باستدعاء Supplier بشكل متكرر. ولأنه لا ينتهي من تلقاء نفسه يجب إضافة عملية قصيرة الدائرة (short-circuit) مثل limit():

import java.util.UUID; import java.util.stream.Stream; // توليد 5 معرّفات UUID عشوائية Stream.generate(() -> UUID.randomUUID().toString()) .limit(5) .forEach(System.out::println); // تدفق ثابت القيمة (مفيد في الاختبارات) Stream.generate(() -> "ping") .limit(3) .forEach(System.out::println); // ping ping ping

لا تحتفظ الدالة المُمررة بذاكرة عن الاستدعاءات السابقة — كل استدعاء مستقل عن سابقه. إذا احتجت إلى عناصر تعتمد على موضعها أو على القيمة السابقة، فاستخدم Stream.iterate بدلًا من ذلك.

Stream.iterate — تدفقات لانهائية بناءً على تسلسل

تمتلك Stream.iterate صيغتين:

  • الصيغة بوسيطتين (Java 8+): Stream.iterate(seed, UnaryOperator) — لانهائية، توقف بـ limit() أو takeWhile().
  • الصيغة بثلاث وسيطات (Java 9+): Stream.iterate(seed, Predicate, UnaryOperator) — شرط توقف مدمج، تعمل كحلقة for.
import java.util.stream.Stream; // صيغة بوسيطتين: الأعداد الزوجية 0، 2، 4، 6، 8 Stream.iterate(0, n -> n + 2) .limit(5) .forEach(System.out::println); // صيغة ثلاث وسيطات (Java 9): نفس النتيجة دون الحاجة لـ limit() Stream.iterate(0, n -> n < 10, n -> n + 2) .forEach(System.out::println); // متتالية فيبوناتشي (أول 10 حدود) باستخدام مصفوفة للاحتفاظ بالحالة Stream.iterate(new long[]{0, 1}, f -> new long[]{f[1], f[0] + f[1]}) .limit(10) .map(f -> f[0]) .forEach(System.out::println);
لا تستدعِ أبدًا عملية نهائية (terminal operation) على تدفق لانهائي بدون حارس قصير الدائرة. استدعاء collect() أو count() على تدفق generate غير محدود أو iterate بوسيطتين سيعمل إلى الأبد (أو حتى نفاد ذاكرة JVM). اقرن دائمًا هذه التدفقات بـ limit(n) أو takeWhile(predicate).

اختيار المصنع المناسب

  • لديك List أو Set؟ استخدم .stream().
  • لديك مصفوفة موجودة؟ استخدم Arrays.stream().
  • لديك مجموعة صغيرة من القيم المعروفة؟ استخدم Stream.of().
  • تحتاج إلى قيم مولّدة لا علاقة لكل منها بما سبقها؟ استخدم Stream.generate().
  • تحتاج إلى تسلسل حيث تعتمد كل قيمة على السابقة؟ استخدم Stream.iterate().

الخلاصة

تتيح Java خمس طرق رئيسية لإنشاء تدفق: من Collection، أو من مصفوفة عبر Arrays.stream()، أو من قيم حرفية عبر Stream.of()، أو من مصادر لانهائية عبر Stream.generate() وStream.iterate(). لكل نوع مصدر حالة استخدام واضحة. في الدرس التالي ستُضيف مراحل التحويل الأولى — filter وmap وforEach — إلى التدفقات التي أنشأتها هنا.