لمن هذه المقالة؟
يمكنك قراءة المزيد في هذا البرنامج التعليمي لـ Oracle
وفي أي مكان آخر. وهذا ما يسمى " الكتابة المستهدفة ". يمكنك تسمية المتغيرات كما تريد — ليس عليك استخدام نفس الأسماء المحددة في الواجهة. إذا لم تكن هناك معلمات، فما عليك سوى الإشارة إلى الأقواس الفارغة. إذا كان هناك معلمة واحدة فقط، فما عليك سوى الإشارة إلى اسم المتغير دون أي قوسين. الآن بعد أن فهمنا المعلمات، حان الوقت لمناقشة نص تعبير لامدا. داخل الأقواس المتعرجة، تكتب التعليمات البرمجية تمامًا كما تفعل مع الطريقة العادية. إذا كان الكود الخاص بك يتكون من سطر واحد، فيمكنك حذف الأقواس المتعرجة بالكامل (على غرار عبارات if وfor-loops). إذا أعادت lambda ذات السطر الواحد شيئًا ما، فلن يتعين عليك تضمين عبارة
- إنه مخصص للأشخاص الذين يعتقدون أنهم يعرفون Java Core جيدًا، ولكن ليس لديهم أدنى فكرة عن تعبيرات lambda في Java. أو ربما سمعوا شيئًا ما عن تعبيرات لامدا، لكن التفاصيل غير متوفرة
- إنه مخصص للأشخاص الذين لديهم فهم معين لتعبيرات لامدا، لكنهم ما زالوا خائفين منها وغير معتادين على استخدامها.
-
يجب أن تفهم البرمجة الشيئية (OOP)، وهي:
- الطبقات، والأشياء، والفرق بينهما؛
- الواجهات، وكيف تختلف عن الفئات، والعلاقة بين الواجهات والفئات؛
- الأساليب، وكيفية استدعائها، والأساليب المجردة (أي الأساليب بدون تطبيق)، ومعلمات الطريقة، ووسائط الطريقة وكيفية تمريرها؛
- معدّلات الوصول، والأساليب/المتغيرات الثابتة، والأساليب/المتغيرات النهائية؛
- وراثة الفئات والواجهات، وراثة متعددة للواجهات.
- معرفة Java Core: الأنواع العامة (الأسماء العامة)، والمجموعات (القوائم)، والخيوط.
قليلا من التاريخ
جاءت تعبيرات لامدا إلى جافا من البرمجة الوظيفية، وإلى هناك من الرياضيات. في الولايات المتحدة في منتصف القرن العشرين، عملت كنيسة ألونزو، التي كانت مغرمة جدًا بالرياضيات وجميع أنواع التجريدات، في جامعة برينستون. كان ألونزو تشيرش هو من اخترع حساب التفاضل والتكامل لامدا، والذي كان في البداية مجموعة من الأفكار المجردة التي لا علاقة لها بالبرمجة على الإطلاق. عمل علماء الرياضيات مثل آلان تورينج وجون فون نيومان في جامعة برينستون في نفس الوقت. اجتمع كل شيء معًا: توصلت الكنيسة إلى حساب التفاضل والتكامل لامدا. طور تورينج آلة الحوسبة التجريدية الخاصة به، والتي تُعرف الآن باسم "آلة تورينج". واقترح فون نيومان بنية حاسوبية شكلت أساس أجهزة الكمبيوتر الحديثة (وتسمى الآن "بنية فون نيومان"). في ذلك الوقت، لم تكن أفكار ألونزو تشيرش مشهورة مثل أعمال زملائه (باستثناء مجال الرياضيات البحتة). ومع ذلك، بعد فترة وجيزة، أصبح جون مكارثي (وهو أيضًا خريج جامعة برينستون، وفي وقت قصتنا، موظفًا في معهد ماساتشوستس للتكنولوجيا) مهتمًا بأفكار تشيرش. وفي عام 1958، ابتكر أول لغة برمجة وظيفية، LISP، بناءً على تلك الأفكار. وبعد 58 عامًا، تسربت أفكار البرمجة الوظيفية إلى Java 8. ولم يمر حتى 70 عامًا... بصراحة، هذه ليست المدة الأطول التي يستغرقها تطبيق فكرة رياضية عمليًا.لب الموضوع
تعبير لامدا هو نوع من الوظائف. يمكنك اعتبارها طريقة Java عادية ولكن مع القدرة المميزة على التمرير إلى طرق أخرى كوسيطة. صحيح. لقد أصبح من الممكن تمرير ليس فقط الأرقام والسلاسل والقطط إلى الطرق، بل أيضًا إلى طرق أخرى! متى قد نحتاج إلى هذا؟ سيكون من المفيد، على سبيل المثال، إذا أردنا تمرير بعض أساليب رد الاتصال. أي أننا إذا أردنا أن يكون لدى الطريقة التي نسميها القدرة على استدعاء طريقة أخرى نمررها إليها. بمعنى آخر، لدينا القدرة على تمرير رد اتصال واحد في ظل ظروف معينة، ورد اتصال مختلف في ظروف أخرى. وبالتالي فإن طريقتنا التي تتلقى عمليات الاسترجاعات الخاصة بنا تستدعيها. الفرز هو مثال بسيط. لنفترض أننا نكتب بعض خوارزميات الفرز الذكية التي تبدو كما يلي:public void mySuperSort() {
// We do something here
if(compare(obj1, obj2) > 0)
// And then we do something here
}
في if
البيان، نسمي compare()
الطريقة، ونمرر كائنين للمقارنة، ونريد أن نعرف أي من هذه الكائنات "أكبر". نحن نفترض أن الواحد "الأكبر" يأتي قبل "الأصغر". لقد وضعت "أكبر" بين علامتي الاقتباس، لأننا نكتب طريقة عالمية ستعرف كيفية الفرز ليس فقط بترتيب تصاعدي، ولكن أيضًا بترتيب تنازلي (في هذه الحالة، سيكون الكائن "الأكبر" في الواقع هو الكائن "الأصغر" ، والعكس). لتعيين خوارزمية محددة لنوعنا، نحتاج إلى بعض الآليات لتمريرها إلى طريقتنا mySuperSort()
. بهذه الطريقة سنكون قادرين على "التحكم" في طريقتنا عند استدعائها. بالطبع، يمكننا كتابة طريقتين منفصلتين — mySuperSortAscend()
و mySuperSortDescend()
— للفرز بترتيب تصاعدي وتنازلي. أو يمكننا تمرير بعض الوسيطات إلى الطريقة (على سبيل المثال، متغير منطقي؛ إذا كان صحيحًا، فسيتم الفرز بترتيب تصاعدي، وإذا كان خطأ، فسيتم الترتيب تنازليًا). ولكن ماذا لو أردنا فرز شيء معقد مثل قائمة من صفائف السلسلة؟ كيف mySuperSort()
ستعرف طريقتنا كيفية فرز صفائف السلسلة هذه؟ حسب الحجم؟ من خلال الطول التراكمي لجميع الكلمات؟ ربما يعتمد أبجديًا على السلسلة الأولى في المصفوفة؟ وماذا لو أردنا فرز قائمة المصفوفات حسب حجم المصفوفة في بعض الحالات، وحسب الطول التراكمي لجميع الكلمات في كل مصفوفة في حالات أخرى؟ أتوقع أنك سمعت بالفعل عن المقارنات وأنه في هذه الحالة سنقوم ببساطة بتمرير كائن مقارنة يصف خوارزمية الفرز المطلوبة إلى طريقة الفرز الخاصة بنا. نظرًا لأنه يتم تنفيذ الطريقة القياسية sort()
بناءً على نفس المبدأ mySuperSort()
، فسوف أستخدمه sort()
في الأمثلة الخاصة بي.
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
Comparator<;String[]> sortByLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
};
Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
int length1 = 0;
int length2 = 0;
for (String s : o1) {
length1 += s.length();
}
for (String s : o2) {
length2 += s.length();
}
return length1 - length2;
}
};
arrays.sort(sortByLength);
نتيجة:
- Dota GTA5 Halo
- if then else
- I really love Java
هنا يتم فرز المصفوفات حسب عدد الكلمات في كل مصفوفة. تعتبر المصفوفة التي تحتوي على عدد أقل من الكلمات "أقل". ولهذا السبب يأتي أولاً. تعتبر المصفوفة التي تحتوي على عدد أكبر من الكلمات "أكبر" ويتم وضعها في النهاية. إذا مررنا مقارنة مختلفة للطريقة sort()
، مثل sortByCumulativeWordLength
، فسنحصل على نتيجة مختلفة:
- if then else
- Dota GTA5 Halo
- I really love Java
الآن يتم فرز المصفوفات حسب إجمالي عدد الأحرف في كلمات المصفوفة. في المصفوفة الأولى، هناك 10 أحرف، وفي الثانية - 12، وفي الثالثة - 15. إذا كان لدينا مقارنة واحدة فقط، فلن يتعين علينا الإعلان عن متغير منفصل لها. بدلًا من ذلك، يمكننا ببساطة إنشاء فئة مجهولة في وقت استدعاء الطريقة sort()
. شيء من هذا القبيل:
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};
List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
سنحصل على نفس النتيجة كما في الحالة الأولى. المهمة 1. أعد كتابة هذا المثال بحيث يقوم بفرز المصفوفات ليس بترتيب تصاعدي لعدد الكلمات في كل مصفوفة، ولكن بترتيب تنازلي. نحن نعرف كل هذا بالفعل. نحن نعرف كيفية تمرير الكائنات إلى الأساليب. اعتمادًا على ما نحتاجه في الوقت الحالي، يمكننا تمرير كائنات مختلفة إلى إحدى الطرق، والتي ستقوم بعد ذلك باستدعاء الطريقة التي قمنا بتنفيذها. وهذا يطرح السؤال: لماذا نحتاج في العالم إلى تعبير لامدا هنا؟ لأن تعبير لامدا هو كائن له طريقة واحدة بالضبط. مثل "كائن الأسلوب". طريقة معبأة في كائن. إنه يحتوي فقط على بناء جملة غير مألوف قليلاً (ولكن المزيد عن ذلك لاحقًا). دعونا نلقي نظرة أخرى على هذا الكود:
arrays.sort(new Comparator<String[]>() {
@Override
public int compare(String[] o1, String[] o2) {
return o1.length - o2.length;
}
});
نحن هنا نأخذ قائمة المصفوفات الخاصة بنا ونستدعي طريقتها sort()
، والتي نمرر إليها كائن مقارنة بطريقة واحدة compare()
(اسمها لا يهم بالنسبة لنا - ففي نهاية المطاف، إنها الطريقة الوحيدة لهذا الكائن، لذا لا يمكن أن نخطئ). تحتوي هذه الطريقة على معلمتين سنعمل معهم. إذا كنت تعمل في IntelliJ IDEA، فمن المحتمل أنك رأيت أنه يعرض تكثيف التعليمات البرمجية بشكل كبير على النحو التالي:
arrays.sort((o1, o2) -> o1.length - o2.length);
يؤدي هذا إلى تقليل ستة أسطر إلى سطر واحد قصير. تمت إعادة كتابة 6 أسطر في سطر واحد قصير. لقد اختفى شيء ما، لكني أضمن أنه لم يكن شيئًا مهمًا. سيعمل هذا الرمز تمامًا بنفس الطريقة التي يعمل بها مع فئة مجهولة. المهمة 2. خمن إعادة كتابة الحل للمهمة 1 باستخدام تعبير لامدا (على الأقل، اطلب من IntelliJ IDEA تحويل فصلك المجهول إلى تعبير لامدا).
دعونا نتحدث عن الواجهات
من حيث المبدأ، الواجهة هي ببساطة قائمة من الأساليب المجردة. عندما نقوم بإنشاء فصل ينفذ واجهة ما، يجب على فصلنا تنفيذ الطرق المضمنة في الواجهة (أو يتعين علينا جعل الفصل مجردًا). توجد واجهات تحتوي على الكثير من الأساليب المختلفة (على سبيل المثال،List
)، وهناك واجهات تحتوي على أسلوب واحد فقط (على سبيل المثال، Comparator
أو Runnable
). هناك واجهات لا تحتوي على طريقة واحدة (ما يسمى بواجهات العلامات مثل Serializable
). تسمى الواجهات التي لها أسلوب واحد فقط بالواجهات الوظيفية . وفي Java 8، تم تمييزها بتعليق توضيحي خاص: @FunctionalInterface
. هذه الواجهات ذات الطريقة الواحدة مناسبة كأنواع مستهدفة لتعبيرات لامدا. كما قلت أعلاه، تعبير لامدا هو طريقة ملفوفة في كائن. وعندما نمرر مثل هذا الجسم، فإننا نمرر هذه الطريقة فقط. اتضح أننا لا نهتم بما تسمى الطريقة. الشيء الوحيد الذي يهمنا هو معلمات الطريقة وبالطبع جسم الطريقة. في جوهره، تعبير لامدا هو تنفيذ واجهة وظيفية. أينما نرى واجهة ذات طريقة واحدة، يمكن إعادة كتابة فئة مجهولة على شكل لامدا. إذا كانت الواجهة تحتوي على أكثر أو أقل من أسلوب واحد، فلن يعمل تعبير لامدا وسنستخدم بدلاً من ذلك فئة مجهولة أو حتى مثيلًا لفئة عادية. حان الوقت الآن للتعمق في معلومات لامدا قليلاً. :)
بناء الجملة
بناء الجملة العام هو شيء من هذا القبيل:(parameters) -> {method body}
أي أن الأقواس تحيط بمعلمات الطريقة، و"السهم" (الذي يتكون من واصلة وعلامة أكبر من)، ثم نص الطريقة بين قوسين، كما هو الحال دائمًا. تتوافق المعلمات مع تلك المحددة في طريقة الواجهة. إذا كان من الممكن تحديد أنواع المتغيرات بشكل لا لبس فيه بواسطة المترجم (في حالتنا، فهو يعرف أننا نعمل مع مصفوفات السلسلة، لأن كائننا List
مكتوب باستخدام String[
])، فلن يتعين عليك الإشارة إلى أنواعها.
إذا كانت غامضة، فحدد النوع. ستقوم IDEA بتلوينها باللون الرمادي إذا لم تكن هناك حاجة إليها. |
return
. ولكن إذا كنت تستخدم الأقواس المتعرجة، فيجب عليك تضمين return
عبارة بشكل صريح، تمامًا كما تفعل في الطريقة العادية.
أمثلة
مثال 1.() -> {}
أبسط مثال. والأكثر لا معنى له :)، لأنه لا يفعل أي شيء. مثال 2.
() -> ""
مثال آخر مثير للاهتمام. لا يأخذ شيئًا ويعيد سلسلة فارغة ( return
تم حذفه لأنه غير ضروري). وهنا نفس الشيء، ولكن مع return
:
() -> {
return "";
}
مثال 3. "مرحبا بالعالم!" باستخدام لامدا
() -> System.out.println("Hello, World!")
لا يأخذ شيئًا ولا يُرجع شيئًا (لا يمكننا وضعه return
قبل استدعاء System.out.println()
، لأن println()
نوع الإرجاع الخاص بالأسلوب هو void
). يعرض ببساطة التحية. هذا مثالي لتنفيذ الواجهة Runnable
. المثال التالي أكثر اكتمالا:
public class Main {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello, World!")).start();
}
}
او مثل هذا:
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println("Hello, World!"));
t.start();
}
}
أو يمكننا حتى حفظ تعبير لامدا ككائن Runnable
ثم تمريره إلى Thread
المُنشئ:
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("Hello, World!");
Thread t = new Thread(runnable);
t.start();
}
}
دعونا نلقي نظرة فاحصة على اللحظة التي يتم فيها حفظ تعبير لامدا في متغير. Runnable
تخبرنا الواجهة أن كائناتها يجب أن يكون لها طريقة public void run()
. وفقًا للواجهة، run
لا تأخذ الطريقة أي معلمات. ولا يُرجع شيئًا، أي أن نوع الإرجاع الخاص به هو void
. وفقًا لذلك، سيقوم هذا الكود بإنشاء كائن بطريقة لا تأخذ أو تعيد أي شيء. وهذا يتوافق تمامًا مع طريقة Runnable
الواجهة run()
. ولهذا السبب تمكنا من وضع تعبير لامدا هذا في Runnable
متغير. مثال 4.
() -> 42
مرة أخرى، لا يتطلب الأمر شيئًا، ولكنه يُرجع الرقم 42. يمكن وضع تعبير لامدا هذا في متغير Callable
، لأن هذه الواجهة تحتوي على طريقة واحدة فقط تبدو كالتالي:
V call(),
أين V
هو نوع الإرجاع (في حالتنا، int
). وبناء على ذلك، يمكننا حفظ تعبير لامدا على النحو التالي:
Callable<Integer> c = () -> 42;
مثال 5. تعبير لامدا يتضمن عدة أسطر
() -> {
String[] helloWorld = {"Hello", "World!"};
System.out.println(helloWorld[0]);
System.out.println(helloWorld[1]);
}
مرة أخرى، هذا تعبير لامدا بدون معلمات ونوع void
إرجاع (نظرًا لعدم وجود return
عبارة). مثال 6
x -> x
هنا نأخذ x
متغير ونعيده. يرجى ملاحظة أنه إذا كان هناك معلمة واحدة فقط، فيمكنك حذف الأقواس المحيطة بها. وهنا نفس الشيء، ولكن مع قوسين:
(x) -> x
وإليك مثال مع عبارة إرجاع صريحة:
x -> {
return x;
}
أو مثل هذا مع الأقواس وبيان الإرجاع:
(x) -> {
return x;
}
أو مع إشارة صريحة إلى النوع (وبالتالي بين قوسين):
(int x) -> x
مثال 7
x -> ++x
نحن نأخذها x
ونعيدها، ولكن فقط بعد إضافة 1. يمكنك إعادة كتابة لامدا على النحو التالي:
x -> x + 1
في كلتا الحالتين، نحذف الأقواس حول المعلمة ونص الطريقة، بالإضافة إلى return
العبارة، لأنها اختيارية. يتم تقديم الإصدارات ذات الأقواس وبيان الإرجاع في المثال 6. المثال 8
(x, y) -> x % y
نحن نأخذ x
ونعيد y
باقي القسمة x
على y
. الأقواس حول المعلمات مطلوبة هنا. وهي اختيارية فقط عندما يكون هناك معلمة واحدة فقط. وهنا مع إشارة صريحة إلى الأنواع:
(double x, int y) -> x % y
مثال 9
(Cat cat, String name, int age) -> {
cat.setName(name);
cat.setAge(age);
}
نحن نأخذ Cat
كائنًا واسمًا String
وعمرًا صحيحًا. في الطريقة نفسها، نستخدم الاسم والعمر اللذين تم تمريرهما لتعيين المتغيرات على القطة. نظرًا لأن cat
الكائن الخاص بنا هو نوع مرجعي، فسيتم تغييره خارج تعبير لامدا (سيحصل على الاسم والعمر الذي تم تمريره). إليك إصدار أكثر تعقيدًا قليلًا يستخدم لامدا مشابهًا:
public class Main {
public static void main(String[] args) {
// Create a cat and display it to confirm that it is "empty"
Cat myCat = new Cat();
System.out.println(myCat);
// Create a lambda
Settable<Cat> s = (obj, name, age) -> {
obj.setName(name);
obj.setAge(age);
};
// Call a method to which we pass the cat and lambda
changeEntity(myCat, s);
// Display the cat on the screen and see that its state has changed (it has a name and age)
System.out.println(myCat);
}
private static <T extends HasNameAndAge> void changeEntity(T entity, Settable<T> s) {
s.set(entity, "Smokey", 3);
}
}
interface HasNameAndAge {
void setName(String name);
void setAge(int age);
}
interface Settable<C extends HasNameAndAge> {
void set(C entity, String name, int age);
}
class Cat implements HasNameAndAge {
private String name;
private int age;
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
نتيجة:
Cat{name='null', age=0}
Cat{name='Smokey', age=3}
كما ترون، Cat
كان للكائن حالة واحدة، ثم تغيرت الحالة بعد أن استخدمنا تعبير لامدا. تتحد تعبيرات Lambda بشكل مثالي مع الأدوية العامة. وإذا أردنا إنشاء Dog
فئة تنفذ أيضًا HasNameAndAge
، فيمكننا إجراء نفس العمليات في Dog
الطريقة main()
دون تغيير تعبير لامدا. المهمة 3. اكتب واجهة وظيفية بطريقة تأخذ رقمًا وترجع قيمة منطقية. اكتب تنفيذًا لواجهة كتعبير لامدا الذي يُرجع صحيحًا إذا كان الرقم الذي تم تمريره قابلاً للقسمة على 13. المهمة 4. اكتب واجهة وظيفية بطريقة تأخذ سلسلتين وترجع أيضًا سلسلة. اكتب تطبيقًا لواجهة مثل تعبير لامدا الذي يُرجع السلسلة الأطول. المهمة 5. اكتب واجهة وظيفية بطريقة تأخذ ثلاثة أرقام بفاصلة عائمة: a وb وc وتقوم أيضًا بإرجاع رقم الفاصلة العائمة. اكتب تنفيذًا لهذه الواجهة كتعبير لامدا الذي يُرجع المُميز. في حال نسيت، فهذا D = b^2 — 4ac
. المهمة 6. باستخدام الواجهة الوظيفية من المهمة 5، اكتب تعبير لامدا الذي يُرجع نتيجة a * b^c
. شرح تعبيرات لامدا في جافا. مع الأمثلة والمهام. الجزء 2
GO TO FULL VERSION