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

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

نشرت في المجموعة
لمن هذه المقالة؟
  • لمن قرأ الجزء الأول من هذا المقال؛

  • بالنسبة لأولئك الذين يعتقدون أنهم يعرفون 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(). لا تتغير قيمة المتغير نفسه أثناء تشغيل البرنامج ويشير دائمًا إلى نفس الكائن، مما يسمح لنا بالإعلان عن متغير فورًا باستخدام الكلمة الأساسية final. نفس الأمثلة، ولكن مع تعبيرات لامدا:
int counter = 0;
Runnable r = () -> counter++;
لا يتم تجميعه لنفس السبب مثل الخيار مع فئة مجهولة: counterيجب ألا يتغير أثناء تشغيل البرنامج. ولكن هكذا - كل شيء على ما يرام:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
وهذا ينطبق أيضًا على طرق الاتصال. من داخل تعبير لامدا، لا يمكنك الوصول إلى جميع المتغيرات "المرئية" فحسب، بل يمكنك أيضًا استدعاء تلك الأساليب التي يمكنك الوصول إليها.
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {
        System.out.println("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
على الرغم من أن الطريقة staticMethod()خاصة، إلا أنه "يمكن الوصول إليها" ليتم استدعاؤها داخل الطريقة main()، لذلك يمكن أيضًا الوصول إليها من خلال الاستدعاء من داخل lambda الذي تم إنشاؤه في الطريقة main.

لحظة تنفيذ كود تعبير لامدا

قد يبدو هذا السؤال بسيطًا جدًا بالنسبة لك، ولكنه يستحق الطرح: متى سيتم تنفيذ الكود الموجود داخل تعبير لامدا؟ في لحظة الخلق؟ أو في الوقت الذي (لا يزال غير معروف أين) سيتم استدعاؤه؟ من السهل جدًا التحقق.
System.out.println("Запуск программы");

// много всякого разного codeа
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого codeа
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
الإخراج على الشاشة:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
يمكن ملاحظة أنه تم تنفيذ كود تعبير لامدا في النهاية، بعد إنشاء الخيط وفقط عندما تصل عملية تنفيذ البرنامج إلى التنفيذ الفعلي للطريقة run(). وليس على الإطلاق وقت إعلانه. من خلال الإعلان عن تعبير لامدا، قمنا فقط بإنشاء كائن من النوع Runnableووصف سلوك طريقته run(). تم إطلاق الطريقة نفسها في وقت لاحق.

مراجع الطريقة؟

لا ترتبط بشكل مباشر باللامدا نفسها، ولكن أعتقد أنه سيكون من المنطقي أن نقول بضع كلمات عنها في هذه المقالة. لنفترض أن لدينا تعبير لامدا لا يفعل أي شيء خاص، ولكنه يستدعي فقط طريقة ما.
x -> System.out.println(x)
لقد سلموه شيئًا х، وقد اتصل به ببساطة System.out.println()ومرره إلى هناك х. وفي هذه الحالة يمكننا استبداله برابط الطريقة التي نحتاجها. مثله:
System.out::println
نعم، بدون الأقواس في النهاية! مثال أكثر اكتمالا:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

strings.forEach(x -> System.out.println(x));
في السطر الأخير نستخدم طريقة forEach()تقبل كائن الواجهة Consumer. وهذه مرة أخرى واجهة وظيفية بأسلوب واحد فقط void accept(T t). وبناء على ذلك، نكتب تعبير لامدا الذي يأخذ معلمة واحدة (بما أنه مكتوب في الواجهة نفسها، فإننا لا نشير إلى نوع المعلمة، ولكن نشير إلى أنه سيتم استدعاؤها х). في نص تعبير لامدا نكتب الكود سيتم تنفيذه عند استدعاء الطريقة accept(). هنا نعرض ببساطة على الشاشة ما هو موجود في المتغير х. تمر الطريقة نفسها forEach()عبر جميع عناصر المجموعة، وتستدعي Consumerطريقة كائن الواجهة الذي تم تمريره إليه (lamda الخاص بنا) accept()، حيث يمرر كل عنصر من المجموعة. كما قلت سابقًا، هذا تعبير لامدا (ببساطة استدعاء طريقة أخرى) يمكننا استبداله بمرجع إلى الطريقة التي نحتاجها. ثم سيبدو الكود الخاص بنا كما يلي:
List<String> strings = new LinkedList<>();
strings.add("Mother");
strings.add("soap");
strings.add("frame");

strings.forEach(System.out::println);
الشيء الرئيسي هو أن المعلمات المقبولة للطرق (println()و accept()). نظرًا لأن الطريقة println()يمكن أن تقبل أي شيء (فهي مثقلة بجميع العناصر الأولية وأي كائنات)، فبدلاً من تعبير لامدا، يمكننا تمرير forEach()مرجع فقط إلى الطريقة println()، ثم forEach()ستأخذ كل عنصر من المجموعة وتمررها مباشرة إلى الطريقة println()بالنسبة لأولئك الذين يواجهون هذا لأول مرة، يرجى ملاحظة يرجى ملاحظة أننا لا نسمي الطريقة System.out.println()(مع النقاط بين الكلمات والأقواس في النهاية)، بل نمرر المرجع إلى هذه الطريقة نفسها.
strings.forEach(System.out.println());
سيكون لدينا خطأ في التجميع. لأنه قبل الاستدعاء forEach()، ستلاحظ Java أنه يتم استدعاؤها System.out.println()، وستفهم أنه تم إرجاعها voidوستحاول voidتمرير هذا إلى forEach()كائن الكتابة المنتظر هناك Consumer.

بناء الجملة لاستخدام مراجع الطريقة

الأمر بسيط جدًا:
  1. تمرير إشارة إلى طريقة ثابتةNameКласса:: NameСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. تمرير مرجع إلى طريقة غير ثابتة باستخدام كائن موجودNameПеременнойСОбъектом:: method name

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            Main instance = new Main();
            strings.forEach(instance::nonStaticMethod);
        }
    
        private void nonStaticMethod(String s) {
            // do something
        }
    }
  3. نقوم بتمرير مرجع إلى طريقة غير ثابتة باستخدام الفئة التي يتم فيها تنفيذ هذه الطريقةNameКласса:: method name

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Vasya"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            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);
            }
        }
    }
  4. يعد تمرير رابط إلى المنشئ NameКласса::new
    باستخدام روابط الطريقة أمرًا مريحًا للغاية عندما تكون هناك طريقة جاهزة تشعر بالرضا التام عنها وترغب في استخدامها كرد اتصال. في هذه الحالة، بدلًا من كتابة تعبير لامدا باستخدام رمز تلك الطريقة، أو تعبير لامدا حيث نسمي هذه الطريقة ببساطة، فإننا ببساطة نمرر مرجعًا إليها. هذا كل شئ.

فرق مثير للاهتمام بين الطبقة المجهولة وتعبير لامدا

في فئة مجهولة، thisتشير الكلمة الأساسية إلى كائن من تلك الفئة المجهولة. وإذا استخدمناها thisداخل لامدا، فسنتمكن من الوصول إلى كائن فئة التأطير. حيث كتبنا فعلا هذا التعبير. يحدث هذا لأن تعبيرات لامدا، عند تجميعها، تصبح طريقة خاصة للفئة التي تمت كتابتها فيها. لا أنصح باستخدام هذه "الميزة" لما لها من آثار جانبية تتعارض مع مبادئ البرمجة الوظيفية. لكن هذا النهج يتوافق تمامًا مع OOP. ;)

من أين حصلت على المعلومات أو ماذا أقرأ

تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION