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

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

در گروه منتشر شد
این مقاله برای چه کسانی است؟
  • این برای افرادی است که فکر می کنند قبلاً Java Core را به خوبی می شناسند، اما هیچ سرنخی در مورد عبارات لامبدا در جاوا ندارند. یا شاید آنها چیزی در مورد عبارات لامبدا شنیده باشند، اما جزئیات وجود ندارد
  • این برای افرادی است که درک خاصی از عبارات لامبدا دارند، اما هنوز از آنها وحشت دارند و به استفاده از آنها عادت ندارند.
توضیحی در مورد عبارات لامبدا در جاوا.  همراه با مثال و کار.  قسمت 1 - 1اگر شما با یکی از این دسته بندی ها مناسب نیستید، ممکن است این مقاله را خسته کننده، ناقص یا به طور کلی فنجان چای شما نباشد. در این مورد، به راحتی به چیزهای دیگر بروید یا، اگر در این موضوع به خوبی مسلط هستید، لطفاً در نظرات پیشنهاداتی را در مورد اینکه چگونه می توانم مقاله را بهبود یا تکمیل کنم، ارائه دهید. مطالب مدعی ارزش آکادمیک نیست، چه رسد به تازگی. کاملا برعکس: من سعی خواهم کرد چیزهایی را که پیچیده هستند (برای برخی افراد) به ساده ترین شکل ممکن توصیف کنم. درخواستی برای توضیح Stream API به من انگیزه داد تا این را بنویسم. من در مورد آن فکر کردم و تصمیم گرفتم که برخی از نمونه های جریان من بدون درک عبارات لامبدا غیرقابل درک باشد. بنابراین ما با عبارات لامبدا شروع می کنیم. برای درک این مقاله چه چیزهایی باید بدانید؟
  1. شما باید برنامه نویسی شی گرا (OOP) را درک کنید، یعنی:

    • کلاس ها، اشیاء و تفاوت بین آنها.
    • رابط‌ها، تفاوت آنها با کلاس‌ها و رابطه بین اینترفیس‌ها و کلاس‌ها.
    • متدها، نحوه فراخوانی آنها، متدهای انتزاعی (یعنی متدهای بدون پیاده سازی)، پارامترهای متد، آرگومانهای متد و نحوه ارسال آنها.
    • اصلاح کننده های دسترسی، روش ها/متغیرهای استاتیک، روش ها/متغیرهای نهایی؛
    • وراثت کلاس ها و واسط ها، وراثت چندگانه رابط ها.
  2. دانش Java Core: انواع عمومی (عمومی)، مجموعه ها (لیست ها)، رشته ها.
خب بریم سراغش

کمی تاریخچه

عبارات لامبدا از برنامه نویسی تابعی به جاوا و از ریاضیات به آنجا آمده است. در ایالات متحده در اواسط قرن بیستم، آلونزو چرچ که علاقه زیادی به ریاضیات و انواع انتزاعات داشت، در دانشگاه پرینستون کار می کرد. این آلونزو چرچ بود که حساب لامبدا را اختراع کرد، که در ابتدا مجموعه ای از ایده های انتزاعی بود که کاملاً به برنامه نویسی ارتباط نداشت. ریاضیدانانی مانند آلن تورینگ و جان فون نویمان همزمان در دانشگاه پرینستون کار می کردند. همه چیز جمع شد: چرچ با حساب لامبدا آمد. تورینگ ماشین محاسباتی انتزاعی خود را توسعه داد که اکنون به عنوان "ماشین تورینگ" شناخته می شود. و فون نویمان معماری کامپیوتری را پیشنهاد کرد که اساس کامپیوترهای مدرن را تشکیل داده است (که اکنون "معماری فون نویمان" نامیده می شود). در آن زمان، ایده های آلونزو چرچ به اندازه آثار همکارانش (به استثنای رشته ریاضیات محض) شناخته شده نبود. با این حال، کمی بعد جان مک کارتی (همچنین فارغ التحصیل دانشگاه پرینستون و در زمان داستان ما، کارمند موسسه فناوری ماساچوست) به ایده های چرچ علاقه مند شد. در سال 1958، او اولین زبان برنامه نویسی کاربردی، LISP را بر اساس این ایده ها ایجاد کرد. و 58 سال بعد، ایده های برنامه نویسی تابعی به جاوا 8 لو رفت. حتی 70 سال هم نگذشته است... راستش را بخواهید، این طولانی ترین زمانی نیست که یک ایده ریاضی در عمل به کار گرفته شده است.

قلب موضوع

عبارت لامبدا نوعی تابع است. شما می توانید آن را یک متد معمولی جاوا در نظر بگیرید، اما با قابلیت متمایز برای انتقال به روش های دیگر به عنوان یک آرگومان. درست است. انتقال نه تنها اعداد، رشته ها و گربه ها به روش ها، بلکه روش های دیگر نیز ممکن شده است! چه زمانی ممکن است به این نیاز داشته باشیم؟ برای مثال، اگر بخواهیم برخی از روش‌های برگشت تماس را پاس کنیم، مفید خواهد بود. یعنی اگر به متدی که فراخوانی می‌کنیم نیاز داریم تا بتوانیم متد دیگری را فراخوانی کنیم که به آن پاس می‌دهیم. به عبارت دیگر، بنابراین ما این توانایی را داریم که در شرایط خاص یک تماس و در شرایط دیگر یک تماس متفاوت را ارسال کنیم. و به طوری که روش ما که تماس های ما را دریافت می کند آنها را فراخوانی می کند. مرتب سازی یک مثال ساده است. فرض کنید در حال نوشتن یک الگوریتم مرتب‌سازی هوشمندانه هستیم که به شکل زیر است:
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
اکنون آرایه های are بر اساس تعداد کل حروف در کلمات آرایه مرتب می شوند. در آرایه اول، 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). اینترفیس هایی که تنها یک متد دارند، رابط های کاربردی نیز نامیده می شوند . در جاوا 8 حتی با یک حاشیه نویسی خاص مشخص شده اند: @FunctionalInterface. این رابط های تک روشی هستند که به عنوان انواع هدف برای عبارات لامبدا مناسب هستند. همانطور که در بالا گفتم، عبارت لامبدا روشی است که در یک شی پیچیده شده است. و وقتی از چنین شی ای عبور می کنیم، اساساً از این روش واحد عبور می کنیم. معلوم می شود که برای ما مهم نیست که نام روش چیست. تنها چیزی که برای ما مهم است پارامترهای روش و البته بدنه روش است. در اصل، یک عبارت لامبدا اجرای یک رابط کاربردی است. هر جا که یک رابط با یک متد مشاهده کنیم، یک کلاس ناشناس را می توان به صورت لامبدا بازنویسی کرد. اگر اینترفیس بیشتر یا کمتر از یک متد داشته باشد، یک عبارت لامبدا کار نخواهد کرد و در عوض از یک کلاس ناشناس یا حتی نمونه ای از یک کلاس معمولی استفاده می کنیم. حالا وقت آن است که کمی به لامبدا بپردازیم. :)

نحو

نحو کلی چیزی شبیه به این است:
(parameters) -> {method body}
یعنی پرانتزهایی که پارامترهای متد را احاطه کرده اند، یک "پیکان" (که با خط فاصله و علامت بزرگتر از آن تشکیل می شود)، و سپس بدنه روش در پرانتزها، مثل همیشه. پارامترها با پارامترهای مشخص شده در روش رابط مطابقت دارند. اگر انواع متغیرها را کامپایلر می‌تواند بدون ابهام تعیین کند (در مورد ما، می‌داند که ما با آرایه‌های رشته‌ای کار می‌کنیم، زیرا Listشی ما با استفاده از ] تایپ می‌شود String[)، پس نیازی نیست که انواع آنها را مشخص کنید.
اگر مبهم هستند، نوع آن را مشخص کنید. IDEA در صورت عدم نیاز آن را خاکستری رنگ می کند.
می توانید در این آموزش Oracle و جاهای دیگر بیشتر بخوانید. به این « تایپ هدف » می گویند. شما می توانید متغیرها را هر چه می خواهید نام گذاری کنید — لازم نیست از همان نام های مشخص شده در رابط استفاده کنید. اگر هیچ پارامتری وجود ندارد، فقط پرانتزهای خالی را نشان دهید. اگر فقط یک پارامتر وجود دارد، به سادگی نام متغیر را بدون هیچ پرانتزی نشان دهید. اکنون که پارامترها را فهمیدیم، زمان آن است که در مورد بدنه عبارت لامبدا صحبت کنیم. در داخل بریس های فرفری، دقیقاً مانند روش معمولی کد می نویسید. اگر کد شما از یک خط تشکیل شده است، می توانید مهاربندهای فرفری را به طور کامل حذف کنید (شبیه به دستورات if و حلقه های for). اگر لامبدای تک خطی شما چیزی را برمی گرداند، نیازی نیست که 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()متد داشته باشند. با توجه به رابط، این 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و برمی گردانیم . وجود پرانتز در اطراف پارامترها در اینجا ضروری است. آنها فقط زمانی اختیاری هستند که فقط یک پارامتر وجود داشته باشد. در اینجا با اشاره ای صریح به انواع آن آمده است: yxy
(double x, int y) -> x % y
مثال 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
ما یک Catشی، یک Stringنام، و یک int age می گیریم. در خود روش از نام و سن عبور برای تنظیم متغیرهای گربه استفاده می کنیم. از آنجایی که شی ما 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شی یک حالت داشت و پس از استفاده از عبارت لامبدا، وضعیت تغییر کرد. عبارات لامبدا کاملاً با کلیات ترکیب می شوند. و اگر نیاز به ایجاد Dogکلاسی داشته باشیم که آن را نیز پیاده‌سازی کند HasNameAndAge، می‌توانیم همان عملیات را در Dogمتد main() بدون تغییر عبارت lambda انجام دهیم. وظیفه 3. یک رابط کاربردی با روشی بنویسید که یک عدد را می گیرد و یک مقدار بولی را برمی گرداند. یک پیاده سازی از چنین رابطی به عنوان یک عبارت لامبدا بنویسید که اگر عدد ارسال شده بر 13 بخش پذیر باشد، true را برمی گرداند. پیاده سازی چنین رابطی را به عنوان عبارت لامبدا بنویسید که رشته طولانی تر را برمی گرداند. وظیفه 5. یک رابط کاربردی با روشی بنویسید که سه عدد ممیز شناور را بگیرد: a، b و c و همچنین یک عدد ممیز شناور را برمی گرداند. پیاده سازی چنین رابطی را به عنوان عبارت لامبدا بنویسید که تفکیک کننده را برمی گرداند. در صورتی که شما فراموش کرده اید، این است D = b^2 — 4ac. وظیفه 6. با استفاده از رابط کاربردی از Task 5، یک عبارت لامبدا بنویسید که نتیجه را برمی گرداند a * b^c. توضیحی در مورد عبارات لامبدا در جاوا. همراه با مثال و کار. قسمت 2
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION