Optional وجافا الحديثة

مقدّمة إلى Optional

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

مقدّمة إلى Optional

في الدرس السابق رأينا كيف يُفضي null إلى NullPointerException وعقود غير واضحة وكود دفاعي منتشر في كل مكان. قدّم Java 8 الفئة java.util.Optional<T> كحاوية مكتوبة تعبّر صراحةً عن احتمالية الغياب. يتناول هذا الدرس الطرق الثلاث لإنشاء Optional والأساليب الأساسية للتحقّق من وجود قيمة بداخله.

ما هو Optional وما ليس عليه

Optional<T> غلاف من نوع قيمة. له حالتان فحسب:

  • حاضر — يحتوي على قيمة غير null من النوع T.
  • فارغ — لا يحتوي على أي قيمة (مشابه لـ null لكنه صريح).
Optional أداةٌ لنوع القيمة المُعادة، ليس لنوع الحقل. غرضه أن يجعل توقيع الدالة يقول "هذا قد لا يُعيد شيئًا" دون أن يضطر المُستدعي لقراءة Javadoc أو التخمين. إنه ليس بديلًا شاملًا عن كل مرجع قابل للإلغاء في قاعدة الكود — استخدامه كنوع حقل أو معامل دالة يُضيف تكلفة بفائدة ضئيلة.

الطرق الثلاث للإنشاء

Optional.of — حين تكون متأكدًا من وجود القيمة

Optional.of(value) يُغلّف قيمة تعلم يقينًا أنها غير null. إذا مرّرت null رمى NullPointerException فورًا، وهذا مقصود — يفشل مبكرًا عند موقع الإنشاء بدلًا من الانتشار الصامت.

import java.util.Optional; String name = "Alice"; Optional<String> opt = Optional.of(name); // آمن: name غير null بالتأكيد // تمرير null ينفجر هنا مباشرة — جيّد، فشل مبكّر // Optional<String> bad = Optional.of(null); // يرمي NullPointerException

استخدم Optional.of حين تأتي القيمة من مصدر تتحكّم فيه وتضمن أنها غير null — كثابت أو كائن منشأ للتو أو قيمة تحقّقت منها للتو.

Optional.ofNullable — حين قد تكون القيمة null أو لا

Optional.ofNullable(value) هو أكثر المصانع شيوعًا. يفحص القيمة: إن كانت غير null يُعيد Optional حاضرًا، وإن كانت null يُعيد Optional فارغًا. إنه الجسر بين واجهات برمجية قديمة تُعيد null وعالم Optional.

import java.util.Optional; import java.util.Map; Map<String, String> config = Map.of("host", "localhost"); // get() يُعيد null لأن "port" غير موجود في الخريطة Optional<String> maybePort = Optional.ofNullable(config.get("port")); System.out.println(maybePort); // Optional.empty System.out.println(maybePort.isPresent()); // false Optional<String> maybeHost = Optional.ofNullable(config.get("host")); System.out.println(maybeHost); // Optional[localhost] System.out.println(maybeHost.isPresent()); // true

Optional.empty — Optional فارغ صريح

Optional.empty() ينشئ حاوية فارغة بلا قيمة. تستخدمه في الدوال التي تُعيد Optional<T> حين لا يوجد شيء لإعادته — إنه البديل المكتوب والمتعمَّد لعبارة return null.

import java.util.Optional; import java.util.List; public Optional<String> findFirstLongWord(List<String> words, int minLength) { for (String word : words) { if (word.length() >= minLength) { return Optional.of(word); // وجدنا كلمة — نُغلّفها } } return Optional.empty(); // لا يوجد تطابق — صريح ومقصود }

إعادة Optional.empty() أكثر صدقًا بكثير من إعادة null: يُلزم المُترجم المُستدعِين بالتعامل مع نوع الإعادة Optional<String>، ولا حاجة لأي تعليق أو تعليقة توضيحية للتعبير عن الغياب.

التحقّق من وجود القيمة

بعد الحصول على Optional يمكنك الاستفسار عما إذا كان يحتوي على قيمة بثلاث طرق مترابطة:

  • isPresent() — تُعيد true إذا كانت قيمة موجودة.
  • isEmpty() — تُعيد true إذا كانت الحاوية فارغة (أُضيفت في Java 11؛ عكس isPresent() بصياغة أوضح).
  • get() — تُعيد القيمة المُغلّفة، لكنها ترمي NoSuchElementException إذا كانت الحاوية فارغة.
import java.util.Optional; Optional<Integer> score = Optional.of(42); if (score.isPresent()) { System.out.println("Score: " + score.get()); // Score: 42 } Optional<Integer> noScore = Optional.empty(); if (noScore.isEmpty()) { System.out.println("No score recorded."); // No score recorded. } // خطير — احرص دائمًا على حماية get() بـ isPresent() أو استخدم طرقًا أكثر أمانًا // noScore.get(); // يرمي NoSuchElementException
تجنّب استدعاء get() المجرّد في كود الإنتاج. استدعاء get() دون فحص isPresent() مسبق خطير تمامًا مثل إلغاء مرجع قد يكون null — لقد استبدلت NullPointerException بـ NoSuchElementException فحسب. في الدروس القادمة ستتعلّم طرق الاستخراج الأكثر أمانًا (orElse وorElseGet وmap وifPresent) التي تعبّر عن النية بوضوح ولا يمكنها الرمي.

المساواة وتمثيل النص

تتجاوز Optional دالتي equals وhashCode بتفويض الأمر للقيمة المُغلّفة؛ فـ Optional حاضران تحويان قيمتين متساويتين يعتبران متساويين، وOptional فارغان يعتبران متساويين دائمًا.

Optional<String> a = Optional.of("hello"); Optional<String> b = Optional.of("hello"); Optional<String> c = Optional.empty(); Optional<String> d = Optional.empty(); System.out.println(a.equals(b)); // true System.out.println(c.equals(d)); // true System.out.println(a.equals(c)); // false // toString مقروء للتنقيح System.out.println(a); // Optional[hello] System.out.println(c); // Optional.empty

اختيار الطريقة المناسبة للإنشاء

شجرة قرار بسيطة:

  • تتحكّم في القيمة وهي لن تكون null أبدًا → Optional.of(value)
  • القيمة تأتي من واجهة برمجية قديمة أو مدخلات مستخدم قد تكون null → Optional.ofNullable(value)
  • تكتب دالة وليس لديك نتيجة لإعادتها → Optional.empty()
فضّل ofNullable عند الشك. إذا لم تكن متأكدًا 100% من أن القيمة غير null استخدم ofNullable. التحقّق الإضافي من null لا يُكلّف شيئًا يُذكر، والفشل المبكّر لـ of مفيد فقط حين تريد فعلًا الانهيار الصاخب عند انتهاك العقد — كالتحقّق في المنشئ مثلًا.

الخلاصة

Optional.of يُغلّف قيمة مضمونة غير null ويفشل مبكّرًا عند null. Optional.ofNullable يُغلّف أي شيء محوّلًا null إلى فارغ بصمت. Optional.empty() يمثّل إعادة لا-قيمة صريحة. للتفتيش عن الحالة استخدم isPresent() أو isEmpty() (Java 11+)، وعامِل get() المجرّد باعتباره ملاذًا أخيرًا يستلزم حمايةً. في الدرس القادم ستتعرّف على واجهة الاستخراج والتحويل الأغنى — orElse وorElseGet وorElseThrow — التي تجعل Optional مفيدًا حقًا لا مجرّد فحص null مُعاد تسميته.