CodeGym /وبلاگ جاوا /Random-FA /توضیحی در مورد عبارات لامبدا در جاوا. همراه با مثال و کار...
John Squirrels
مرحله
San Francisco

توضیحی در مورد عبارات لامبدا در جاوا. همراه با مثال و کار. قسمت 2

در گروه منتشر شد
این مقاله برای چه کسانی است؟
  • این برای افرادی است که قسمت اول این مقاله را می خوانند.
  • این برای افرادی است که فکر می کنند قبلاً Java Core را به خوبی می شناسند، اما هیچ سرنخی در مورد عبارات لامبدا در جاوا ندارند. یا شاید آنها چیزی در مورد عبارات لامبدا شنیده باشند، اما جزئیات وجود ندارد.
  • این برای افرادی است که درک خاصی از عبارات لامبدا دارند، اما هنوز از آنها وحشت دارند و به استفاده از آنها عادت ندارند.
اگر شما با یکی از این دسته بندی ها مناسب نیستید، ممکن است این مقاله را خسته کننده، ناقص یا به طور کلی فنجان چای شما نباشد. در این مورد، به راحتی به چیزهای دیگر بروید یا، اگر در این موضوع به خوبی مسلط هستید، لطفاً در نظرات پیشنهاداتی را در مورد اینکه چگونه می توانم مقاله را بهبود یا تکمیل کنم، ارائه دهید. توضیحی در مورد عبارات لامبدا در جاوا.  همراه با مثال و کار.  قسمت 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();
این در مورد روش های فراخوانی نیز صدق می کند. در عبارات لامبدا، شما نه تنها می توانید به همه متغیرهای "مرئی" دسترسی داشته باشید، بلکه می توانید هر روش قابل دسترسی را نیز فراخوانی کنید.
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()، بنابراین می توان آن را از داخل یک لامبدا ایجاد شده در متد نیز فراخوانی کرد 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!
می بینید که عبارت lambda در انتها، پس از ایجاد نخ و تنها زمانی که اجرای برنامه به متد رسید، اجرا شد run(). البته نه زمانی که اعلام می شود. با اعلان یک عبارت لامبدا، ما فقط یک Runnableشی را ایجاد کرده ایم و نحوه run()رفتار روش آن را شرح داده ایم. خود روش خیلی دیرتر اجرا می شود.

مراجع روش؟

مراجع روش مستقیماً به لامبدا مربوط نمی شود، اما فکر می کنم منطقی است که در این مقاله چند کلمه در مورد آنها بگوییم. فرض کنید یک عبارت لامبدا داریم که کار خاصی انجام نمی دهد، بلکه به سادگی یک متد را فراخوانی می کند.
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()متد می تواند هر چیزی را بپذیرد (برای همه انواع اولیه و همه اشیا بیش از حد بارگذاری شده است)، به جای عبارات لامبدا، می توانیم به سادگی یک مرجع به متد را println()به forEach(). سپس forEach()هر عنصر در مجموعه را می گیرد و مستقیماً به println()متد ارسال می کند. برای کسانی که برای اولین بار با این مشکل روبرو می شوند، لطفاً توجه داشته باشید که ما تماس نمی گیریم System.out.println()(با نقطه بین کلمات و با پرانتز در پایان). در عوض، ما به این روش اشاره می کنیم. اگر این را بنویسیم
strings.forEach(System.out.println());
یک خطای کامپایل خواهیم داشت. قبل از فراخوانی به forEach()، جاوا می‌بیند که 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