سلام! در جستجوی Java Syntax Pro، ما عبارات لامبدا را مورد مطالعه قرار دادیم و گفتیم که آنها چیزی بیش از اجرای یک روش تابعی از یک رابط کاربردی نیستند. به عبارت دیگر، این پیاده سازی یک کلاس ناشناس (ناشناخته) است، روش تحقق نیافته آن. و اگر در سخنرانی های دوره به دستکاری با عبارات لامبدا پرداختیم، اکنون به اصطلاح، طرف دیگر را در نظر خواهیم گرفت: یعنی همین رابط ها. نسخه هشتم جاوا مفهوم رابط های کاربردی را معرفی کرد . این چیه؟ یک رابط با یک روش اجرا نشده (انتزاعی) کاربردی در نظر گرفته می شود. بسیاری از رابطهای خارج از جعبه تحت این تعریف قرار میگیرند، مانند رابطی که قبلاً مورد بحث قرار گرفت محمول
مصرف كننده
تامین کننده
تابع
UnaryOperator
Comparator
. و همچنین رابط هایی که خودمان ایجاد می کنیم، مانند:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
ما یک رابط داریم که وظیفه آن تبدیل اشیاء از یک نوع به اشیاء نوع دیگر (نوعی آداپتور) است. حاشیه نویسی @FunctionalInterface
چیز فوق العاده پیچیده یا مهمی نیست، زیرا هدف آن این است که به کامپایلر بگوید که این رابط کاربردی است و نباید بیش از یک روش داشته باشد. اگر یک رابط با این حاشیه نویسی بیش از یک روش اجرا نشده (انتزاعی) داشته باشد، کامپایلر از این رابط عبور نمی کند، زیرا آن را به عنوان کد اشتباه درک می کند. رابط های بدون این حاشیه نویسی را می توان کاربردی در نظر گرفت و کار خواهد کرد، اما @FunctionalInterface
این چیزی بیش از بیمه اضافی نیست. برگردیم سر Comparator
کلاس اگر به کد (یا مستندات ) آن نگاه کنید ، میبینید که بیش از یک روش دارد. سپس میپرسید: پس چگونه میتوان آن را یک رابط کاربردی در نظر گرفت؟ رابط های انتزاعی می توانند متدهایی داشته باشند که در محدوده یک روش واحد نیستند:
- ایستا
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
پس از دریافت این روش، کامپایلر شکایتی نکرد، به این معنی که رابط ما هنوز کار می کند.
- روش های پیش فرض
default
:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
}
باز هم می بینیم که کامپایلر شروع به شکایت نکرد و از محدودیت های رابط عملکردی فراتر نرفته ایم.
- متدهای کلاس شی
Object
. این در مورد رابط ها صدق نمی کند. اما اگر یک متد انتزاعی در اینترفیس داشته باشیم که امضا را با برخی از متدهای کلاس مطابقت دهد Object
، چنین متد (یا متدهایی) محدودیت رابط عملکردی ما را نمیشکند:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
default void writeToConsole(T t) {
System.out.println("Текущий an object - " + t.toString());
}
boolean equals(Object obj);
}
و دوباره، کامپایلر ما شکایت نمی کند، بنابراین رابط Converter
هنوز هم کاربردی در نظر گرفته می شود. حال سوال این است: چرا باید خودمان را به یک روش اجرا نشده در یک رابط کاربردی محدود کنیم؟ و سپس تا بتوانیم آن را با استفاده از لامبدا پیاده سازی کنیم. بیایید با یک مثال به این موضوع نگاه کنیم Converter
. برای انجام این کار، بیایید یک کلاس ایجاد کنیم Dog
:
public class Dog {
String name;
int age;
int weight;
public Dog(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
و مشابه آن Raccoon
(راکون):
public class Raccoon {
String name;
int age;
int weight;
public Raccoon(final String name, final int age, final int weight) {
this.name = name;
this.age = age;
this.weight = weight;
}
}
فرض کنید یک شی داریم Dog
و باید یک شی را بر اساس فیلدهای آن ایجاد کنیم Raccoon
. یعنی Converter
یک شی از یک نوع را به دیگری تبدیل می کند. چگونه به نظر خواهد رسید:
public static void main(String[] args) {
Dog dog = new Dog("Bobbie", 5, 3);
Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);
Raccoon raccoon = converter.convert(dog);
System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
وقتی آن را اجرا می کنیم، خروجی زیر را به کنسول دریافت می کنیم:
Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
و این بدان معنی است که روش ما به درستی کار کرده است.
رابط های کاربردی پایه جاوا 8
خوب، اکنون اجازه دهید به چندین رابط کاربردی که جاوا 8 برای ما آورده است و به طور فعال در ارتباط با Stream API استفاده می شود، نگاه کنیم.محمول
Predicate
- یک رابط کاربردی برای بررسی اینکه آیا یک شرط خاص برآورده شده است یا خیر. اگر شرط برآورده شود، برمی گردد true
، در غیر این صورت - false
:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
به عنوان مثال، ایجاد یک را در نظر بگیرید Predicate
که برابری تعدادی از نوع را بررسی می کند Integer
:
public static void main(String[] args) {
Predicate<Integer> isEvenNumber = x -> x % 2==0;
System.out.println(isEvenNumber.test(4));
System.out.println(isEvenNumber.test(3));
}
خروجی کنسول:
true
false
مصرف كننده
Consumer
(از انگلیسی - "مصرف کننده") - یک رابط عملکردی که یک شی از نوع T را به عنوان آرگومان ورودی می گیرد، برخی اقدامات را انجام می دهد، اما چیزی را بر نمی گرداند:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
به عنوان مثال، را در نظر بگیرید که وظیفه آن خروجی دادن یک تبریک به کنسول با آرگومان رشته ای است: Consumer
public static void main(String[] args) {
Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
greetings.accept("Elena");
}
خروجی کنسول:
Hello Elena !!!
تامین کننده
Supplier
(از انگلیسی - ارائه دهنده) - یک رابط کاربردی که هیچ آرگومان را نمی گیرد، اما یک شی از نوع T را برمی گرداند:
@FunctionalInterface
public interface Supplier<T> {
T get();
}
به عنوان مثال، را در نظر بگیرید Supplier
که نام های تصادفی را از یک لیست تولید می کند:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList .add("Elena");
nameList .add("John");
nameList .add("Alex");
nameList .add("Jim");
nameList .add("Sara");
Supplier<String> randomName = () -> {
int value = (int)(Math.random() * nameList.size());
return nameList.get(value);
};
System.out.println(randomName.get());
}
و اگر این را اجرا کنیم، نتایج تصادفی از لیست نامهای کنسول را خواهیم دید.
تابع
Function
- این رابط کاربردی یک آرگومان T را می گیرد و آن را به یک شی از نوع R می فرستد که در نتیجه برگردانده می شود:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
به عنوان مثال، بیایید اعداد را از فرمت رشته ای ( ) به فرمت عددی ( ) در نظر بگیریم: Function
String
Integer
public static void main(String[] args) {
Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
System.out.println(valueConverter.apply("678"));
}
وقتی آن را اجرا می کنیم، خروجی زیر را به کنسول دریافت می کنیم:
678
PS: اگر نه تنها اعداد، بلکه کاراکترهای دیگر را نیز به رشته ارسال کنیم، یک استثنا ایجاد می شود - NumberFormatException
.
UnaryOperator
UnaryOperator
- یک رابط کاربردی که یک شی از نوع T را به عنوان پارامتر می گیرد، برخی از عملیات ها را روی آن انجام می دهد و نتیجه عملیات را در قالب یک شی از همان نوع T برمی گرداند:
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
UnaryOperator
، که از روش خود apply
برای مربع یک عدد استفاده می کند:
public static void main(String[] args) {
UnaryOperator<Integer> squareValue = x -> x * x;
System.out.println(squareValue.apply(9));
}
خروجی کنسول:
81
ما به پنج رابط کاربردی نگاه کردیم. این تنها چیزی نیست که با جاوا 8 در دسترس ما است - اینها رابط های اصلی هستند. بقیه موارد موجود آنالوگ های پیچیده آنها هستند. لیست کامل را می توان در اسناد رسمی Oracle یافت .
رابط های کاربردی در Stream
همانطور که در بالا توضیح داده شد، این رابط های کاربردی به شدت با Stream API همراه هستند. میپرسی چطور؟ و به طوری که بسیاری از روش هاStream
به طور خاص با این رابط های کاربردی کار می کنند. بیایید به نحوه استفاده از رابط های کاربردی در Stream
.
روش با محمول
برای مثال، بیایید متد کلاس را در نظر بگیریمStream
- filter
که به عنوان آرگومان در نظر گرفته میشود Predicate
و Stream
فقط عناصری را برمیگرداند که شرط را برآورده میکنند Predicate
. در زمینه Stream
-a، این بدان معنی است که فقط از عناصری عبور می کند که وقتی از آنها در یک متد رابط true
استفاده می کنید، برگردانده می شوند . این همان چیزی است که مثال ما برای آن به نظر می رسد ، اما برای فیلتری از عناصر در : test
Predicate
Predicate
Stream
public static void main(String[] args) {
List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.filter(x -> x % 2==0)
.collect(Collectors.toList());
}
در نتیجه، فهرست evenNumbers
از عناصر {2، 4، 6، 8} تشکیل خواهد شد. و همانطور که به یاد داریم، collect
تمام عناصر را در یک مجموعه خاص جمع آوری می کند: در مورد ما، در List
.
روش با مصرف کننده
یکی از روشهاییStream
که از رابط کاربردی استفاده میکند Consumer
، این است peek
. این چیزی است که مثال ما برای Consumer
in شبیه خواهد بود Stream
:
public static void main(String[] args) {
List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
.peek(x -> System.out.println("Hello " + x + " !!!"))
.collect(Collectors.toList());
}
خروجی کنسول:
Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
اما از آنجایی که این روش peek
با کار میکند Consumer
، تغییر رشتههای در Stream
رخ نمیدهد، بلکه با عناصر اصلی peek
باز میگردد Stream
: همان چیزی که به آن رسیدند. بنابراین، فهرست peopleGreetings
شامل عناصر "النا"، "جان"، "الکس"، "جیم"، "سارا" خواهد بود. یک روش معمولی نیز وجود دارد foreach
که مشابه روش است peek
، اما تفاوت آن در پایانی بودن آن است.
روش با تامین کننده
نمونهای از روشیStream
که از رابط عملکردی استفاده میکند ، Supplier
این است generate
که یک دنباله بینهایت بر اساس رابط عملکردی ارسال شده به آن ایجاد میکند. بیایید از مثال خود Supplier
برای چاپ پنج نام تصادفی در کنسول استفاده کنیم:
public static void main(String[] args) {
ArrayList<String> nameList = new ArrayList<>();
nameList.add("Elena");
nameList.add("John");
nameList.add("Alex");
nameList.add("Jim");
nameList.add("Sara");
Stream.generate(() -> {
int value = (int) (Math.random() * nameList.size());
return nameList.get(value);
}).limit(5).forEach(System.out::println);
}
و این خروجی است که در کنسول دریافت می کنیم:
John
Elena
Elena
Elena
Jim
در اینجا ما از روش limit(5)
برای تعیین محدودیت برای متد استفاده کردیم generate
، در غیر این صورت برنامه نامهای تصادفی را به طور نامحدود در کنسول چاپ میکرد.
روش با تابع
یک مثال معمولی از یک روش باStream
آرگومان Function
، متدی است map
که عناصر یک نوع را می گیرد، کاری با آنها انجام می دهد و آنها را منتقل می کند، اما اینها قبلاً می توانند عناصر نوع دیگری باشند. یک مثال با Function
in چگونه ممکن است به نظر برسد Stream
:
public static void main(String[] args) {
List<Integer> values = Stream.of("32", "43", "74", "54", "3")
.map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
در نتیجه، فهرستی از اعداد دریافت می کنیم، اما در Integer
.
روش با UnaryOperator
به عنوان متدی کهUnaryOperator
به عنوان آرگومان استفاده میکند، بیایید یک متد کلاس را در نظر بگیریم Stream
- iterate
. این روش شبیه به روش است generate
: همچنین یک دنباله بی نهایت تولید می کند اما دو آرگومان دارد:
- اولین عنصر عنصری است که تولید توالی از آن آغاز می شود.
- دومی است
UnaryOperator
که بیانگر اصل تولید عناصر جدید از عنصر اول است.
UnaryOperator
اما در روش iterate
:
public static void main(String[] args) {
Stream.iterate(9, x -> x * x)
.limit(4)
.forEach(System.out::println);
}
وقتی آن را اجرا می کنیم، خروجی زیر را به کنسول دریافت می کنیم:
9
81
6561
43046721
یعنی هر کدام از عناصر ما در خودش ضرب می شود و به همین ترتیب برای چهار عدد اول. همین! اگر پس از خواندن این مقاله یک قدم به درک و تسلط بر Stream API در جاوا نزدیک شوید، عالی خواهد بود!
GO TO FULL VERSION