שלום! ב-Java Syntax Pro Quest, למדנו ביטויי למבדה ואמרנו שהם לא יותר מיישום של שיטה פונקציונלית מממשק פונקציונלי. במילים אחרות, זהו יישום של מחלקה אנונימית (לא ידועה), השיטה הלא ממומשת שלה. ואם בהרצאות הקורס התעמקנו במניפולציות עם ביטויי למבדה, כעת נשקול, כביכול, את הצד השני: כלומר, ממשקים אלו ממש. הגרסה השמינית של Java הציגה את הרעיון של ממשקים פונקציונליים . מה זה? ממשק עם שיטה אחת לא מיושמת (מופשטת) נחשב פונקציונלי. ממשקים רבים מחוץ לקופסה נופלים תחת הגדרה זו, כגון, למשל, הממשק שנדון קודם לכן לְבַסֵס
צרכן
ספק
פוּנקצִיָה
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
וזה אומר שהשיטה שלנו עבדה כמו שצריך.
ממשקים פונקציונליים בסיסיים של Java 8
ובכן, עכשיו בואו נסתכל על כמה ממשקים פונקציונליים ש-Java 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
נ.ב.: אם נעביר לא רק מספרים, אלא גם תווים אחרים לתוך המחרוזת, ייזרק חריג - 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
בדקנו חמישה ממשקים פונקציונליים. זה לא כל מה שזמין לנו החל מ-Java 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
ב 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