العمل مع قيم Optional
العمل مع قيم Optional
في الدرس السابق تعرّفت على ماهية Optional وكيفية إنشائه. حان الآن وقت الاستخراج الفعلي للقيمة الداخلية — أو معالجة حالة الغياب. تمنحك Java أربع توابع أساسية للاستخراج: get()، وorElse()، وorElseGet()، وorElseThrow(). لكل منها عقد مختلف وتكلفة أداء مختلفة ومواقف بعينها هي الأنسب لها. اختيار التابع الخاطئ هو أحد أكثر أخطاء Optional شيوعًا في قواعد الكود الحقيقية.
get() — الاختصار الخطر
يُعيد get() القيمة مباشرةً إن كانت موجودة، ويرمي NoSuchElementException إن كان Optional فارغًا. يبدو مغريًا لأنّه مختصر:
get() دون استدعاء isPresent() أولًا مطابق معنويًا لإلغاء مرجع nullable دون التحقق من null — لقد نقلت المشكلة فقط. الهدف كله من Optional هو إجبار المستدعي على معالجة حالة الغياب على مستوى النوع، وget() يتيح لك تجاوز هذا الانضباط كليًا. الاستخدام الوحيد المشروع هو داخل فرع حرسته بـ isPresent() مسبقًا، لكن حتى في تلك الحالة فالبدائل الآمنة أدناه أوضح في الغالب.
orElse() — تقديم قيمة بديلة ثابتة
يُعيد orElse(T other) القيمة إن وُجدت، وإلا يُعيد الوسيطة التي تمرّرها:
يُقيَّم البديل بصورة فورية (eager) — أي أن التعبير الذي تمرّره يُحسب قبل استدعاء orElse تمامًا، بصرف النظر عمّا إذا كان Optional فارغًا أم لا. لثابت مثل "Guest" لا توجد مشكلة. لكن احذر حين يكون البديل مكلف الحساب:
orElse(0) أو orElse("")). أي شيء يتضمن استدعاء تابع ينتمي إلى orElseGet().
orElseGet() — بديل كسول عبر Supplier
يقبل orElseGet(Supplier<? extends T> supplier) تعبيرًا لامدائيًا (أو مرجع تابع) ولا يستدعيه إلا حين يكون Optional فارغًا. لا يُنفَّذ المورّد إطلاقًا إذا كانت القيمة موجودة:
هذا الفارق بالغ الأهمية في المسارات الحساسة للأداء. خذ سيناريو بحث في الذاكرة المؤقتة (cache):
لا تُضرب قاعدة البيانات إلا عند إخفاق التخزين المؤقت — وهذا بالضبط السلوك المطلوب. لو استخدمنا orElse(db.loadLabel(userId)) لاستُدعيت قاعدة البيانات في كل مرة، بما في ذلك حالات إصابة الكاش.
orElse(new BigObject()) يُخصص ذاكرة في كل مرة؛ orElseGet(BigObject::new) يُخصّص فقط عند الإخفاق. في حلقة مكثّفة هذا الفارق ملموس.
orElseThrow() — الإشارة إلى انتهاك عقد برمجي
يُعيد orElseThrow() (بدون وسيطات، أُضيف في Java 10) القيمة إن وُجدت، وإلا يرمي NoSuchElementException. وظيفيًا يشبه get()، لكن الاسم يوصل النية: أنت تؤكّد أن Optional الفارغ في هذه النقطة يمثّل خطأً أو شرطًا مسبقًا منتهكًا.
التحميل الزائد الأكثر فائدة يقبل Supplier<? extends Throwable> لترمي استثناءً خاصًا بالنطاق:
التحميل المزوّد بالمورّد كسول — لا يُبنى كائن الاستثناء إلا حين يكون Optional فارغًا فعلًا.
get() المجرد، لأن رسالتك المخصصة تُسمّي بالضبط ما هو مفقود ولماذا.
مقارنة الأربعة جنبًا إلى جنب
إليك السيناريو نفسه معبَّرًا عنه بكل تابع حتى يتضح الفارق بجلاء:
مثال عملي واقعي
خذ خدمة تُحمّل ملف تعريف المستخدم وتبني اسم العرض للواجهة:
لاحظ أن الكود لا يستدعي get() قط ولا يختبر isPresent() يدويًا. المنطق يُقرأ كخط أنابيب: يُفضَّل الاسم المفضّل، ثم الاسم الأول كبديل، ثم "Anonymous" كبديل أخير. كل بديل لا يُقيَّم إلا إذا أنتج المرحلة السابقة قيمة فارغة.
الخلاصة
- get() — تجنّبه؛ استخدمه فقط داخل حراسة
isPresent()إن لم يجدِ غيره. - orElse(ثابت) — تقييم فوري؛ آمن للثوابت والتعبيرات الرخيصة.
- orElseGet(مورّد) — تقييم كسول؛ استخدمه متى تضمّن البديل استدعاء تابع أو إدخالًا/إخراجًا أو إنشاء كائن.
- orElseThrow(مورّد) — يرمي عند الفراغ؛ استخدمه لفرض الشروط المسبقة وإنتاج رسائل خطأ ذات مغزى على حدود الطبقات.