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

العمل مع قيم Optional

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

العمل مع قيم Optional

في الدرس السابق تعرّفت على ماهية Optional وكيفية إنشائه. حان الآن وقت الاستخراج الفعلي للقيمة الداخلية — أو معالجة حالة الغياب. تمنحك Java أربع توابع أساسية للاستخراج: get()، وorElse()، وorElseGet()، وorElseThrow(). لكل منها عقد مختلف وتكلفة أداء مختلفة ومواقف بعينها هي الأنسب لها. اختيار التابع الخاطئ هو أحد أكثر أخطاء Optional شيوعًا في قواعد الكود الحقيقية.

get() — الاختصار الخطر

يُعيد get() القيمة مباشرةً إن كانت موجودة، ويرمي NoSuchElementException إن كان Optional فارغًا. يبدو مغريًا لأنّه مختصر:

Optional<String> name = Optional.of("Alice"); String value = name.get(); // "Alice" Optional<String> empty = Optional.empty(); String boom = empty.get(); // يرمي NoSuchElementException أثناء التشغيل
تجنّب get() في غالبية الحالات. استدعاء get() دون استدعاء isPresent() أولًا مطابق معنويًا لإلغاء مرجع nullable دون التحقق من null — لقد نقلت المشكلة فقط. الهدف كله من Optional هو إجبار المستدعي على معالجة حالة الغياب على مستوى النوع، وget() يتيح لك تجاوز هذا الانضباط كليًا. الاستخدام الوحيد المشروع هو داخل فرع حرسته بـ isPresent() مسبقًا، لكن حتى في تلك الحالة فالبدائل الآمنة أدناه أوضح في الغالب.

orElse() — تقديم قيمة بديلة ثابتة

يُعيد orElse(T other) القيمة إن وُجدت، وإلا يُعيد الوسيطة التي تمرّرها:

Optional<String> maybeName = findUserName(userId); // يُعيد "Guest" إن لم يُعثر على اسم String displayName = maybeName.orElse("Guest");

يُقيَّم البديل بصورة فورية (eager) — أي أن التعبير الذي تمرّره يُحسب قبل استدعاء orElse تمامًا، بصرف النظر عمّا إذا كان Optional فارغًا أم لا. لثابت مثل "Guest" لا توجد مشكلة. لكن احذر حين يكون البديل مكلف الحساب:

// خاطئ: يُستدعى computeExpensiveDefault() دائمًا حتى حين Optional ممتلئ String result = maybeValue.orElse(computeExpensiveDefault()); // صحيح: استخدم orElseGet() للبدائل المحسوبة (انظر أدناه)
قاعدة عمل مع orElse(): استخدمه فقط حين يكون البديل ثابتًا وقت الترجمة، أو متغيرًا محسوبًا مسبقًا، أو تعبيرًا رخيصًا جدًا (مثل orElse(0) أو orElse("")). أي شيء يتضمن استدعاء تابع ينتمي إلى orElseGet().

orElseGet() — بديل كسول عبر Supplier

يقبل orElseGet(Supplier<? extends T> supplier) تعبيرًا لامدائيًا (أو مرجع تابع) ولا يستدعيه إلا حين يكون Optional فارغًا. لا يُنفَّذ المورّد إطلاقًا إذا كانت القيمة موجودة:

Optional<String> maybeName = findUserName(userId); // يُنفَّذ التعبير اللامدائي فقط حين maybeName فارغ String displayName = maybeName.orElseGet(() -> loadDefaultFromDatabase(userId));

هذا الفارق بالغ الأهمية في المسارات الحساسة للأداء. خذ سيناريو بحث في الذاكرة المؤقتة (cache):

public String getUserLabel(long userId) { return cache.find(userId) // يُعيد Optional<String> .orElseGet(() -> { String fresh = db.loadLabel(userId); cache.put(userId, fresh); return fresh; }); }

لا تُضرب قاعدة البيانات إلا عند إخفاق التخزين المؤقت — وهذا بالضبط السلوك المطلوب. لو استخدمنا orElse(db.loadLabel(userId)) لاستُدعيت قاعدة البيانات في كل مرة، بما في ذلك حالات إصابة الكاش.

orElseGet() هو الخيار الصحيح أيضًا لإنشاء الكائنات. orElse(new BigObject()) يُخصص ذاكرة في كل مرة؛ orElseGet(BigObject::new) يُخصّص فقط عند الإخفاق. في حلقة مكثّفة هذا الفارق ملموس.

orElseThrow() — الإشارة إلى انتهاك عقد برمجي

يُعيد orElseThrow() (بدون وسيطات، أُضيف في Java 10) القيمة إن وُجدت، وإلا يرمي NoSuchElementException. وظيفيًا يشبه get()، لكن الاسم يوصل النية: أنت تؤكّد أن Optional الفارغ في هذه النقطة يمثّل خطأً أو شرطًا مسبقًا منتهكًا.

التحميل الزائد الأكثر فائدة يقبل Supplier<? extends Throwable> لترمي استثناءً خاصًا بالنطاق:

// بدون وسيطة: يرمي NoSuchElementException إن كان فارغًا User user = userRepository.findById(id) .orElseThrow(); // مع مورّد: يرمي استثناءً من النطاق — أكثر إفادةً بكثير User user = userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException( "User with id " + id + " does not exist"));

التحميل المزوّد بالمورّد كسول — لا يُبنى كائن الاستثناء إلا حين يكون Optional فارغًا فعلًا.

متى تستخدم orElseThrow(): استخدمه على حدود الطبقات حيث الغياب استثنائي حقًا — مثلًا حين يتحقق المتحكم بالفعل من وجود كيان وطبقة الخدمة لا ينبغي أن ترى نتيجة فارغة. كما يجعل تتبّعات المكدّس أكثر فائدة بكثير من get() المجرد، لأن رسالتك المخصصة تُسمّي بالضبط ما هو مفقود ولماذا.

مقارنة الأربعة جنبًا إلى جنب

إليك السيناريو نفسه معبَّرًا عنه بكل تابع حتى يتضح الفارق بجلاء:

Optional<String> opt = findSetting("theme"); // 1. get() — خطر؛ تجنّبه إلا بحراسة مسبقة String v1 = opt.get(); // 2. orElse() — فوري؛ ممتاز للثوابت String v2 = opt.orElse("light"); // 3. orElseGet() — كسول؛ ممتاز للبدائل المحسوبة String v3 = opt.orElseGet(() -> configService.getDefaultTheme()); // 4. orElseThrow() — يؤكّد الوجود؛ ممتاز للعقود String v4 = opt.orElseThrow(() -> new IllegalStateException("theme setting is required"));

مثال عملي واقعي

خذ خدمة تُحمّل ملف تعريف المستخدم وتبني اسم العرض للواجهة:

public record UserProfile(String firstName, String lastName, String preferredName) {} public String buildDisplayName(long userId) { Optional<UserProfile> profile = profileRepository.findById(userId); // استخدم الاسم المفضّل إن وُجد؛ ارجع للاسم الأول عبر استدعاء كسول؛ لا null أبدًا String name = profile .map(UserProfile::preferredName) .filter(s -> !s.isBlank()) .orElseGet(() -> profile .map(UserProfile::firstName) .orElse("Anonymous")); return name; }

لاحظ أن الكود لا يستدعي get() قط ولا يختبر isPresent() يدويًا. المنطق يُقرأ كخط أنابيب: يُفضَّل الاسم المفضّل، ثم الاسم الأول كبديل، ثم "Anonymous" كبديل أخير. كل بديل لا يُقيَّم إلا إذا أنتج المرحلة السابقة قيمة فارغة.

الخلاصة

  • get() — تجنّبه؛ استخدمه فقط داخل حراسة isPresent() إن لم يجدِ غيره.
  • orElse(ثابت) — تقييم فوري؛ آمن للثوابت والتعبيرات الرخيصة.
  • orElseGet(مورّد) — تقييم كسول؛ استخدمه متى تضمّن البديل استدعاء تابع أو إدخالًا/إخراجًا أو إنشاء كائن.
  • orElseThrow(مورّد) — يرمي عند الفراغ؛ استخدمه لفرض الشروط المسبقة وإنتاج رسائل خطأ ذات مغزى على حدود الطبقات.