CodeGym /مدونة جافا /Random-AR /شرح تعبيرات لامدا في جافا. مع الأمثلة والمهام. الجزء 1
John Squirrels
مستوى
San Francisco

شرح تعبيرات لامدا في جافا. مع الأمثلة والمهام. الجزء 1

نشرت في المجموعة
لمن هذه المقالة؟
  • إنه مخصص للأشخاص الذين يعتقدون أنهم يعرفون Java Core جيدًا، ولكن ليس لديهم أدنى فكرة عن تعبيرات lambda في Java. أو ربما سمعوا شيئًا ما عن تعبيرات لامدا، لكن التفاصيل غير متوفرة
  • إنه مخصص للأشخاص الذين لديهم فهم معين لتعبيرات لامدا، لكنهم ما زالوا خائفين منها وغير معتادين على استخدامها.
شرح تعبيرات لامدا في جافا.  مع الأمثلة والمهام.  الجزء 1 - 1إذا كنت لا تنتمي إلى إحدى هذه الفئات، فقد تجد هذه المقالة مملة أو معيبة أو لا تناسبك بشكل عام. في هذه الحالة، لا تتردد في الانتقال إلى أشياء أخرى، أو إذا كنت على دراية جيدة بالموضوع، يرجى تقديم اقتراحات في التعليقات حول كيف يمكنني تحسين المقالة أو استكمالها. ولا تدعي المادة أن لها أي قيمة أكاديمية، ناهيك عن كونها جديدة. بل على العكس تمامًا: سأحاول وصف الأشياء المعقدة (بالنسبة لبعض الأشخاص) بأبسط ما يمكن. لقد ألهمني طلب شرح Stream API لكتابة هذا. لقد فكرت في الأمر وقررت أن بعض أمثلة الدفق الخاصة بي ستكون غير مفهومة دون فهم تعبيرات لامدا. لذا، سنبدأ بتعبيرات لامدا. ما الذي تحتاج إلى معرفته لفهم هذه المقالة؟
  1. يجب أن تفهم البرمجة الشيئية (OOP)، وهي:

    • الطبقات، والأشياء، والفرق بينهما؛
    • الواجهات، وكيف تختلف عن الفئات، والعلاقة بين الواجهات والفئات؛
    • الأساليب، وكيفية استدعائها، والأساليب المجردة (أي الأساليب بدون تطبيق)، ومعلمات الطريقة، ووسائط الطريقة وكيفية تمريرها؛
    • معدّلات الوصول، والأساليب/المتغيرات الثابتة، والأساليب/المتغيرات النهائية؛
    • وراثة الفئات والواجهات، وراثة متعددة للواجهات.
  2. معرفة 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);
نتيجة:
  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
هنا يتم فرز المصفوفات حسب عدد الكلمات في كل مصفوفة. تعتبر المصفوفة التي تحتوي على عدد أقل من الكلمات "أقل". ولهذا السبب يأتي أولاً. تعتبر المصفوفة التي تحتوي على عدد أكبر من الكلمات "أكبر" ويتم وضعها في النهاية. إذا مررنا مقارنة مختلفة للطريقة sort()، مثل sortByCumulativeWordLength، فسنحصل على نتيجة مختلفة:
  1. if then else
  2. Dota GTA5 Halo
  3. 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 بتلوينها باللون الرمادي إذا لم تكن هناك حاجة إليها.
يمكنك قراءة المزيد في هذا البرنامج التعليمي لـ Oracle وفي أي مكان آخر. وهذا ما يسمى " الكتابة المستهدفة ". يمكنك تسمية المتغيرات كما تريد — ليس عليك استخدام نفس الأسماء المحددة في الواجهة. إذا لم تكن هناك معلمات، فما عليك سوى الإشارة إلى الأقواس الفارغة. إذا كان هناك معلمة واحدة فقط، فما عليك سوى الإشارة إلى اسم المتغير دون أي قوسين. الآن بعد أن فهمنا المعلمات، حان الوقت لمناقشة نص تعبير لامدا. داخل الأقواس المتعرجة، تكتب التعليمات البرمجية تمامًا كما تفعل مع الطريقة العادية. إذا كان الكود الخاص بك يتكون من سطر واحد، فيمكنك حذف الأقواس المتعرجة بالكامل (على غرار عبارات if وfor-loops). إذا أعادت lambda ذات السطر الواحد شيئًا ما، فلن يتعين عليك تضمين عبارة 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
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION