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

تحويل قيم Optional

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

تحويل قيم Optional

في الدرس السابق تعلّمت كيف تستخرج قيمة من Optional بأمان باستخدام orElse وorElseGet وorElseThrow. لكن النمط الأكثر شيوعًا هو أنّك لا تريد استخراج القيمة على الإطلاق — بل تريد تطبيق منطق ما عليها والإبقاء على النتيجة مُغلَّفة، حتى تستمر حالة الغياب في الانتشار تلقائيًا. هذا بالضبط ما تفعله دوال map وflatMap وfilter.

الفكرة الجوهرية: تعامل مع Optional كأنّه Stream من عنصر واحد. تتيح لك دوال التحويل بناء خط معالجة متسلسل. إذا كان Optional فارغًا في أي خطوة، يُتخطّى باقي الخط تلقائيًا — دون فحوصات null، ودون جمل if.

map — تطبيق دالة على القيمة المُغلَّفة

map(Function<T, R> mapper) تُطبّق mapper إذا كانت قيمة موجودة وتُعيد Optional<R>. أما إذا كان الأصل فارغًا فتُعيد Optional.empty() دون استدعاء mapper أصلًا.

Optional<String> name = Optional.of(" Alice "); Optional<String> trimmed = name.map(String::trim); // Optional["Alice"] Optional<Integer> length = name.map(String::trim) .map(String::length); // Optional[5] Optional<String> empty = Optional.<String>empty() .map(String::toUpperCase); // Optional.empty — لم تُستدعَ mapper

قارن ذلك بالنسخة الأمريّة (الإجرائية):

// أسلوب إجرائي — عليك تذكّر فحص null في كل مكان String raw = " Alice "; Integer len = null; if (raw != null) { String t = raw.trim(); if (t != null) { len = t.length(); } }

مع map تصف التحويل؛ أما انتشار حالة الغياب فيجري تلقائيًا.

flatMap — حين تُعيد الدالة قيمة Optional بحد ذاتها

لنفترض أنّ دالة التحويل تُعيد هي الأخرى Optional. لو استخدمت map العادية ستحصل على Optional<Optional<T>> متداخل، وهذا نادرًا ما تريده. flatMap تُسطّح هذا التداخل بمستوى واحد.

public class User { private String email; // قد تكون null public Optional<String> getEmail() { return Optional.ofNullable(email); } } public class EmailService { public static Optional<String> normalize(String email) { if (email == null || !email.contains("@")) return Optional.empty(); return Optional.of(email.toLowerCase().trim()); } } Optional<User> userOpt = Optional.of(new User(" Bob@Example.com ")); // خاطئ — ينتج Optional<Optional<String>> Optional<Optional<String>> nested = userOpt.map(User::getEmail); // صحيح — flatMap تُسطّح التداخل تلقائيًا Optional<String> email = userOpt .flatMap(User::getEmail) // Optional[" Bob@Example.com "] .flatMap(EmailService::normalize); // Optional["bob@example.com"]
قاعدة عملية: استخدم map حين تُعيد دالتك قيمة عادية. استخدم flatMap حين تُعيد دالتك Optional بالفعل. إذا رأيت Optional<Optional<...>> في IDE الخاص بك، الحل هو flatMap.

filter — إفراغ Optional بشرط

filter(Predicate<T> predicate) تُبقي على القيمة إذا أعادت الشرط true؛ وإلا تُعيد Optional.empty(). لن يُرمى أي استثناء — إذا كان Optional فارغًا أصلًا لا تُستدعى الدالة الشرطية.

Optional<Integer> score = Optional.of(72); Optional<Integer> passing = score.filter(s -> s >= 60); // Optional[72] Optional<Integer> perfect = score.filter(s -> s == 100); // Optional.empty // تسلسل filter مع map Optional<String> grade = Optional.of(85) .filter(s -> s >= 0 && s <= 100) // التحقق من النطاق .map(s -> s >= 90 ? "A" : s >= 80 ? "B" : "C"); // Optional["B"]

بناء خط معالجة حقيقي

تتكامل الدوال الثلاث بسلاسة. إليك مثالًا واقعيًا: قراءة إعداد مستخدم من خريطة ضبط، وتحويله إلى عدد صحيح بعد التحقق منه، ثم تقييده ضمن نطاق مقبول.

import java.util.Map; import java.util.Optional; Map<String, String> config = Map.of("timeout", "45", "retries", "abc"); // الخط: جلب -> تحليل بأمان -> تحقق -> تقييد Optional<Integer> timeout = Optional.ofNullable(config.get("timeout")) .map(String::trim) .flatMap(v -> { try { return Optional.of(Integer.parseInt(v)); } catch (NumberFormatException e) { return Optional.empty(); } }) .filter(t -> t > 0) .map(t -> Math.min(t, 120)); // تقييد بحد أقصى 120 ثانية System.out.println(timeout); // Optional[45] Optional<Integer> retries = Optional.ofNullable(config.get("retries")) .map(String::trim) .flatMap(v -> { try { return Optional.of(Integer.parseInt(v)); } catch (NumberFormatException e) { return Optional.empty(); } }) .filter(r -> r > 0) .map(r -> Math.min(r, 10)); System.out.println(retries); // Optional.empty ("abc" فشل في التحليل)

لاحظ غياب أي if (x != null) في هذا الكود. يُعالج المسار الكامل للحالات الغائبة أو غير الصالحة بشكل هيكلي.

الفارق عن عمليات Stream

Optional.map تتصرف كـStream.map على تدفق من عنصر واحد على الأكثر. أحد الفوارق العملية: إذا أعادت دالة map الخاصة بك null، فإن Optional.map تحوّل النتيجة بصمت إلى Optional.empty() بدلًا من تغليف null. هذا متعمَّد — يمنع إعادة إدخال القيم الفارغة التي تحاول تجنّبها.

Optional<String> result = Optional.of("hello") .map(s -> (String) null); // الدالة تُعيد null System.out.println(result); // Optional.empty — وليس Optional[null]
لا تستخدم map/flatMap للآثار الجانبية فقط. إذا وجدت نفسك تكتب optional.map(x -> { doSomething(x); return x; }) لتشغيل أثر جانبي، استخدم ifPresent أو ifPresentOrElse بدلًا من ذلك. دوال التحويل تعبّر عن نية واضحة: "أنتج قيمة جديدة." تهريب آثار جانبية فيها يُربك القارئ وقد يُدخل أخطاء خفيّة.

or() — توفير Optional احتياطي (Java 9+)

أحيانًا تريد تجربة مصدر آخر حين يكون Optional الأول فارغًا، والمصدر الثاني هو الآخر Optional. or(Supplier<Optional<T>>) تُغطّي هذه الحالة بنظافة دون الحاجة إلى تحايلات flatMap:

Optional<String> primary = Optional.empty(); Optional<String> secondary = Optional.of("fallback"); Optional<String> result = primary.or(() -> secondary); System.out.println(result); // Optional[fallback]

هذا أكثر أمانًا من primary.orElseGet(() -> secondary.orElse(null))، الذي يستخرج القيمة مبكرًا ويفقد سياق Optional.

الخلاصة

استخدم map لتحويل قيمة موجودة والبقاء داخل Optional. استخدم flatMap حين يُعيد التحويل بحد ذاته Optional، لتجنّب التداخل. استخدم filter لاستبعاد القيم التي لا تُحقق شرطًا معينًا. سلسلها معًا لكتابة خطوط معالجة مقروءة وآمنة من القيم الفارغة. في الدرس القادم ستتعرف على الأنماط الشائعة التي تتألق فيها هذه الدوال — والأنماط المضادة التي يُساء فيها استخدامها.