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

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

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

الوصول إلى المتغيرات الخارجية

هل يتم تجميع هذا الرمز مع فئة مجهولة؟
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كائنًا.

بناء الجملة لمراجع الأسلوب

انها بسيطة جدا:
  1. نقوم بتمرير إشارة إلى طريقة ثابتة مثل هذا: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
        }
    }
  2. نقوم بتمرير مرجع إلى طريقة غير ثابتة باستخدام كائن موجود، مثل هذا: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
        }
    }
  3. نقوم بتمرير مرجع إلى طريقة غير ثابتة باستخدام الفئة التي تنفذها على النحو التالي: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);
            }
        }
    }
  4. نقوم بتمرير إشارة إلى مُنشئ مثل هذا:ClassName::new

    تعتبر مراجع الطريقة ملائمة جدًا عندما يكون لديك بالفعل طريقة تعمل بشكل مثالي كرد اتصال. في هذه الحالة، بدلًا من كتابة تعبير لامدا يحتوي على كود الطريقة، أو كتابة تعبير لامدا الذي يستدعي الطريقة ببساطة، نقوم ببساطة بتمرير مرجع إليها. وهذا كل شيء.

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

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

من أين حصلت على معلوماتي وماذا يجب أن تقرأ أيضًا؟

وبالطبع وجدت الكثير من الأشياء على جوجل :)
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION