معرفی Lambda Expressionها در جاوا SE 8 همراه با بازنویسی بزرگی از API جاوا بود. کلاسهای بیشتری در JDK 8 پس از معرفی Lambda بهروزرسانی شدند تا JDK 5 پس از معرفی Generics.
به لطف تعریف بسیار سادهی Functional Interfaceها، Interfaceهای موجود زیادی بدون نیاز به تغییر Functional شدند. همین موضوع درباره کد موجود شما هم صادق است: اگر Interfaceهایی در برنامهتان دارید که قبل از جاوا SE 8 نوشته شدهاند، ممکن است بدون نیاز به دست زدن، Functional شوند و امکان پیادهسازی آنها با Lambda فراهم شود.
---java.util.functionJDK 8 همچنین پکیج جدیدی معرفی میکند: java.util.function با Functional Interfaceهایی برای استفاده در برنامهتان. این Functional Interfaceها بهطور گسترده در API جاوا هم استفاده میشوند، بهویژه در Collections Framework و Stream API. این پکیج در ماژول java.base قرار دارد.
با کمی بیش از ۴۰ Interface، این پکیج در نگاه اول ممکن است کمی ترسناک به نظر برسد. معلوم میشود حول چهار Interface اصلی سازماندهی شده. درک آنها کلید درک بقیه است.
---Supplier<T>Supplier<T>اولین Interface، Interface Supplier<T> است. به طور خلاصه، Supplier هیچ آرگومانی نمیگیرد و شیای برمیگرداند.
باید واقعاً بگوییم: Lambdaای که Interface Supplier را پیادهسازی میکند هیچ آرگومانی نمیگیرد و شیای برمیگرداند. میانبر زدن چیزها را به یاد ماندنیتر میکند، تا زمانی که گیجکننده نباشد.
این Interface واقعاً ساده است: متد default یا static ندارد، فقط یک متد سادهی get() دارد:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Lambda زیر پیادهسازی این Interface است:
Supplier<String> supplier = () -> "Hello Duke!";`
این Lambda Expression فقط رشتهی کاراکتر Hello Duke! را برمیگرداند. همچنین میتوانید Supplierای بنویسید که هر بار فراخوانی شود شی جدیدی برگرداند:
Random random = new Random(314L);
Supplier<Integer> newRandom = () -> random.nextInt(10);
for (int index = 0; index < 5; index++) {
IO.println(newRandom.get() + " ");
}
فراخوانی متد get() این Supplier، random.nextInt() را فراخوانی میکند و عدد صحیح تصادفی تولید میکند. چون بذر این مولد تصادفی روی مقدار 314L ثابت است، باید اعداد صحیح تصادفی زیر را ببینید:
1
3
5
3
0
2
توجه کنید این Lambda متغیری از حوزهی محصورکننده را Capture میکند: random و این متغیر را effectively final میکند.
Supplier<T>توجه کنید چگونه با Supplier newRandom در مثال قبلی عدد تصادفی تولید کردید:
for (int index = 0; index < 5; index++) {
IO.println(newRandom.get() + " ");
}
فراخوانی متد get() Interface Supplier Lambda شما را فراخوانی میکند.
Lambda Expressionها برای پردازش داده در برنامهها استفاده میشوند. سرعت اجرای Lambda Expression در JDK بسیار حیاتی است. هر چرخهی CPU که قابل صرفهجویی باشد باید صرفهجویی شود، چون ممکن است بهینهسازی قابلتوجهی در برنامهی واقعی باشد.
طبق این اصل، API جاوا نسخههای تخصصی و بهینهشدهی Interface Supplier<T> را هم ارائه میدهد.
ممکن است متوجه شده باشید مثال دوم ما نوع Integer را تأمین میکند، در حالی که متد Random.nextInt() یک int برمیگرداند. پس در کدی که نوشتید، دو اتفاق در زیر پرده میافتد:
int برگرداندهشده توسط Random.nextInt() ابتدا توسط مکانیزم Auto-Boxing به Integer Boxing میشود؛Integer هنگام اختصاص به متغیر nextRandom توسط مکانیزم Auto-Unboxing Unbox میشود.Auto-Boxing مکانیزمی است که طبق آن مقدار int مستقیماً به شی Integer اختصاص داده میشود:
int i = 12;
Integer integer = i;
در زیر پرده، شیای برای شما ایجاد میشود که آن مقدار را بستهبندی میکند.
Auto-Unboxing عکس آن را انجام میدهد. میتوانید Integer را به مقدار int با Unbox کردن مقدار داخلی Integer اختصاص دهید:
Integer integer = Integer.valueOf(12);
int i = integer;
این Boxing / Unboxing رایگان نیست. بیشتر اوقات، این هزینه در مقایسه با کارهای دیگر برنامهتان مثل دریافت داده از پایگاه داده یا سرویس راهدور کوچک است. اما در برخی موارد ممکن است این هزینه قابل قبول نباشد و نیاز به اجتناب از پرداخت آن داشته باشید.
خبر خوب: JDK با Interface IntSupplier راهحلی ارائه میدهد:
@FunctionalInterface
public interface IntSupplier {
int getAsInt();
}
توجه کنید میتوانید از همان کد برای پیادهسازی این Interface استفاده کنید:
Random random = new Random(314L);
IntSupplier newRandom = () -> random.nextInt();
تنها تغییر در کد برنامهتان این است که باید getAsInt() را به جای get() فراخوانی کنید:
for (int i = 0; i < 5; i++) {
int nextRandom = newRandom.getAsInt();
IO.println("next random = " + nextRandom);
}
نتیجهی اجرای این کد یکسان است، اما این بار هیچ Boxing / Unboxingای رخ نداده: این کد عملکرد بهتری نسبت به کد قبلی دارد.
JDK چهار Supplier تخصصی ارائه میدهد تا Boxing / Unboxing غیرضروری در برنامهتان اجتناب شود: IntSupplier، BooleanSupplier، LongSupplier و DoubleSupplier.
---نسخههای تخصصی بیشتری از Functional Interfaceها برای مدیریت انواع اولیه خواهید دید. قرارداد نامگذاری سادهای برای متدهای abstract آنها وجود دارد: نام متد abstract اصلی (
get()در مورد Supplier) را بگیرید و نوع بازگشتی را اضافه کنید. پس برای Interfaceهای Supplier داریم:getAsBoolean()،getAsInt()،getAsLong()، وgetAsDouble().
Consumer<T>دومین Interface، Interface Consumer<T> است. Consumer عکس Supplier عمل میکند: آرگومانی میگیرد و چیزی برنمیگرداند.
Interface کمی پیچیدهتر است: Default Methodهایی در آن وجود دارد که بعداً در این آموزش پوشش داده میشود. روی متد abstract آن تمرکز کنیم:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
// default methods removed
}
قبلاً Consumerها را پیادهسازی کردهاید:
Consumer<String> printer = s -> IO.println(s);
میتوانید مثال قبلی را با این Consumer بهروزرسانی کنید:
for (int i = 0; i < 5; i++) {
int nextRandom = newRandom.getAsInt();
printer.accept("next random = " + nextRandom);
}
فرض کنید نیاز به چاپ عدد صحیح دارید. میتوانید Consumer زیر را بنویسید:
Consumer<Integer> printer = i -> IO.println(i);`
سپس ممکن است با همان مشکل Auto-Boxing مواجه شوید. آیا این Boxing / Unboxing از نظر عملکرد در برنامهتان قابل قبول است؟
نگران نباشید اگر اینطور نیست، JDK با سه Consumer تخصصی موجود شما را پوشش میدهد: IntConsumer، LongConsumer و DoubleConsumer. متدهای abstract این سه Consumer از همان قرارداد Supplier پیروی میکنند، از آنجا که نوع بازگشتی همیشه void است، همگی accept نامیده میشوند.
سپس JDK نسخهی دیگری از Interface Consumer<T> اضافه میکند که به جای یک، دو آرگومان میگیرد، که بهطور طبیعی Interface BiConsumer<T, U> نامیده میشود:
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
// default methods removed
}
مثالی از BiConsumer:
BiConsumer<Random, Integer> randomNumberPrinter =
(random, number) -> {
for (int i = 0; i < number; i++) {
IO.println("next random = " + random.nextInt());
}
};
میتوانید از این BiConsumer برای نوشتن مثال قبلی به شکل دیگری استفاده کنید:
randomNumberPrinter.accept(new Random(314L), 5);
سه نسخهی تخصصی از Interface BiConsumer<T, U> برای مدیریت انواع اولیه وجود دارد: ObjIntConsumer<T>، ObjLongConsumer<T> و ObjDoubleConsumer<T>.
متدهای مهم زیادی به Interfaceهای Collections Framework اضافه شده که در بخش دیگری از این آموزش پوشش داده میشود. یکی از آنها Consumer<T> به عنوان آرگومان میگیرد و بسیار مفید است: متد Iterable.forEach(). مثال سادهای که همه جا خواهید دید:
List<String> strings = ...; // really any list of any kind of objects
Consumer<String> printer = s -> IO.println(s);
strings.forEach(printer);
خط آخر کد فقط Consumer را روی تمام اشیای لیست اعمال میکند. اینجا فقط آنها را یکییکی روی کنسول چاپ میکند.
این متد forEach() راهی برای دسترسی به تکرار داخلی روی تمام عناصر هر Iterable فراهم میکند و کد را خواناتر میسازد.
Predicate<T>سومین Interface، Interface Predicate<T> است. Predicate برای آزمایش یک شی استفاده میشود. برای فیلتر کردن Streamها در Stream API استفاده میشود.
متد abstract آن شیای میگیرد و مقدار boolean برمیگرداند:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// default and static methods removed
}
قبلاً مثالی از Predicate<String> دیدهاید:
Predicate<String> length3 = s -> s.length() == 3;
برای آزمایش رشتهی دادهشده، فقط کافی است متد test() Interface Predicate را فراخوانی کنید:
String word = ...; // any word
boolean isOfLength3 = length3.test(word);
IO.println("Is of length 3? " + isOfLength3);
فرض کنید نیاز به آزمایش مقادیر عدد صحیح دارید. میتوانید Predicate زیر را بنویسید:
Predicate<Integer> isGreaterThan10 = i -> i > 10;
همین موضوع درباره Consumerها، Supplier و این Predicate هم صادق است. آنچه این Predicate به عنوان آرگومان میگیرد، ارجاعی به نمونهی کلاس Integer است. راهحل JDK همانند Supplier و Consumer است: Predicateهای تخصصی. همراه با Predicate<String> سه Interface تخصصی وجود دارد: IntPredicate، LongPredicate و DoublePredicate.
پس میتوانید مثال قبلی را به این صورت بنویسید:
IntPredicate isGreaterThan10 = i -> i > 10;
میبینید سینتکس خود Lambda یکسان است، تنها تفاوت این است که i اکنون نوع int است به جای Integer.
طبق قراردادی که با Consumer<T> دیدید، JDK Interface BiPredicate<T, U> را اضافه میکند که به جای یک عنصر، دو عنصر را آزمایش میکند:
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
// default methods removed
}
مثالی از BiPredicate:
BiPredicate<String, Integer> isOfLength = (word, length) -> word.length() == length;
یکی از متدهای اضافهشده به Collections Framework Predicate میگیرد: متد removeIf(). این متد از Predicate برای آزمایش هر عنصر Collection استفاده میکند. اگر نتیجهی آزمایش true باشد، آن عنصر از Collection حذف میشود.
List<String> immutableStrings =
List.of("one", "two", "three", "four", "five");
List<String> strings = new ArrayList<>(immutableStrings);
Predicate<String> isEvenLength = s -> s.length() % 2 == 0;
strings.removeIf(isEvenLength);
IO.println("strings = " + strings);
اجرای این کد نتیجهی زیر را تولید میکند:
strings = [one, two, three]
نکاتی دربارهی این مثال:
removeIf() این Collection را تغییر میدهد.removeIf() را روی Collection غیرقابل تغییر فراخوانی کنید.Arrays.asList() Collectionای تولید میکند که مثل آرایه رفتار میکند.Function<T, R>چهارمین Interface، Interface Function<T, R> است. متد abstract یک Function شیای از نوع T میگیرد و تبدیل آن شی به نوع دیگر U را برمیگرداند:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// default and static methods removed
}
Functionها در Stream API برای نگاشت اشیا به اشیای دیگر استفاده میشوند. Predicate را میتوان نوع تخصصی Function دید که boolean برمیگرداند.
مثالی از Function که رشته میگیرد و طول آن را برمیگرداند:
Function<String, Integer> toLength = s -> s.length();
String word = ...; // any kind of word will do
int length = toLength.apply(word);
عملیات Boxing و Unboxing را در حال اجرا میبینید. اول، متد length() یک int برمیگرداند. چون Function Integer برمیگرداند، این int Boxing میشود. سپس نتیجه به متغیر length از نوع int اختصاص داده میشود، پس Integer Unbox میشود.
JDK نسخههای تخصصی Interface Function<T, R> را ارائه میدهد. هم نوع ورودی و هم نوع خروجی میتوانند چهار نوع مختلف داشته باشند: نوع پارامتریشده T، int، long و double.
یک Interface خاص وجود دارد: UnaryOperator<T> که از Function<T, T> ارث میبرد. برای نامگذاری Functionهایی استفاده میشود که آرگومانی از نوع مشخصی میگیرند و نتیجهای از همان نوع برمیگردانند.
جدول ۱۶ نوع تخصصی Function که در پکیج java.util.function وجود دارد:
میتوانید عناصر لیست را با UnaryOperator<T> تغییر دهید. متدی که این UnaryOperator را میگیرد، آن را به متد replaceAll() ارسال میکند:
List<String> strings = Arrays.asList("one", "two", "three");
UnaryOperator<String> toUpperCase = word -> word.toUpperCase();
strings.replaceAll(toUpperCase);
IO.println(strings);
اجرای این کد خروجی زیر را نمایش میدهد:
[ONE, TWO, THREE]
مثل Consumer و Predicate، Functionها هم نسخهای دارند که دو آرگومان میگیرد: BiFunction. Interface BiFunction<T, U, R> که T و U آرگومانها و R نوع بازگشتی است:
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
// default methods removed
}
مثالی از BiFunction با Lambda Expression:
BiFunction<String, String, Integer> findWordInSentence =
(word, sentence) -> sentence.indexOf(word);
UnaryOperator<T> همچنین Interface خواهری با دو آرگومان دارد: BinaryOperator<T> که از BiFunction<T, U, R> ارث میبرد.
پکیج java.util.function اکنون در جاوا مرکزی است، چون تمام Lambda Expressionهایی که در Collections Framework یا Stream API استفاده خواهید کرد یکی از Interfaceهای این پکیج را پیادهسازی میکنند.
همانطور که دیدید، این پکیج Interfaceهای زیادی دارد و پیدا کردن مسیر در آن ممکن است دشوار باشد.
اول، آنچه باید به یاد داشته باشید این است که چهار دسته Interface وجود دارد:
دوم: برخی Interfaceها نسخههایی دارند که به جای یک، دو آرگومان میگیرند: BiConsumerها، BiPredicateها و BiFunctionها.
سوم: برخی Interfaceها نسخههای تخصصی دارند تا Boxing و Unboxing اجتناب شود. به نام نوعی که میگیرند نامگذاری میشوند (مثلاً IntPredicate) یا نوعی که برمیگردانند (مثل ToLongFunction<T>). یا هر دو: IntToDoubleFunction.
آخر: امتدادهایی از Function<T, R> و BiFunction<T, U, R> برای حالتی که تمام نوعها یکسان هستند: UnaryOperator<T> و BinaryOperator<T>، با نسخههای تخصصی برای انواع اولیه.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/