عبارت‌های لامبدا (Lambda) در جاوا

عبارت‌های لامبدا (Lambda) در جاوا

نوشته شده توسط: احمدرضا صدیقی
۰۴ دی ۱۳۹۷
زمان مطالعه: 14 دقیقه
3.5
(2)

مقدمه

مسئله‌ای که در پیاده سازی کلاس‌های بی‌نام وجود دارد این است که اگر کلاسِ بی نام بسیار ساده باشد (مشابه اینترفیس Listener که در مثال قبل ملاحظه کردید) پیاده سازی کلاسِ بی‌نام کمی عجیب و نامفهوم به نظر می‌رسد!
تکه کد زیر، ایجاد یک کلاس بی‌نام از روی اینترفیس Listener را نشان می‌دهد.

interactive.addListener(new Listener(){
public void process(Event event){
System.out.println(“Optoin “+event.getOption()+” has selected”);
}
});

 در اینجا در پیاده سازی اینترفیس Listener چه چیزی اهمیت دارد؟ آیا ذکر نام اینترفیس Listener، نام متد ()process یا مقدار برگشتی این متد void اهمیتی دارند؟ واضح است که جواب این سوالها منفی است، آنچه اهمیت دارد نام پارامتر متد ()process (حتی جنس پارامتر که کلاس Event است هم اهمیت ندارد) و جمله

System.out.println(“Optoin “+event.getOption()+” has selected”);

 که اهمیت دارند. بر همین مبنا کد فوق را می‌توان به صورت زیر نیز نوشت.

interactive.addListener(
event -> System.out.println(“Optoin “+event.getOption()+” has selected”);
});

به عبارتی که جایگزین کلاس بی‌نام شده است عبارت لامبدا گفته می‌شود که در واقع زوایدی که در تعریف کلاس بی‌نام وجود دارد از آن حذف شده است. براساس این عبارت، با ورودی پارامتر Event که تنها پارامتر از تنها متد این کلاس است جمله مقابل -> باید اجرا شود.
در ادامه با ارائه یک مثال ساده، جزییات بیشتری از عبارت‌های لامبدا را تشریح خواهیم کرد.
فرض کنید که نرم افزار یک شبکه اجتماعی را پیاده سازی کرده‌اید. در این برنامه کلاس User را که معرف کاربران شبکه اجتماعی است را به صورت زیر پیاده سازی کرده‌اید.

import java.util.Date;
public class User {
public enum Sex {
MALE, FEMALE
}
String name;
Date birthday;
Sex gender;
String emailAddress;
int age;
//getters/setters
public void printDetails() {
// ...
}
}

 حال تصور کنید که از شما خواسته شده است که کدی بنویسید که بر اساس معیار یا معیارهای مشخصی، کاربران شبکه اجتماعی را انتخاب کند. ممکن است در جای دیگری از برنامه به کاربران انتخاب شده ایمیل ارسال شود، اعلان (Notification) ارسال شود،… آنچه موضوع این مثال است، انتخاب کاربران براساس معیارهای مشخص است.
برای پیاده سازی این نیازمندی، تصور کنید که تمام کاربران شبکه اجتماعی در قالب یک آبجکت java.util.List وجود داشته باشد، برای انتخاب کاربران مثلا براساس سن باید متدی مشابه متد ()printUser به صورت زیر پیاده سازی کنیم.

public static void printUsers(List<User> users, int age) {
for (User u : users) {
if (u.getAge() >= age) {
u.printDetails();
}
}
}

 این متد دو پارامتر که یکی لیست کاربران و دیگری حداقل سن آنان است را دریافت می‌کند و طی یک حلقه For با مقایسه سن هر کاربر با حداقل سن مشخص شده، وی را انتخاب می‌کند. واضح است که کد باید به گونه‌ای نوشته شود که تحت تاثیر تغییر معیارهای انتخاب کاربر قرار نگیرد، به عبارت دیگر اگر معیارهای انتخاب یک کاربر تغییر کند و مثلا علاوه بر حداقل سن، حداکثر سن نیز لحاظ شود انتظار داریم که متد ()PrintUsers دست نخورده باقی بماند زیرا مسئولیت متد ()printUsers معیارهای انتخاب کاربران نیست بلکه صرفا انتخاب کاربران براساس معیارهایی است که برای آن تعیین می‌کنیم. به این منظور اینترفیس CheckUser را که حاوی یک متد ()test است به صورت زیر تعریف می‌کنیم.

public static void printUsers(List<User> users, int age) {
for (User u : users) {
if (u.getAge() >= age) {
u.printDetails();
}
}
}

متد test() در اینترفیس فوق، یک آبجکت Person دریافت می‌کند و با برگرداندن مقدار True یا False انطباق آن کاربر با معیارها را اعلان می‌کند. به این ترتیب می‌توان پیاده سازی‌های مختلفی از این اینترفیس مشخص کرد که هریک براساس ضابطه یا معیارهای خاصی کاربران را انتخاب می‌کند. به عنوان مثال کلاس زیر، کاربران را براساس جنسیت (مرد بودن) و حداقل سن ۱۸ و حداکثر سن ۲۵ انتخاب می‌کند.

public class CheckUserByAgeRange implements CheckUser {
public boolean test(User u) {
return u.gender == User.Sex.MALE &&
u.getAge() >= 18 &&
u.getAge() <= 25;
}
}

 با وجود اینترفیس CheckUser، متد ()printUsers به صورت زیر تغییر می‌کند.

public static void printUsers(
List<User> users, CheckUser checker) {
for (User u : users) {
if (checker.test(u)) {
u.printDetails();
}
}
}

 در پیاده سازی جدید متد ()printUsers ، علاوه بر لیست کاربران یک آبجکت از اینترفیس CheckUser نیز به عنوان پارامتر ارسال شده تا به عنوان معیاری در انتخاب کاربران استفاده شود. حال برای فراخوانی این متد باید کدی مشابه کد زیر نوشته شود.

printUsers(users, new CheckUserByAgeRange());

 در اینجا با کد تمیزتر و قابل فهم‌تری مواجه هستیم که البته بی‌هزینه نیز نبوده است. هزینه آن تولید اینترفیس CheckUser و کلاس CheckUserByAgeRange است. با نگاهی دقیق‌تر به کد فوق، متوجه می‌شویم که می‌توان از پیاده سازی کلاس CheckUserByAgeRange صرفنظر کرد و به جای آن از کلاس بی‌نام بهره برد.

printUsers(
users,
new CheckUser() {
public boolean test(User u) {
return u.getGender() == User.Sex.MALE
&& u.getAge() >= 18
&& u.getAge() <= 25;
}
}
);

که در نتیجه کد ساده‌تری خواهیم داشت.

اینترفیس functional

در مثال فوق، یک اینترفیس به نام CheckUser پیاده سازی کردیم. قبل از آن نیز در توضیح کلاس‌های بی‌نام اینترفیس Listener را پیاده سازی کردیم. هر دوی این اینترفیس‌ها فقط یک متد دارند که اساس کارکرد آنها را تشکیل می‌دهند، به این اینترفیس‌ها «اینترفیس‌های Functional» گفته می‌شود. از آنجاییکه این اینترفیس‌ها فقط یک متد برای پیاده سازی دارند، می‌توان با بکاز نوشتن نام آن متد در پیاده سازی کلاس‌های بی‌نام آنها خودداری کرد. به این شکل از پیاده سازی کلاس‌های بی‌نام که در آن نام متد اینترفیس آورده نمی‌شود و منجر به کد قابل فهم‌تر و ساده‌تری می‌شود عبارت‌های لامبدا گفته می‌شود. بر همین اساس متد ()printUsers به شکل زیر در می‌آید.

printPersons(
users,
(User u) -> u.getGender() == User.Sex.MALE
&& u.getAge() >= 18
&& u.getAge() <= 25
);

 معمولا از علامت @FunctionalInterface برای علامت‌گذاری این اینترفیس‌ها استفاده می‌شود، به این ترتیب کامپایلر جاوا در هنگام کامپایل این اینترفیس از وجود فقط و فقط یک متد در این اینترفیس اطمینان حاصل می‌کند و در غیر اینصورت خطای کامپایل می‌دهد.

@FunctionalInterface
public interface CheckUser {
boolean test(User u);
}

 یکی از نکات جالب در مورد اینترفیس‌های Functional این است که مجموعه‌ای از اینترفیس‌های Functional که کاربرد عمومی دارند در پکیج Java.util.function پیاده سازی شده‌اند که می‌توانید در برنامه‌های خود استفاده کنید. به عنوان نمونه، اینترفیس Predicate<T> یکی از اینترفیس‌هاست که به صورت زیر تعریف شده است.

package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

 که می‌توانید به جای اینترفیس CheckUser که قبلا پیاده سازی کرده‌اید کنید.

public static void printUsersWithPredicate(
List<User> users, Predicate<User> checker) {
for (User u : users) {
if (checker.test(u)) {
u.printDetails();
}
}
}

 فراخوانی متد فوق بر مبنای عبارتهای لامبدا به صورت زیر خواهد بود.

printUsersWithPredicate(
users,
u -> u.getGender() == User.Sex.MALE
&& u.getAge() >= 18
&& u.getAge() <= 25
);

 حال که با اصول عبارت‌های لامبدا آشنا شدید اجازه دهید با معرفی چند اینترفیس functional دیگر مثال خود را تکمیل کنیم.
تا اینجای کار در تمام پیاده سازی‌های متد ()printUsers ، وقتی کاربری حائز شرایط برای انتخاب شدن بود متد ()printDetails آن کاربر را فراخوانی کردیم. این در حالیست که مایلیم مشابه معیارهای انتخاب کاربر که توسط یک آبجکت جداگانه (آبجکتی از Predicate<User>) مشخص گردید، عملیات پس از انتخاب کاربر نیز توسط آبجکت جداگانه‌ای ارائه گردد. خوشبختانه مشابه اینترفیس Predicate، اینترفیس Consumer نیز در جاوا ارائه شده است.

package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}

 به این ترتیب، متد ()printUsers را به صورت زیر تغییر داد.

public static void processUsers(
List<User> users,
Predicate<User> checker,
Consumer<User> consumer) {
for (User u:users) {
if (checker.test(u)) {
consumer.accept(u);
}
}
}

 فراخوانی متد فوق با عبارت‌های لامبدا به صورت زیر خواهد بود.

processUsers(
users,
u -> u.getGender() == User.Sex.MALE
&& u.getAge() >= 18
&& u.getAge() <= 25,
u -> u.printDetails()
);

 دقت کنید که در پیاده سازی اینترفیس Consumer از عبارت لامبدا u->u.printDetails() استفاده شده است.
حال تصور کنید که بخواهیم تغییری در کد فوق بوجود آوریم بگونه‌ای که پس از اینکه یک کاربر حائز شرایط بود، اطلاعات خاصی (مثلا ایمیل، شماره تلفن،…) از پروفایل وی استخراج شود تا در مرحله آخر (یعنی انجام عملیات پس از انتخاب) مورد استفاده قرار گیرد. خوشبختانه برای این منظور نیز اینترفیس Function<T, R> در جاوا وجود دارد.

package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

 متد ()apply در این اینترفیس، آبجکتی از یک جنس را دریافت می‌کند و آبجکتی از یک جنس دیگر برمی‌گرداند. به این ترتیب متد ()apply می‌تواند در مثال ما با دریافت یک آبجکت User پروفایل وی را واکاوی کند و براساس آنچه ما برای آن تعیین می‌کنیم یکی از جزییات پروفایل آن کاربر (مثلا ایمیل، شماره تلفن، …) را برگرداند.
با اضافه کردن اینترفیس Function به کد برنامه مان، متد ()printUsers به صورت زیر در خواهد آمد.

public static void processUsersWithFunction(
List<User> users,
Predicate<User> checker,
Function<User, String> mapper,
Consumer<String> consumer) {
for (User u:users) {
if (checker.test(u)) {
String data = mapper.apply(p);
consumer.accept(data);
}
}
}

 فراخوانی متد فوق با بکارگیری عبارت لامبدا به شکل زیر خواهد بود.

processUsersWithFunction(
users,
u -> u.getGender() == User.Sex.MALE
&& u.getAge() >= 18
&& u.getAge() <= 25,
u -> u.getEmailAddress(),
email -> System.out.println(email)
);

 تصور کنید که متد ()processUsersWithFunction را بخواهیم به شکل عمومی درآوریم به گونه‌ای که بتواند با هر آبجکت دیگری علاوه بر User نیز کار کند. در اینصورت با استفاده از آنچه در فصل Generics با آن آشنا شدید این متد به شکل زیر در خواهد آمد.

public static <X, Y> void processElements(
Iterable<X> source,
Predicate<X> tester,
Function <X, Y> mapper,
Consumer<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}

 فراخوانی متد فوق برای آبجکت‌های User به صورت زیر خواهد بود.

processElements(
users,
u -> u.getGender() == User.Sex.MALE
&& u.getAge() >= 18
&& u.getAge() <= 25,
u -> u.getEmailAddress(),
email -> System.out.println(email)
);

چه رتبه ای می‌دهید؟

میانگین 3.5 / 5. از مجموع 2

اولین نفر باش

title sign
معرفی نویسنده
مقالات
6 مقاله توسط این نویسنده
محصولات
5 دوره توسط این نویسنده

احمدرضا صدیقی متخصص و معمار ارشد جاوا است. از دیگر سوابق حرفه ای او می توان به:معمار ارشد در حوزه جاوا مربوط به پروژه دانشگاه علوم پزشکی، معمار ارشد در حوزه جاوا مربوط به پروژه شرکت خبره پردا، معمار ارشد در حوزه جاوا مربوط به پروژه شرکت کیاتک بنیا، معمار ارشد در حوزه جاوا مربوط به پروژه دانشگاه مالک اشتر، مشاور پروژه‌ ملی طرح جامع مالیاتی، مشاور پروژه‌ ملی وزارت بهداشت، مشاور پروژه‌ بانک ملت، مولف مجموعه کتاب‌های جاوا (فارسی و انگلیسی)، بیش از ۱۲ سال سابقه تدریس جاوا، ارائه فریم‌ورک تخصصی جاوا (اطلس) اشاره کرد.

title sign
دیدگاه کاربران