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

تدفقات الأرقام

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

تدفقات الأرقام

يُعدّ Stream<T> ذو الغرض العام رائعًا للكائنات، غير أنه يحمل تكلفة خفية عند العمل مع الأرقام: كل قيمة بدائية int أو long يجب تغليفها في كائن Integer أو Long. التغليف يُخصّص كائنات على الكومة ويُضغط على مجمّع المهملات. في الخطوط التي تُعالج آلاف أو ملايين القيم، هذه التكلفة حقيقية وملموسة.

تحلّ Java هذه المشكلة بأنواع تدفق مخصّصة للأرقام البدائية: IntStream وLongStream وDoubleStream. هي تمامًا كـ Stream<T> ولكنها تعمل مباشرة على الأرقام البدائية الخام — دون تغليف ودون كائنات مُغلِّفة.

إنشاء IntStream

أكثر الطرق شيوعًا للحصول على IntStream:

import java.util.stream.IntStream; // 1. نطاق مغلق [1, 5] — يشمل الطرفين IntStream closed = IntStream.rangeClosed(1, 5); // 1 2 3 4 5 // 2. نطاق نصف مفتوح [0, 5) — يستثني الحدّ الأعلى (مثل فهرس حلقة for) IntStream open = IntStream.range(0, 5); // 0 1 2 3 4 // 3. قيم صريحة IntStream explicit = IntStream.of(10, 20, 30); // 4. من مصفوفة int int[] arr = {3, 1, 4, 1, 5}; IntStream fromArray = Arrays.stream(arr); // 5. من تدفق كائنات عبر mapToInt List<String> words = List.of("apple", "banana", "kiwi"); IntStream lengths = words.stream().mapToInt(String::length); // 5 6 4
range مقابل rangeClosed: IntStream.range(0, n) يُحاكي حلقة for (int i = 0; i < n; i++) — الحدّ الأعلى مستثنى. أما rangeClosed(1, n) فيُحاكي for (int i = 1; i <= n; i++). اختر التي تتناسب مع نمط التفكير الذي يناسب المسألة.

LongStream — حين لا يكفي int

يملك LongStream نفس واجهة برمجة IntStream لكنه يعمل مع الأرقام البدائية من نوع long. استخدمه كلما كانت قيمك تتجاوز نحو 2.1 مليار — مثل أحجام الملفات بالبايت، أو طوابع Unix الزمنية، أو معرّفات الصفوف في قاعدة بيانات كبيرة.

import java.util.stream.LongStream; // مجموع الأرقام من 1 إلى 1,000,000 — النتيجة ستتجاوز حدود int long sum = LongStream.rangeClosed(1, 1_000_000).sum(); System.out.println(sum); // 500000500000 // الوقت الحالي بالميلي ثانية، ثم إضافة إزاحات long now = System.currentTimeMillis(); LongStream.of(now, now + 3600_000L, now + 7200_000L) .forEach(System.out::println);

العمليات الطرفية المدمجة: sum وaverage وmin وmax

لأن نوع التدفق يعلم بالفعل أنه يحمل أرقامًا، يستطيع تقديم عمليات طرفية لا معنى لها في Stream<T> العام:

IntStream scores = IntStream.of(88, 92, 75, 95, 60); int total = IntStream.of(88, 92, 75, 95, 60).sum(); // 410 int highest = IntStream.of(88, 92, 75, 95, 60).max().getAsInt(); // 95 int lowest = IntStream.of(88, 92, 75, 95, 60).min().getAsInt(); // 60 // average ترجع OptionalDouble لأن التدفق الفارغ لا متوسط له double avg = IntStream.of(88, 92, 75, 95, 60).average().getAsDouble(); // 82.0 System.out.println("المجموع: " + total); System.out.println("الأعلى: " + highest); System.out.println("المتوسط: " + avg);
التدفقات تُستهلك بعد عملية طرفية واحدة. في الأمثلة أعلاه كُتبت كل خطوط الأنابيب من الصفر تحديدًا لأنه لا يمكن إعادة استخدام تدفق. إن احتجت إحصاءات متعددة، استدعِ summaryStatistics() بدلًا من ذلك (انظر أدناه) — فهو يُجري مرورًا واحدًا.

summaryStatistics — مرور واحد يُعطيك كل شيء

استدعاء sum() ثم average() ثم max() على البيانات ذاتها يتطلب ثلاثة تدفقات منفصلة وثلاث مرورات. بينما تحسب summaryStatistics() الإحصاءات الخمس جميعها في مرور واحد:

import java.util.IntSummaryStatistics; IntSummaryStatistics stats = IntStream.of(88, 92, 75, 95, 60) .summaryStatistics(); System.out.println("العدد: " + stats.getCount()); // 5 System.out.println("المجموع: " + stats.getSum()); // 410 System.out.println("الأدنى: " + stats.getMin()); // 60 System.out.println("الأعلى: " + stats.getMax()); // 95 System.out.println("المتوسط: " + stats.getAverage()); // 82.0

يملك LongStream ما يعادلها LongSummaryStatistics، ويملك DoubleStream ما يعادلها DoubleSummaryStatistics. كلٌّ منها يُوفّر نفس الأدوات الخمس.

متى تستخدم summaryStatistics: كلما احتاج كودك أكثر من تجميع رقمي واحد من نفس مجموعة البيانات. إنه دائمًا أكفأ من عمليات طرفية متعددة على تدفقات مُعاد بناؤها، وهو يجعل الكود مختصرًا.

التحويل بين التدفقات البدائية وتدفقات الكائنات

أحيانًا تبدأ بتدفق كائنات وتحتاج تدفقًا رقميًا، أو العكس:

import java.util.List; import java.util.stream.Collectors; List<String> names = List.of("Alice", "Bob", "Charlie", "Dan"); // تدفق كائنات --> IntStream IntStream nameLengths = names.stream().mapToInt(String::length); // IntStream --> Stream<Integer> (يُغلّف كل int) Stream<Integer> boxed = IntStream.range(1, 5).boxed(); // IntStream --> List<Integer> List<Integer> list = IntStream.rangeClosed(1, 5) .boxed() .collect(Collectors.toList()); System.out.println(list); // [1, 2, 3, 4, 5]

مثال عملي: محلّل الدرجات

نضع كل ما تعلمناه معًا — مقتطف واقعي يُحلّل قائمة درجات اختبار:

import java.util.IntSummaryStatistics; import java.util.List; public class GradeAnalyser { public static void main(String[] args) { List<Integer> grades = List.of(73, 88, 55, 91, 67, 84, 49, 95, 78, 62); IntSummaryStatistics stats = grades.stream() .mapToInt(Integer::intValue) .summaryStatistics(); long passing = grades.stream() .mapToInt(Integer::intValue) .filter(g -> g >= 60) .count(); System.out.printf("الطلاب : %d%n", stats.getCount()); System.out.printf("الأعلى : %d%n", stats.getMax()); System.out.printf("الأدنى : %d%n", stats.getMin()); System.out.printf("المتوسط : %.1f%n", stats.getAverage()); System.out.printf("الناجحون : %d / %d%n", passing, stats.getCount()); } }

الخلاصة

استخدم IntStream وLongStream كلما كان خط أنابيبك يعمل مع أعداد صحيحة بدائية — فهي تتجنّب تكلفة التغليف، وتُوفّر عمليات طرفية مريحة مثل sum() وaverage() وmin() وmax()، وتقدّم summaryStatistics() لجمع الإحصاءات الخمس في مرور واحد فعّال. عُد إلى تدفق الكائنات باستخدام .boxed() متى احتجت إلى List أو المزيد من عمليات التدفق الكائني.