عبارات لامبدا (Lambda Expressions)
عبارات لامبدا در JDK 8 به جاوا اضافه شدند. طرز کار عبارات لامبدا از دو قسمت تشکیل میشود: اول خود عبارت لامبدا و دوم functional interfaceها. حال به توضیح هر یک از این موارد میپردازیم:
عبارت لامبدا: یک عبارت لامبدا در واقع یک متد ناشناس (بدون نام) است. البته این متد ناشناس به تنهایی قابل اجرا نیست و برای پیادهسازی متدی که در یک functional interface تعریفشده به کار میرود. بنابراین میتوان گفت که یک عبارت لامبدا نوعی کلاس ناشناس به حساب میآید. به عبارات لامبدا Closure نیز گفته میشود.
Functional Interface: اینترفیسی است که فقط و فقط یک متد در آن تعریف شده است (متد abstract یا همان متدهای معمولی که در اینترفیسها تعریف میشوند). این متد معمولا بیانگر هدف اصلی اینترفیس است بنابراین یک اینترفیس تابعی بیانگر یک عمل (action) خاص است.
مثلا اینترفیس Runnable یک اینترفیس تابعی است چون فقط یک متد به نام run() دارد و متد run عمل اینترفیس Runnable را تعریف میکند.
نکته: به اینترفیسهای تابعی SAM type نیز گفته میشود که SAM مخفف Single Abstract Method است.
مبانی عبارات لامبدا
همراه با عبارات لامبدا یک عملگر جدید نیز به زبان جاوا اضافه شده است که به آن عملگر لامبدا (lambda operator) یا عملگر پیکان (arrow operator) نیز گفته میشود و به شکل -> نوشته میشود. این عملگر یک عبارت لامبدا را به دو بخش تقسیم میکند: بخش سمت چپ پارامترهای موردنیاز عبارت لامبدا و بخش سمت راست بدنه عبارت لامبدا را مشخص میکند که بدنه عبارت لامبدا بیانگر کاری است که عبارت لامبدا انجام میدهد.
دو نوع بدنه لامبدا در جاوا وجود دارد: نوع اول فقط شامل یک عبارت و نوع دوم شامل بلاکی از کدها میباشد.
کد زیر نمونهای از سادهترین عبارت لامبدایی که میتوان نوشت است:
() -> 13
این عبارت لامبدا عدد ۱۳ را برگردانده و هیچ پارامتری دریافت نمیکند و بنابراین لیست پارامترهای آن خالی است.
عبارت لامبدایی که دیدید را میتوان به صورت زیر در قالب یک متد معمولی نوشت:
int test() { return 13; }
البته test یک نام فرضی میباشد و همانطور که گفتیم عبارات لامبدا بدون نام هستند.
هر نوع داده معتبری میتواند به عنوان نوع داده برگشتی یک عبارت لامبدا تعریف شود.
نمونهای دیگر از یک عبارت لامبدا که یک پارامتر دریافت میکند:
(n) -> 1 / n
این عبارت لامبدا عدد ۱ را تقسیم بر مقدار پارامتر دریافتی خود کرده و نتیجه را برمیگرداند.
نکته: اگر یک عبارت لامبدا فقط یک پارامتر دریافت کند میتوان پرانتزی که آن را احاطه کرده است را حذف کرد. مثلا عبارت لامبدای بالا را به صورت زیر نیز میتوان نوشت:
n -> 1 / n
اینترفیسهای تابعی (Functional Interfaces)
همانطور که گفته شد یک اینترفیس تابعی اینترفیسی است که فقط یک متد abstract داشته باشد. اگرچه اینترفیسهای تابعی میتوانند شامل متدهای static یا default نیز باشند اما فقط و فقط یک متد abstract باید در آنها وجود داشته باشد.
یک عبارت لامبدا در واقع پیادهسازی متد abstractی است که در یک اینترفیس تابعی تعریف شده است.
به اینترفیس زیر دقت کنید:
interface MyNumber { int getNumber(); }
در این مثال متد getNumber یک متد abstract است (اگرچه با کلمه abstract تعریف نشده اما در یک اینترفیس متدی که نه static باشد و نه default به صورت ضمنی abstract است). پس این اینترفیس یک اینترفیس تابعی است و عملکرد آن با متد getNumber تعریف شده است.
همانطور که گفتیم یک عبارت لامبدا به تنهایی اجرا نمیشود. در ادامه یک مثال نحوه استفاده از عبارات لامبدا را مشاهده خواهید کرد.
مثال: انتساب یک عبارت لامبدا به یک متغیر از نوع اینترفیس تابعی مربوط به عبارت لامبدا.
اینترفیس MyNumber را که پیشتر کد آن را دیدیم در نظر بگیرید:
MyNumber myNum = () -> 13; System.out.println(myNum.getNumber());
در این کد ابتدا متغیری از نوع اینترفیس تابعی MyNumber تعریف کردیم سپس یک عبارت لامبدا به این متغیر نسبت دادیم. این عبارت لامبدا در واقع پیادهسازی متد getNumber که در اینترفیس تابعی MyNumber وجود دارد میباشد.
حال که متد getNumber را با استفاده از یک عبارت لامبدا پیادهسازی کردیم و به متغیر myNum نسبت دادیم میتوانیم با فراخوانی متد getNumber از متغیر myNum از عبارت لامبدایی که نوشتیم استفاده کنیم.
این کار را با استفاده از کلاسهای ناشناس نیز میتوان انجام داد اما همانطور که دیدید عبارات لامبدا نیاز به کدنویسی کمتری نسبت که کلاسهای ناشناس دارند.
نکته مهمی که باید به آن توجه داشت این است که عبارت لامبدایی که برای پیادهسازی یک متد در یک اینترفیس تابعی مینویسیم باید با آن متد سازگاری داشته باشد. مثلا اگر عبارت لامبدایی که در مثال قبل نوشتیم به جای عدد ۱۳ عدد ۱۳٫۵ را بر میگرداند برنامه اجرا نمیشد چون نوع داده بازگشتی متد getNumber از نوع عدد صحیح int است. همچنین چون متد getNumber هیچ پارامتری دریافت نمیکرد پس عبارت لامبدایی که نوشتیم نیز باید بدون پارامتر باشد.
با توجه به نوع و تعداد پارامترها و نوع داده بازگشتی میتوان پیاده سازیهای مختلفی از یک functional interface را نوشت.
مثال: اینترفیسی به نام NumberTest داریم که متدی به نام test به صورت زیر در آن تعریف شده است:
public interface NumberTest { boolean test(int x, int y); }
حال سه عبارت لامبدا که هرکدام پیادهسازی مختلفی دارند مینویسیم:
NumberTest lessThan = (m, n) -> m < n; NumberTest biggerThan = (a, b) -> a > b; NumberTest equal = (x, y) -> x == y; System.out.println(lessThan.test(5, 8)); System.out.println(biggerThan.test(10, 8)); System.out.println(equal.test(1, 1));
هر سه عبارت لامبدایی که نوشتیم دو پارامتر از نوع int دریافت میکنند و یک مقدار boolean برمیگردانند پس با اینترفیس NumberTest سازگاری دارند اگرچه هر کدام پیادهسازیهای مختلفی دارند.
ضمنا توجه داشته باشید در یک عبارت لامبدا نام پارامترها حتما نباید با نامی که برای پارامترها در اینترفیس نوشته شده یکی باشد. فقط نوع پارامترها، تعداد پارامترها و نوع داده بازگشتی باید با اینترفیس تابعی مطابقت داشته باشد. همچنین پارامترهای متد یک اینترفیس تابعی میتواند هم از نوع Primitive و هم از نوع Reference باشد.
عبارات لامبدای بلاکی (Block Lambda Expressions)
عبارات لامبدایی که تا الان بررسی کردیم همگی از یک عبارت (expression) تشکیل میشدند. به این نوع عبارات لامبدا expression lambda نیز میگویند. اما گاهی اوقات نیاز به نوشتن بیش از یک عبارت در بدنه لامبدا میباشد که در این صورت از لامبداهای بلاکی (block lambda) استفاده میکنیم.
مثال: اینترفیس مثال قبل را به این صورت تغییر دادیم:
public interface NumberTest { String test(int x, int y); }
حال یک لامبدای بلاکی مینویسیم که دو پارامتر این متد را با هم مقایسه کند و عبارت مناسبی را برگرداند:
NumberTest compare = (a, b) -> { if (a > b) { return a + " is greater than " + b; } if (a < b) { return a + " is smaller than " + b; } if (a == b) { return "Equal"; } return null; }; System.out.println(compare.test(8, 10));
همانطور که دیدید در لامبداهای بلاکی باید مقدار بازگشتی را صراحتا با return مشخص کرد.
ارجاع به متد (Method Reference)
قابلیت مهم دیگری به نام ارجاع به متد وجود دارد که به نوعی به عبارات لامبدا مربوط است. با این قابلیت میتوان به یک متد ارجاع داد بدون آنکه نیاز به فراخوانی آن باشد. شباهت ارجاع به متد به عبارات لامبدا از این جهت است که هر دوی آنها باید با یک اینترفیس تابعی تطبیق داشته باشند.
در عبارات لامبدا یک متد ناشناس مینوشتیم که به عنوان کلاس ناشناسی که یک اینترفیس تابعی را پیادهسازی میکند عمل میکرد. ارجاع به متدها نیز همین امکان را به ما میدهند با این تفاوت که متدهایی از قبل نوشته شدهاند و ما فقط به آنها ارجاع میدهیم.
ارجاع به متدهای استاتیک (Reference to Static Methods)
مثال: اینترفیس NumberTest از مثال قبل را داریم:
public interface NumberTest { String test(int x, int y); }
کلاسی به نام Tests داریم که شامل سه متد استاتیک به صورت زیر است:
public class Tests { public static String isBigger(int a, int b) { return a > b ? a + " is bigger than " + b : a + " is not bigger than " + b; } public static String isSmaller(int x, int y) { return x < y ? x + " is smaller than " + y : x + " is not smaller than " + y; } public static String isEqual(int n, int m) { return n == m ? n + " is equal to " + m : n + " is not equal to " + m; } }
در کلاس دیگری متدی به صورت زیر داریم:
public static String testNumbers(NumberTest t, int a, int b) { return t.test(a, b); }
همانطور که میبینید این متد پارامتری از نوع اینترفیس NumberTest دریافت میکند و دو پارامتر دیگر از نوع int. همانطور که از قسمت اینترفیسها آموختید وقتی متغیری از نوع یک اینترفیس داریم باید به آن شیئی از کلاسی نسبت دهیم که آن اینترفیس را پیادهسازی میکند.
به چهار روش میتوانیم به پارامتر t مقدار دهیم. البته روشهای سوم و چهارم فقط در مورد اینترفیسهای تابعی کاربرد دارند:
۱ – ساخت یک کلاس و پیادهسازی اینترفیس NumberTest در آن و سپس ساخت شی از آن کلاس و دادن آن به متد testNumbers
۲ – ساخت یک کلاس ناشناس که اینترفیس NumberTest را پیادهسازی کند و دادن آن به متد testNumbers
۳ – نوشتن یک عبارت لامبدا که با متد موجود در اینترفیس NumberTest سازگار باشد (از لحاظ نوع و تعداد پارامترها و نوع داده بازگشتی)
۴ – روش چهارم این است که میتوانیم متدی به عنوان t به متد testNumbers بدهیم که این متد با متد موجود در اینترفیس NumberTest سازگاری داشته باشد.
از آنجایی که تمام متدهای موجود در کلاس Tests با متد اینترفیس NumberTest سازگاری دارند. چون متدهای موجود در کلاس Tests به صورت استاتیک تعریف شدهاند بنابراین باید به صورت زیر به آنها ارجاع دهیم:
ClassName::methodName
عملگر :: برای ارجاع به متدها ایجاد شده است. پس به عنوان مثال میتوانیم متد testNumbers را به صورت زیر فراخوانی کنیم:
testNumbers(Tests::isBigger, 10, 5);
که در نتیجه متد isBigger با پارامترهای ۱۰ و ۵ فراخوانی شده و مقداری برگردانده میشود.
البته مانند عبارات لامبدا میتوانیم ارجاع به متدها را به صورت زیر نیز استفاده کنیم:
NumberTest t = Tests::isSmaller; System.out.println(t.test(5, 6));
به طور کلی طبق روش چهارم که در بالا اشاره شد به هر متغیر از نوع یک اینترفیس تابعی میتوانیم ارجاع به متدی دهیم که با متد موجود در آن اینترفیس تابعی سازگاری داشته باشد.
نکته: ارجاع به متدها در پشت صحنه تبدیل به شیئی میشوند که اینترفیس تابعی مورد نظر در آن پیادهسازی شده است.
نکته: ارجاع به متدهای نمونه (Instance Methods) نیز مانند ارجاع به متدهای استاتیک است با این تفاوت که به جای نام کلاس باید نام شیئی که از کلاس ایجاد شده را بنویسیم.
باید ۲-۳ مرتبه دیگه بخونمش. یکم پیچیده بود! و اینکه کاربردش هم به نظر میاد کم باشه!
ولی خب خیلی ممنونم از توضیحات کاملتون مدتی بود میخواستم با این عبارت آشنا بشم که منبع فارسی خوبی پیدا نمیکردم.
خواهش میکنم اگر سوالی داشتید میتونید همینجا بپرسید. درباره کاربردشون هم میشه گفت که خیلی پرکاربرد نیستند اما بدون استفاده هم نیستند. به عنوان مثال اگر برنامه نویسی اندروید کار کنید میتونید برای تعریف رویداد کلیک از عبارات لامبدا به جای Anonymous Class استفاده کنید. در بحث Functional Programming هم از عبارات لامبدا استفاده میشه.
نمونه دیگه از کاربرد عبارات لامبدا هم در Stream API هست که در جاوا ۸ معرفی شده.
بله جدیدا متوجه شدم که برای استفاده از رویداد ها بکارگیری لامبدا چقدر میتونه کد رو واضح تر و کوتاه تر بکنه!
در کل جاوای ۸ ساختار های بسیار بسیار خوبی رو اضافه کرده که متاسفانه درحال حاضر تمام قابلیت هاش در اندروید قابل استفاده نیست. مثلا اضافه شدن forEach به کالکشن ها فاق العاده بود که الآن نمیتونیم ازش استفاده کنیم!!!
همین الآن متوجه شدم که کامپایلر”جک” که برای استفاده از جاوا۸ در اندروید باید حتما حضور داشته باشه یه باگی داره که فعلا هم برطرف نشده!
جک برای دیکد کردن متن از سیستم پیشفرض ماشین جاوا استفاده میکنه (windows-1252) در صورتی که متن ما با UTF-8 انکود میشه و همین باعث میشه استرینگ های فارسی در کد جاوای ما در برنامه ناخوانا باشند و خب باید حتما در یک فایل xml اکسترکتشون کنید و …
متاسفانه هم هنوز گریدل به ما اجازه تغییر فرمت دیکود رو نمیده و باید منتظر بمونیم ببینیم گوگل چکار میکنه!!!
سلام . دوره ی جاوا تموم شد…
حالا اندروید رو شروع کنین… چاوا رو که همه بلدن ، اندروید رو بگین ممنون
سلام. فعلا قصدی برای آموزش اندروید ندارم به دلیل اینکه آموزش اندروید بسیار گسترده و برای من بسیار زمان بر هست و حداقل به این زودی توانایی تولیدش رو ندارم. اگه همه جاوا رو بلد باشن که خیلی خوبه 🙂 چون من به شخصه هنوز نمی تونم بگم جاوا رو بلدم.
موفق باشید
ممنون از مطالبتون و اموزش مفید واقع شد اگه ممکنه یه مقدار در مورد php بنویسید با تشکر
خواهش میکنم. من تخصصی در php ندارم. در همین سایت مطالب خوبی در این باره نوشته شده.
سلام یک سوال
براتون مقدوره اموزش هاتون رو PDF کنید تا بتونیم افلاین استفاده کنیم ؟؟؟؟
به به عالی بود و روان. مشخصه که مطالب رو خودتون مسلط هستید و با توضیحات کامل بیان کردید. البته که آموزش فارسی جاوا زیاد پیدا میشه ولی متاسفانه ترجمه های هستند که خود نیاز به ترجمه دارند.
تشکر فراوان. خیلی خوشحال مشیم آندروید رو هم شروع کنید. و کارتون رو ادامه بدید.
بسیار کارا
واقعا این سلسله آموزشهای جاوا خیلی بهم کمک کرد.
همینکه آدم از چیزهایی که میدونه آموزش میده کلی بهش کمک میکنه. امیدوارم بازم مطالبی مثل این ازتون ببینم.