لمن هذه المقالة؟
- إنه للأشخاص الذين قرأوا الجزء الأول من هذه المقالة؛
- إنه مخصص للأشخاص الذين يعتقدون أنهم يعرفون Java Core جيدًا، ولكن ليس لديهم أدنى فكرة عن تعبيرات lambda في Java. أو ربما سمعوا شيئًا ما عن تعبيرات لامدا، لكن التفاصيل غير متوفرة.
- إنه مخصص للأشخاص الذين لديهم فهم معين لتعبيرات لامدا، لكنهم ما زالوا خائفين منها وغير معتادين على استخدامها.
الوصول إلى المتغيرات الخارجية
هل يتم تجميع هذا الرمز مع فئة مجهولة؟int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
لا، counter
يجب أن يكون المتغير final
. أو إذا لم يكن الأمر كذلك final
، فعلى الأقل لا يمكن تغيير قيمته. وينطبق نفس المبدأ في تعبيرات لامدا. يمكنهم الوصول إلى كافة المتغيرات التي يمكنهم "رؤيتها" من المكان الذي تم الإعلان عنها. لكن يجب ألا تغيرها لامدا (تعيين قيمة جديدة لها). ومع ذلك، هناك طريقة لتجاوز هذا التقييد في الفئات المجهولة. ما عليك سوى إنشاء متغير مرجعي وتغيير الحالة الداخلية للكائن. عند القيام بذلك، لا يتغير المتغير نفسه (يشير إلى نفس الكائن) ويمكن وضع علامة عليه بأمان على أنه final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
هنا counter
المتغير لدينا هو إشارة إلى AtomicInteger
كائن. ويتم استخدام الطريقة incrementAndGet()
لتغيير حالة هذا الكائن. لا تتغير قيمة المتغير نفسه أثناء تشغيل البرنامج. فهو يشير دائمًا إلى نفس الكائن، مما يتيح لنا الإعلان عن المتغير باستخدام الكلمة الأساسية النهائية. فيما يلي نفس الأمثلة، ولكن مع تعبيرات لامدا:
int counter = 0;
Runnable r = () -> counter++;
لن يتم تجميع هذا لنفس السبب مثل الإصدار الذي يحتوي على فئة مجهولة: counter
يجب ألا يتغير أثناء تشغيل البرنامج. لكن كل شيء على ما يرام إذا فعلنا ذلك على النحو التالي:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
وهذا ينطبق أيضًا على طرق الاتصال. ضمن تعبيرات lambda، لا يمكنك الوصول إلى جميع المتغيرات "المرئية" فحسب، بل يمكنك أيضًا استدعاء أي أساليب يمكن الوصول إليها.
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
على الرغم من staticMethod()
كونه خاصًا، إلا أنه يمكن الوصول إليه داخل main()
الطريقة، لذا يمكن استدعاؤه أيضًا من داخل lambda الذي تم إنشاؤه في main
الطريقة.
متى يتم تنفيذ تعبير لامدا؟
قد تجد السؤال التالي بسيطًا للغاية، ولكن يجب عليك طرحه بنفس الطريقة: متى سيتم تنفيذ الكود الموجود داخل تعبير لامدا؟ عندما يتم إنشاؤه؟ أو متى يطلق عليه (وهو ما لم يعرف بعد)؟ هذا سهل التحقق إلى حد ما.System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
إخراج الشاشة:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
يمكنك أن ترى أنه تم تنفيذ تعبير لامدا في النهاية، بعد إنشاء سلسلة المحادثات وفقط عندما يصل تنفيذ البرنامج إلى الطريقة run()
. بالتأكيد ليس عندما يتم الإعلان عنها. من خلال الإعلان عن تعبير لامدا، قمنا فقط بإنشاء Runnable
كائن ووصفنا كيفية run()
تصرف طريقته. يتم تنفيذ الطريقة نفسها في وقت لاحق بكثير.
مراجع الطريقة؟
لا ترتبط مراجع الأساليب بشكل مباشر بـ lambdas، ولكن أعتقد أنه من المنطقي أن نقول بضع كلمات عنها في هذه المقالة. لنفترض أن لدينا تعبير لامدا الذي لا يفعل أي شيء خاص، ولكنه ببساطة يستدعي طريقة.x -> System.out.println(x)
فإنه يتلقى بعض x
المكالمات فقط System.out.println()
، ويمر في x
. وفي هذه الحالة يمكننا استبدالها بالإشارة إلى الطريقة المطلوبة. مثله:
System.out::println
هذا صحيح - لا توجد أقواس في النهاية! إليك مثال أكثر اكتمالا:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
في السطر الأخير، نستخدم الطريقة forEach()
التي تأخذ كائنًا ينفذ الواجهة Consumer
. مرة أخرى، هذه واجهة وظيفية، لها void accept(T t)
طريقة واحدة فقط. وبناءً على ذلك، نكتب تعبير لامدا الذي يحتوي على معلمة واحدة (نظرًا لأنه مكتوب في الواجهة نفسها، فإننا لا نحدد نوع المعلمة؛ بل نشير فقط إلى أننا سنسميها x
). في نص تعبير لامدا، نكتب الكود الذي سيتم تنفيذه عند accept()
استدعاء الطريقة. نحن هنا نعرض ببساطة ما انتهى به الأمر في المتغير x
. تتكرر هذه forEach()
الطريقة نفسها عبر جميع العناصر الموجودة في المجموعة وتستدعي الطريقة accept()
الخاصة بتنفيذ الواجهة Consumer
(لامدا الخاصة بنا)، وتمرير كل عنصر في المجموعة. كما قلت، يمكننا استبدال تعبير لامدا هذا (الذي يصنف ببساطة طريقة مختلفة) بإشارة إلى الطريقة المطلوبة. ثم سيبدو الكود الخاص بنا كما يلي:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
الشيء الرئيسي هو أن المعلمات والأساليب println()
متطابقة accept()
. نظرًا لأن println()
الطريقة يمكن أن تقبل أي شيء (يتم تحميلها بشكل زائد لجميع أنواع العناصر الأولية وجميع الكائنات)، فبدلاً من تعبيرات lambda، يمكننا ببساطة تمرير مرجع إلى الطريقة println()
إلى forEach()
. بعد ذلك forEach()
سوف يأخذ كل عنصر في المجموعة ويمرره مباشرة إلى println()
الطريقة. لأي شخص يواجه هذا للمرة الأولى، يرجى ملاحظة أننا لا نتصل System.out.println()
(مع وجود نقاط بين الكلمات والأقواس في النهاية). بدلاً من ذلك، نقوم بتمرير إشارة إلى هذه الطريقة. إذا كتبنا هذا
strings.forEach(System.out.println());
سيكون لدينا خطأ في التجميع. قبل استدعاء forEach()
Java، ترى Java ما System.out.println()
يتم استدعاؤه، لذا فهي تدرك أن القيمة المرجعة ستحاول void
التمرير void
إلى كائن forEach()
، بدلاً من ذلك تتوقع Consumer
كائنًا.
بناء الجملة لمراجع الأسلوب
انها بسيطة جدا:-
نقوم بتمرير إشارة إلى طريقة ثابتة مثل هذا:
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
نقوم بتمرير مرجع إلى طريقة غير ثابتة باستخدام كائن موجود، مثل هذا:
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
نقوم بتمرير مرجع إلى طريقة غير ثابتة باستخدام الفئة التي تنفذها على النحو التالي:
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
نقوم بتمرير إشارة إلى مُنشئ مثل هذا:
ClassName::new
تعتبر مراجع الطريقة ملائمة جدًا عندما يكون لديك بالفعل طريقة تعمل بشكل مثالي كرد اتصال. في هذه الحالة، بدلًا من كتابة تعبير لامدا يحتوي على كود الطريقة، أو كتابة تعبير لامدا الذي يستدعي الطريقة ببساطة، نقوم ببساطة بتمرير مرجع إليها. وهذا كل شيء.
تمييز مثير للاهتمام بين الفئات المجهولة وتعبيرات لامدا
في فئة مجهولة،this
تشير الكلمة الأساسية إلى كائن من فئة مجهولة. لكن إذا استخدمنا هذا داخل لامدا، فسنتمكن من الوصول إلى كائن الفئة التي تحتوي عليه. الذي كتبنا فيه بالفعل تعبير لامدا. يحدث هذا بسبب تجميع تعبيرات لامدا في طريقة خاصة للفئة التي تمت كتابتها فيها. لا أوصي باستخدام هذه "الميزة"، نظرًا لأن لها تأثيرًا جانبيًا ويتعارض مع مبادئ البرمجة الوظيفية. ومع ذلك، فإن هذا النهج يتوافق تمامًا مع OOP. ;)
من أين حصلت على معلوماتي وماذا يجب أن تقرأ أيضًا؟
- البرنامج التعليمي على الموقع الرسمي لشركة أوراكل. الكثير من المعلومات التفصيلية، بما في ذلك الأمثلة.
- فصل حول مراجع الطريقة في نفس البرنامج التعليمي لـ Oracle.
- انغمس في ويكيبيديا إذا كنت فضوليًا حقًا.
GO TO FULL VERSION