سلوك يمكنك تمريره
تعبير لامدا هو كتلة مدمجة من الكود يمكنك تسليمها إلى دالة، أو تخزينها في متغير، أو إرجاعها، تماماً كما تفعل مع رقم أو نص. قبل ظهور لامدا، كان تمرير السلوك يعني كتابة صنف كامل (أو صنف مجهول مطوّل) لمجرد تغليف دالة واحدة. تختزل لامدا ذلك إلى جوهره: المعاملات والجسم.
تنفّذ لامدا دائماً واجهة وظيفية، أي واجهة تحتوي على دالة مجردة واحدة بالضبط (وقد تعرّفت عليها في نهاية صفحة الواجهات). يستنتج المترجم من السياق أي واجهة تقصد، ثم يربط لامدا الخاصة بك بتلك الدالة الوحيدة.
x -> x * 2 هو التنفيذ الكامل للدالة apply. لا new، ولا جسم صنف، ولا اسم دالة، فكل ذلك توفّره الواجهة.
صياغة السهم
تتخذ كل لامدا الشكل المعاملات -> الجسم. وتتكيّف الأجزاء حسب ما تحتاج إليه:
() -> 42 // بلا معاملات
x -> x + 1 // معامل واحد، الأقواس اختيارية
(x, y) -> x + y // معاملان أو أكثر يتطلبان أقواساً
(int x, int y) -> x + y // الأنواع اختيارية - تُستنتج عادةً
x -> { // جسم الكتلة يتطلب أقواساً معقوفة و return
int doubled = x * 2;
return doubled + 1;
}
جسم التعبير الواحد (x -> x + 1) يُرجِع قيمته ضمنياً، دون الكلمة المفتاحية return. وفي اللحظة التي تستخدم فيها الأقواس المعقوفة، تكون تكتب كتلة عادية، ويجب عليك استخدام return صراحةً إذا كانت الدالة تُرجع شيئاً. ومن الأخطاء الشائعة خلط الأمرين: x -> { x + 1 } لا يُترجَم، لأن الكتلة تتطلب عبارة (return x + 1;).
لامدا تحل محل الأصناف المجهولة
أوضح طريقة لإدراك ما تقدّمه لامدا هي المقارنة بين قبل وبعد. كان الترتيب باستخدام Comparator مخصّص يبدو هكذا فيما مضى:
// قبل - صنف مجهول
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
والشيء نفسه بصيغة لامدا يصبح سطراً واحداً سهل القراءة:
Comparator واجهة وظيفية (دالتها المجردة الوحيدة هي compare)، لذا تنطبق لامدا في مكانها مباشرةً. وتختفي كل المراسم، أي new والصنف وتوقيع الدالة، فلا يبقى سوى منطق المقارنة.
صندوق أدوات java.util.function
نادراً ما تحتاج إلى التصريح بواجهة وظيفية خاصة بك. فحزمة java.util.function تأتي بالأشكال الشائعة، وتقبلها تقريباً جميع واجهات مكتبة جافا البرمجية:
Function<T, R>- تأخذTوتُرجعR(apply)Predicate<T>- تأخذTوتُرجعboolean(test)Supplier<T>- لا تأخذ شيئاً وتُرجعT(get)Consumer<T>- تأخذTولا تُرجع شيئاً (accept)
هذه واجهات معمّمة (generic)؛ فـ Function<String, Integer> يعيد استخدام المعمّمات التي رأيتها في الصفحة السابقة للحفاظ على أمان الأنواع. اختر تلك التي يطابق شكلها ما يحتاج الكود إلى استقباله وإنتاجه.
مراجع الدوال
عندما لا تفعل لامدا شيئاً سوى استدعاء دالة واحدة موجودة مسبقاً، يمكنك استبدالها بـ مرجع دالة باستخدام ::. إنها القيمة نفسها، لكن مكتوبة بصورة أكثر مباشرة:
تأتي مراجع الدوال بأنواع متعددة: String::toUpperCase (دالة نسخة تُستدعى على كل معامل)، وMath::max (دالة ثابتة)، وSystem.out::println (دالة على كائن محدّد)، وArrayList::new (باني). لا تلجأ إلى أحدها إلا عندما يُقرأ بوضوح؛ فإذا اضطررت إلى التفكير في أي صيغة تنطبق، فإن لامدا عادية تفي بالغرض.
التقاط المتغيرات
يمكن للامدا أن تستخدم المتغيرات المحلية من الدالة المحيطة بها، لكن فقط إذا كانت نهائية (final) أو نهائية فعلياً، أي أُسندت مرة واحدة ولم تتغير أبداً. تلتقط جافا القيمة في اللحظة التي تُنشأ فيها لامدا، لذا فإن متغيراً قد يتغير لاحقاً سيكون ملتبساً.
إذا أعدت إسناد factor في أي موضع، يرفض المترجم لامدا برسالة "variable used in lambda expression should be final or effectively final". وحين تحتاج فعلاً إلى حالة قابلة للتعديل ومشتركة، التقط كائناً بدلاً من ذلك، مثل حقل أو عنصر مصفوفة أو AtomicInteger، لأن المرجع يبقى ثابتاً حتى وإن تغيّر محتواه. ولاحظ أن لامدا، بخلاف الأصناف المجهولة، لا تنشئ نطاقاً خاصاً بها: فكلمة this داخل لامدا تشير إلى النسخة المحيطة، لا إلى لامدا نفسها.
التالي: Streams
لامدا هي اللبنة الأساسية، لا الوجهة النهائية. ويظهر مردودها الحقيقي مع واجهة Streams، حيث تسلسل عمليات مثل filter وmap وreduce، تأخذ كل منها لامدا، لتعبّر عن تحويلات البيانات في صورة سلسلة معالجة (pipeline) سهلة القراءة بدلاً من تشابك الحلقات. وهذا موضوع الصفحة التالية.
الأسئلة الشائعة
ما هو تعبير لامدا في جافا؟
لامدا هي طريقة مختصرة لكتابة نسخة من واجهة وظيفية، أي واجهة تحتوي على دالة مجردة واحدة فقط. فبدلاً من كتابة صنف مجهول كامل، تكتب المعاملات -> الجسم. يربط المترجم تعبير لامدا بالدالة الوحيدة في الواجهة، لذا فإن Runnable r = () -> System.out.println("hi"); يمثل كائن Runnable كاملاً. تتيح لك لامدا تمرير السلوك تماماً كما تمرّر البيانات.
ما الفرق بين لامدا ومرجع الدالة في جافا؟
ينتج كلاهما نسخة من واجهة وظيفية. تحتوي لامدا على معاملات صريحة وجسم (s -> s.toUpperCase())، بينما مرجع الدالة هو اختصار للامدا لا تفعل سوى استدعاء دالة واحدة موجودة مسبقاً (String::toUpperCase). استخدم مرجع دالة عندما لا تفعل لامدا شيئاً سوى تمرير معاملاتها إلى دالة واحدة محدّدة بالاسم، فهو أقصر وأوضح في القراءة.
لماذا يجب أن تكون المتغيرات المستخدمة في لامدا في جافا نهائية (final) أو نهائية فعلياً؟
يمكن للامدا أن تلتقط متغيرات محلية من الدالة المحيطة بها، لكن فقط إذا لم تتغير أبداً بعد إسنادها، وهذا ما يعنيه مصطلح "نهائية فعلياً". تلتقط جافا القيمة لا مرجعاً حياً للمتغير، لذا فإن السماح بإعادة الإسناد سيكون ملتبساً وغير آمن عبر الخيوط (threads). إذا احتجت إلى حالة قابلة للتعديل ومشتركة، فاستخدم بدلاً من ذلك حقلاً أو غلافاً مثل مصفوفة أو AtomicInteger.