JavaRush /مدونة جافا /Random-AR /شعبية حول تعبيرات لامدا في جافا. مع الأمثلة والمهام. الجز...
Стас Пасинков
مستوى
Киев

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

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

قليلا من التاريخ

جاءت تعبيرات لامدا إلى جافا من البرمجة الوظيفية، ومن الرياضيات. في منتصف القرن العشرين في أمريكا، عملت كنيسة ألونزو معينة في جامعة برينستون، التي كانت مغرمة جدًا بالرياضيات وجميع أنواع التجريدات. كان ألونزو تشيرش هو من ابتكر حساب التفاضل والتكامل لامدا، والذي كان في البداية عبارة عن مجموعة من الأفكار المجردة ولم يكن له أي علاقة بالبرمجة. وفي الوقت نفسه، عمل علماء الرياضيات مثل آلان تورينج وجون فون نيومان في نفس جامعة برينستون. اجتمع كل شيء معًا: ابتكر تشرش نظام حساب التفاضل والتكامل لامدا، وطوّر تورينج آلة الحوسبة المجردة الخاصة به، والتي تُعرف الآن باسم "آلة تورينج". حسنًا، اقترح فون نيومان رسمًا تخطيطيًا لبنية أجهزة الكمبيوتر، والتي شكلت أساس أجهزة الكمبيوتر الحديثة (وتسمى الآن "هندسة فون نيومان"). في ذلك الوقت، لم تكتسب أفكار ألونزو تشيرش نفس الشهرة التي حظيت بها أعمال زملائه (باستثناء مجال الرياضيات "البحتة"). ومع ذلك، في وقت لاحق قليلا، أصبح جون مكارثي (أيضا خريج جامعة برينستون، في وقت القصة - موظف في معهد ماساتشوستس للتكنولوجيا) مهتما بأفكار الكنيسة. وبناءً عليها، ابتكر في عام 1958 أول لغة برمجة وظيفية، Lisp. وبعد 58 عامًا، تسربت أفكار البرمجة الوظيفية إلى Java برقم 8. ولم يمر حتى 70 عامًا... في الواقع، هذه ليست أطول فترة زمنية لتطبيق فكرة رياضية في الممارسة العملية.

الجوهر

تعبير لامدا هو مثل هذه الوظيفة. يمكنك اعتبار ذلك طريقة عادية في Java، والفرق الوحيد هو أنه يمكن تمريرها إلى طرق أخرى كوسيطة. نعم، أصبح من الممكن تمرير ليس فقط الأرقام والسلاسل والقطط إلى الطرق، ولكن أيضًا إلى طرق أخرى! متى قد نحتاج إلى هذا؟ على سبيل المثال، إذا أردنا تمرير بعض عمليات رد الاتصال. نحتاج إلى الطريقة التي نستدعيها حتى نتمكن من استدعاء طريقة أخرى نمررها إليها. وهذا يعني أن لدينا الفرصة لإرسال رد اتصال واحد في بعض الحالات وآخر في حالات أخرى. وطريقتنا، التي تقبل عمليات الاسترجاعات، ستستدعيها. مثال بسيط هو الفرز. لنفترض أننا نكتب نوعًا من الفرز الصعب الذي يبدو كما يلي:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
حيث ifنسمي الطريقة compare()، نمرر هناك كائنين نقارنهما، ونريد معرفة أي من هذه الكائنات "أكبر". سنضع ما هو "أكثر" قبل ما هو "أصغر". لقد كتبت "المزيد" بين علامتي اقتباس لأننا نكتب طريقة عالمية ستكون قادرة على الفرز ليس فقط بترتيب تصاعدي ولكن أيضًا بترتيب تنازلي (في هذه الحالة، سيكون "المزيد" هو الكائن الأصغر حجمًا، والعكس صحيح). . لتعيين القاعدة لكيفية الفرز بالضبط، نحتاج إلى تمريرها بطريقة أو بأخرى إلى ملف mySuperSort(). في هذه الحالة، سنكون قادرين بطريقة أو بأخرى على "التحكم" في طريقتنا أثناء استدعائها. بالطبع، يمكنك كتابة طريقتين منفصلتين mySuperSortAsc()للفرز mySuperSortDesc()بترتيب تصاعدي وتنازلي. أو قم بتمرير بعض المعلمات داخل الطريقة (على سبيل المثال، booleanإذا true، قم بالفرز بترتيب تصاعدي، وإذا falseبترتيب تنازلي). ولكن ماذا لو أردنا فرز ليس بعض الهياكل البسيطة، ولكن، على سبيل المثال، قائمة من صفائف السلسلة؟ كيف ستعرف طريقتنا mySuperSort()كيفية فرز صفائف السلسلة هذه؟ إلى حجم؟ حسب الطول الكلي للكلمات؟ ربما أبجديًا، اعتمادًا على الصف الأول في المصفوفة؟ ولكن ماذا لو، في بعض الحالات، كنا بحاجة إلى فرز قائمة من المصفوفات حسب حجم المصفوفة، وفي حالة أخرى، حسب الطول الإجمالي للكلمات في المصفوفة؟ أعتقد أنك سمعت بالفعل عن المقارنات وأنه في مثل هذه الحالات نقوم ببساطة بتمرير كائن المقارنة إلى طريقة الفرز لدينا، والتي نصف فيها القواعد التي نريد الفرز وفقًا لها. وبما أن الطريقة القياسية sort()يتم تطبيقها على نفس المبدأ، mySuperSort()فسوف أستخدم الطريقة القياسية في الأمثلة sort().
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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[]> sortByWordsLength = 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. غسلت أمي الإطار
  2. السلام العمل يجوز
  3. أنا حقا أحب جافا
هنا يتم فرز المصفوفات حسب عدد الكلمات في كل مصفوفة. تعتبر المصفوفة التي تحتوي على عدد أقل من الكلمات "أصغر". ولهذا السبب يأتي في البداية. الشخص الذي يحتوي على المزيد من الكلمات يعتبر "أكثر" وينتهي في النهاية. إذا sort()قمنا بتمرير مقارنة أخرى إلى الطريقة (sortByWordsLength)، فستكون النتيجة مختلفة:
  1. السلام العمل يجوز
  2. غسلت أمي الإطار
  3. أنا حقا أحب جافا
الآن يتم فرز المصفوفات حسب العدد الإجمالي للأحرف في كلمات هذه المصفوفة. في الحالة الأولى هناك 10 أحرف، وفي الثانية 12، وفي الثالثة 15. إذا استخدمنا مقارنة واحدة فقط، فلن نتمكن من إنشاء متغير منفصل له، ولكن ببساطة قم بإنشاء كائن من فئة مجهولة مباشرة في وقت استدعاء الطريقة sort(). مثل هذا:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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;
    }
});
هنا نأخذ قائمتنا arraysونسمي طريقتها sort()، حيث نمرر كائن مقارنة بطريقة واحدة compare()(لا يهمنا ما يسمى، لأنه الوحيد في هذا الكائن، ونحن لن نفوته). تأخذ هذه الطريقة معلمتين، وسنعمل معهما بعد ذلك. إذا كنت تعمل في IntelliJ IDEA ، فمن المحتمل أنك رأيت كيف يقدم لك هذا الرمز لتقصيره بشكل كبير:
arrays.sort((o1, o2) -> o1.length - o2.length);
وهكذا تحولت ستة أسطر إلى سطر واحد قصير. تمت إعادة كتابة 6 أسطر في سطر واحد قصير. لقد اختفى شيء ما، لكنني أضمن أنه لم يختفي أي شيء مهم، وسيعمل هذا الرمز تمامًا كما هو الحال مع فئة مجهولة. المهمة 2 . اكتشف كيفية إعادة كتابة حل المشكلة 1 باستخدام لامدا (كحل أخير، اطلب من IntelliJ IDEA تحويل فصلك المجهول إلى لامدا).

دعونا نتحدث عن الواجهات

في الأساس، الواجهة هي مجرد قائمة من الأساليب المجردة. عندما نقوم بإنشاء فصل ونقول أنه سينفذ نوعًا ما من الواجهة، يجب أن نكتب في فصلنا تطبيقًا للطرق المدرجة في الواجهة (أو، كحل أخير، لا نكتبه، بل نجعل الفصل مجردًا ). هناك واجهات تحتوي على العديد من الأساليب المختلفة (على سبيل المثال List)، وهناك واجهات ذات أسلوب واحد فقط (على سبيل المثال، نفس Comparator أو Runnable). توجد واجهات بدون طريقة واحدة على الإطلاق (ما يسمى بواجهات العلامات، على سبيل المثال القابلة للتسلسل). وتسمى أيضًا تلك الواجهات التي تحتوي على طريقة واحدة فقط بالواجهات الوظيفية . في Java 8، يتم تمييزها بتعليق توضيحي خاص @FunctionalInterface . إنها واجهات ذات طريقة واحدة مناسبة للاستخدام بواسطة تعبيرات لامدا. كما قلت أعلاه، تعبير لامدا هو طريقة ملفوفة في كائن. وعندما نمرر مثل هذا الجسم في مكان ما، فإننا في الواقع نمرر هذه الطريقة الوحيدة. اتضح أنه لا يهمنا ما تسمى هذه الطريقة. كل ما يهمنا هو المعلمات التي تأخذها هذه الطريقة، وفي الواقع رمز الطريقة نفسها. تعبير لامدا هو في الأساس. تنفيذ واجهة وظيفية. عندما نرى واجهة ذات طريقة واحدة، فهذا يعني أنه يمكننا إعادة كتابة مثل هذه الفئة المجهولة باستخدام لامدا. إذا كانت الواجهة تحتوي على أكثر/أقل من طريقة واحدة، فلن يناسبنا تعبير لامدا، وسنستخدم فئة مجهولة، أو حتى فئة عادية. حان الوقت للحفر في لامدا. :)

بناء الجملة

بناء الجملة العام هو شيء من هذا القبيل:
(параметры) -> {тело метода}
أي أن الأقواس بداخلها معلمات الطريقة، "السهم" (هذان حرفان متتاليان: ناقص وأكبر)، وبعد ذلك يكون نص الطريقة بين قوسين متعرجين، كما هو الحال دائمًا. تتوافق المعلمات مع تلك المحددة في الواجهة عند وصف الطريقة. إذا كان من الممكن تعريف نوع المتغيرات بوضوح بواسطة المترجم (في حالتنا، من المعروف على وجه اليقين أننا نعمل مع مصفوفات من السلاسل، لأنه يتم كتابتها بدقة بواسطة مصفوفات من السلاسل)، فلن يلزم Listنوع المتغيرات String[]أنيق المهنية عارضة.
إذا لم تكن متأكدًا، فحدد النوع، وستقوم IDEA بتمييزه باللون الرمادي إذا لم تكن هناك حاجة إليه.
يمكنك قراءة المزيد في برنامج Oracle التعليمي ، على سبيل المثال. وهذا ما يسمى "الكتابة المستهدفة" . يمكن إعطاء المتغيرات أية أسماء، وليس بالضرورة تلك المحددة في الواجهة. إذا لم تكن هناك معلمات، ثم بين قوسين فقط. إذا كان هناك معلمة واحدة فقط، فقط اسم المتغير بدون أقواس. لقد قمنا بفرز المعلمات، الآن حول نص تعبير لامدا نفسه. داخل الأقواس المتعرجة، اكتب الكود كما هو الحال في الطريقة العادية. إذا كان الكود بأكمله يتكون من سطر واحد فقط، فلن تضطر إلى كتابة أقواس متعرجة على الإطلاق (كما هو الحال مع ifs والحلقات). إذا قامت لامدا بإرجاع شيء ما، ولكن جسمه يتكون من سطر واحد، فليس 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();
    }
}
أو يمكننا أيضًا حفظ تعبير lambda ككائن من النوع 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(). وفقًا للواجهة، لا تقبل طريقة التشغيل أي شيء كمعلمات. ولا يرجع شيئا (void). لذلك، عند الكتابة بهذه الطريقة، سيتم إنشاء كائن بطريقة لا تقبل أو تعيد أي شيء. وهو ما يتوافق تمامًا مع الطريقة الموجودة run()في الواجهة Runnable. ولهذا السبب تمكنا من وضع تعبير لامدا هذا في متغير مثل 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
وهنا الخيار مع واحد صريح return:
x -> {
    return x;
}
أو هكذا بين قوسين و return:
(x) -> {
    return x;
}
أو مع إشارة صريحة إلى النوع (وبالتالي بين قوسين):
(int x) -> x
مثال 7
x -> ++x
نحن نقبله хونعيده، ولكن من أجل 1المزيد. يمكنك أيضًا إعادة كتابتها على النحو التالي:
x -> x + 1
في كلتا الحالتين، لا نشير إلى أقواس حول المعلمة ونص الطريقة والكلمة return، لأن هذا ليس ضروريًا. يتم وصف الخيارات ذات الأقواس والإرجاع في المثال 6. المثال 8
(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، وسلسلة تحتوي على اسم وعدد صحيح. في الطريقة نفسها، قمنا بتعيين الاسم والعمر الذي تم تمريره إلى القطة. نظرًا لأن المتغير catالخاص بنا هو نوع مرجعي، فسوف يتغير كائن Cat الموجود خارج تعبير lambda (سيتلقى الاسم والعمر الذي تم تمريره بالداخل). إصدار أكثر تعقيدًا قليلًا يستخدم لامدا مشابهة:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Murzik", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    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='Murzik', age=3} كما ترون، في البداية كان لكائن Cat حالة واحدة، ولكن بعد استخدام تعبير لامدا، تغيرت الحالة . تعمل تعبيرات Lambda بشكل جيد مع الأدوية العامة. وإذا أردنا إنشاء فئة Dog، على سبيل المثال، سيتم تنفيذها أيضًا WithNameAndAge، فيمكننا في الطريقة main()إجراء نفس العمليات مع Dog، دون تغيير تعبير لامدا نفسه على الإطلاق. المهمة 3 . اكتب واجهة وظيفية بطريقة تأخذ رقمًا وترجع قيمة منطقية. اكتب تنفيذًا لمثل هذه الواجهة في شكل تعبير لامدا الذي يتم إرجاعه trueإذا كان الرقم الذي تم تمريره قابلاً للقسمة على 13 بدون باقي المهمة 4 . اكتب واجهة وظيفية بطريقة تأخذ سلسلتين وتعيد نفس السلسلة. اكتب تنفيذًا لهذه الواجهة على شكل lambda الذي يُرجع أطول سلسلة. المهمة 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