صياغة Lambda
في الدرس السابق رأيت كيف تُتيح لك الفئات المجهولة تمرير السلوك كقيمة. تعبيرات Lambda هي طريقة أنظف للتعبير عن الفكرة ذاتها. يتناول هذا الدرس كل جزء من الصياغة: كيفية كتابة قوائم المعاملات، ومتى يمكن حذف الأقواس وتعليقات النوع، والفرق بين جسم التعبير وجسم الكتلة.
شكل تعبير Lambda
يتكوّن تعبير Lambda من ثلاثة أجزاء مفصولة برمز السهم ->:
(المعاملات) -> الجسم
الجانب الأيسر يُعلن عمّا يستقبله التعبير. الجانب الأيمن يُعلن عمّا يفعله. لا نوع إرجاع، ولا مُحدِّد وصول، ولا اسم.
قوائم المعاملات
صياغة المعاملات مرنة. القواعد هي:
- بلا معاملات — اكتب أقواساً فارغة:
() -> ...
- معامل واحد — الأقواس اختيارية إذا حذفت النوع:
x -> ... أو (x) -> ...
- معاملان أو أكثر — الأقواس إلزامية:
(a, b) -> ...
- أنواع صريحة — الأقواس إلزامية:
(String s) -> ...
// بلا معاملات
Runnable r = () -> System.out.println("Hello");
// معامل واحد — لا حاجة للأقواس
java.util.function.Consumer<String> greet = name -> System.out.println("Hi " + name);
// معاملان
java.util.function.BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// أنواع صريحة (نادرة لكن صحيحة)
java.util.function.BiFunction<Integer, Integer, Integer> multiply = (Integer x, Integer y) -> x * y;
قاعدة الاتساق: إذا كتبت النوع لأحد المعاملات فيجب أن تكتبه لجميعها. لا يمكن الخلط بين المعاملات المُنوَّعة وغير المُنوَّعة في Lambda واحد.
استنتاج النوع
في معظم تعبيرات Lambda العملية تحذف أنواع المعاملات. يستنتجها المُترجم من النوع المستهدف — الواجهة الوظيفية التي يُسند إليها التعبير أو يُمرَّر إليها. هذا هو الاستنتاج ذاته الذي يستخدمه Java مع var واستدعاءات الدوال العامة.
import java.util.List;
import java.util.function.Predicate;
List<String> names = List.of("Alice", "Bob", "Charlie");
// يعرف المُترجم أن filter() تتوقع Predicate<String>،
// فيستنتج أن 's' من نوع String.
// تكتب: s -> s.length() > 4
// لا: (String s) -> s.length() > 4
names.stream()
.filter(s -> s.length() > 4)
.forEach(System.out::println);
عند وجود التباس حقيقي — مثلاً حين تكون الدالة مُحمَّلة بزيادة مع واجهات وظيفية غير متوافقة — يطلب منك المُترجم إضافة أنواع صريحة للحل. هذا الوضع نادر.
جسم التعبير
إذا كان جسم Lambda تعبيراً واحداً، فنتيجة ذلك التعبير هي قيمة الإرجاع الضمنية. لا أقواس معقوفة، ولا كلمة return.
import java.util.function.Function;
// جسم تعبير — نظيف ومقروء
Function<String, Integer> lengthOf = s -> s.length();
System.out.println(lengthOf.apply("hello")); // 5
تتألق أجسام التعبير في التحويلات الصغيرة والمقارنات والمُحدِّدات. حافظ عليها كتعابير كلما ناسب المنطق سطراً واحداً.
جسم الكتلة
حين تحتاج أكثر من جملة واحدة — متغيرات محلية، شروط، حلقات — لُفّ الجسم بأقواس معقوفة. يجب عندئذٍ استخدام جملة return صريحة للـ Lambda غير المُرجِعة للـ void.
import java.util.function.Function;
Function<String, String> titleCase = s -> {
if (s == null || s.isEmpty()) {
return s;
}
return Character.toUpperCase(s.charAt(0)) + s.substring(1).toLowerCase();
};
System.out.println(titleCase.apply("jAVA")); // Java
افضل أجسام التعبير. جسم الكتلة الأطول من ثلاثة أو أربعة أسطر يعني عادةً أن المنطق يستحق دالة خاصة مُسمَّاة. يمكنك حينئذٍ الإشارة إليها بمرجع دالة، مما يُبقي موقع الاستدعاء نظيفاً والمنطق قابلاً للاختبار بمعزل.
الإرجاع داخل جسم الكتلة
من الأخطاء الشائعة نسيان أن return داخل Lambda يخرج من Lambda لا من الدالة المُحيطة.
import java.util.List;
void printLongNames(List<String> names) {
names.forEach(name -> {
if (name.length() <= 3) {
return; // يخرج من Lambda لهذا العنصر فقط، لا من printLongNames()
}
System.out.println(name);
});
System.out.println("Done"); // يُنفَّذ دائماً
}
التعابير المتوافقة مع void: يمكن استخدام تعبير يُنتج قيمة كجسم لـ Lambda مُرجِع لـ void. مثلاً، list.add(item) تُرجع boolean، لكن يمكنك استخدامها كجسم لـ Lambda من نوع Consumer<T> — يتجاهل Java قيمة الإرجاع ببساطة. يُسمّي المُترجم هذا تعبيراً متوافقاً مع void.
تجميع المفاهيم: مثال الترتيب
Comparator إحدى أقدم الواجهات الوظيفية في Java. ترتيب قائمة حسب طول النص يُبيّن كل صيغ الصياغة جنباً إلى جنب:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
List<String> words = new ArrayList<>(List.of("banana", "fig", "apple", "kiwi"));
// جسم كتلة مع أنواع صريحة
words.sort((String a, String b) -> {
return Integer.compare(a.length(), b.length());
});
// جسم تعبير — أنواع مستنتَجة
words.sort((a, b) -> Integer.compare(a.length(), b.length()));
// أقصر مع دالة مصنع
words.sort(Comparator.comparingInt(String::length));
System.out.println(words); // [fig, kiwi, apple, banana]
الخلاصة
تدور صياغة Lambda حول ثلاثة خيارات: عدد المعاملات (وهل تكتب أنواعها)، واستخدام جسم التعبير أو جسم الكتلة، وهل تضع أقواساً حول معامل واحد غير مُنوَّع. يستنتج المُترجم الأنواع من الواجهة الوظيفية المستهدفة، لذا نادراً ما تحتاج لتهجئتها. استخدم أجسام التعبير للمنطق الموجز، وأجسام الكتلة حين تحتاج جملات متعددة — وفكّر في استخراج أجسام الكتلة الطويلة إلى دوال مُسمَّاة.