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

Optional مع Streams

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

Optional مع Streams

يوجد في Streams عمليتان نهائيتان — findFirst() وfindAny() — لا تُعيدان قيمة ملموسة مباشرة، بل تُعيدان Optional<T>. يتناول هذا الدرس السبب وراء ذلك، وكيفية التعامل مع هذا الـOptional بأمان.

لماذا تُعيد عمليات البحث Optional؟

قد يكون الـstream فارغًا. لو طلبت "أعطني أول عنصر" وكان الـstream لا يحتوي أي عناصر، فلا توجد قيمة منطقية للإعادة. في إصدارات Java القديمة كان الحل هو إعادة null، وكان على كل كود يستدعي الدالة أن يتذكّر التحقق من null. يجعل Optional<T> احتمال الغياب صريحًا في نظام الأنواع — ويُجبر المُصرِّف على معالجته.

Optional هو حاوية. إما تحتوي قيمة واحدة (موجودة) أو لا شيء (فارغة). ليست مجموعة — تحتوي عنصرًا واحدًا على الأكثر. فكّر فيها كبديل مُحدَّد النوع لـnull.

findFirst() — بحث حتمي

تُعيد findFirst() أول عنصر في الـstream يجتاز عمليات الـfilter السابقة، مُغلَّفًا داخل Optional. "الأول" يعني العنصر الأول وفق ترتيب المصدر الأصلي (كقائمة List مثلًا).

import java.util.List; import java.util.Optional; List<String> names = List.of("Alice", "Bob", "Charlie", "Anna"); Optional<String> first = names.stream() .filter(name -> name.startsWith("A")) .findFirst(); System.out.println(first); // Optional[Alice]

يجد الـstream "Alice" أولًا ويتوقف — لا يفحص بقية القائمة. تعمل الـstreams بشكل كسول: تسحب العمليات النهائية فقط العناصر التي تحتاجها.

findAny() — بحث ملائم للمعالجة المتوازية

تُعيد findAny() أي عنصر يُحقق شروط الـfilter السابقة. في الـstream التسلسلي عادةً ما تُعيد العنصر الأول، لكن لا يوجد ضمان من JVM على ذلك. في الـstream المتوازي تُعيد أي عنصر تجده أي خيط تنفيذ أولًا، وهذا أسرع لأن الخيوط لا تحتاج الاتفاق على الترتيب.

import java.util.List; import java.util.Optional; List<Integer> numbers = List.of(1, 3, 5, 8, 10, 12); Optional<Integer> anyEven = numbers.parallelStream() .filter(n -> n % 2 == 0) .findAny(); // قد تطبع Optional[8] أو Optional[10] أو Optional[12] — غير حتمي System.out.println(anyEven);
قاعدة عملية: استخدم findFirst() حين تحتاج نتيجة متوقعة وقابلة للتكرار (الاختبارات، المعالجة المرتّبة). استخدم findAny() حين تحتاج فقط أي تطابق وتستخدم stream متوازيًا لتحسين الأداء.

معالجة نتيجة Optional

تُعطيك كلتا العمليتين Optional<T>. توجد عدة طرق لفتح القيمة منه بأمان.

isPresent() / get()

أوضح أسلوب — لكنه الأكثر تفصيلًا:

Optional<String> result = names.stream() .filter(n -> n.startsWith("Z")) .findFirst(); if (result.isPresent()) { System.out.println("Found: " + result.get()); } else { System.out.println("Not found"); }
لا تستدعِ get() دون التحقق أولًا بـisPresent() (أو استخدام أحد البدائل الأكثر أمانًا أدناه). إذا كان الـOptional فارغًا، يُلقي get() استثناء NoSuchElementException — وهو تمامًا ما صُمِّم Optional لمنعه.

orElse() — توفير قيمة افتراضية

تُعيد القيمة إن وُجدت، أو القيمة الاحتياطية التي تُمرّرها:

String found = names.stream() .filter(n -> n.startsWith("Z")) .findFirst() .orElse("Unknown"); System.out.println(found); // Unknown

orElseGet() — قيمة افتراضية كسولة عبر Supplier

مثل orElse()، لكن القيمة الاحتياطية تُحسَب فقط عند الحاجة فعليًا. فضّل هذا حين يكون الاحتياطي مكلفًا (استعلام قاعدة بيانات، بناء كائن، إلخ):

String found = names.stream() .filter(n -> n.startsWith("A")) .findFirst() .orElseGet(() -> "default-from-db"); System.out.println(found); // Alice (lambda لا تُنفَّذ أصلًا)

orElseThrow() — الفشل الصريح

يُلقي استثناءً إذا كان الـOptional فارغًا. استخدمه حين يكون غياب القيمة خطأً برمجيًا حقيقيًا:

String admin = names.stream() .filter(n -> n.equals("Admin")) .findFirst() .orElseThrow(() -> new IllegalStateException("Admin user must exist in the list"));

ifPresent() — تنفيذ تأثير جانبي عند الوجود فقط

ينفّذ Consumer عند احتواء الـOptional قيمة، ولا يفعل شيئًا عند فراغه:

names.stream() .filter(n -> n.startsWith("B")) .findFirst() .ifPresent(n -> System.out.println("Processing: " + n)); // Processing: Bob

سلسلة تحويلات Optional

يمتلك Optional نفسه دوال map() وfilter()، فيمكنك تحويل القيمة بداخله دون فتحه مبكرًا:

List<String> emails = List.of("alice@example.com", "bob@example.com"); Optional<String> domain = emails.stream() .filter(e -> e.startsWith("alice")) .findFirst() .map(e -> e.substring(e.indexOf('@') + 1)); // يُحوِّل القيمة داخل Optional domain.ifPresent(d -> System.out.println("Domain: " + d)); // Domain: example.com

مرجع سريع

  • findFirst() — أول تطابق وفق ترتيب المصدر؛ للاستخدام التسلسلي الحتمي.
  • findAny() — أي تطابق؛ يُفضَّل على الـstreams المتوازية للأداء.
  • orElse(T) — قيمة افتراضية ثابتة.
  • orElseGet(Supplier) — قيمة افتراضية كسولة، تُحسَب فقط عند الفراغ.
  • orElseThrow(Supplier) — إلقاء استثناء حين يكون الغياب خطأً.
  • ifPresent(Consumer) — تأثير جانبي عند وجود القيمة.
  • map(Function) — تحويل القيمة مع البقاء داخل Optional.

إتقان هذه الأنماط يجعل كودك يُعبّر بوضوح عن نواياه: القيم الغائبة مُعالَجة صراحةً، وأخطاء NPE مُتجنَّبة بالتصميم، والبحث المتوازي سهل التعبير عنه.