ברוך הבא למדריך לדוגמאות ממשקי פונקציונליים ב-Java 8. ג'אווה תמיד הייתה שפת תכנות מונחה עצמים. מה שזה אומר הוא שהכל בתכנות ב-Java מסתובב סביב אובייקטים (למעט כמה סוגי נתונים פרימיטיביים לקלות). ב-Java אין לנו רק פונקציות, הן חלק ממחלקה ואנו צריכים להשתמש במחלקה/אובייקט כדי לקרוא לכל פונקציה.
ממשקי פונקציונליים ב-Java 8
אם נסתכל על שפות תכנות אחרות כמו C++, JavaScript; הן נקראות שפות תכנות פונקציונליות מאחר ואנו יכולים לכתוב פונקציות ולהשתמש בהן כשנדרש. חלק מהשפות האלו תומכות גם בתכנות מונחה עצמים וגם בתכנות פונקציונלי. להיות מונחה עצמים אינו רע, אך זה מביא הרבה פספוס לתוכנית. לדוגמה, נניח שעלינו ליצור מופע של Runnable. כמו כן, נהוג לעשות זאת באמצעות כיתות אנונימיות כמו בדוגמה למעלה.
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
אם תביטו בקוד לעיל, החלק האמיתי של השימוש הוא הקוד בתוך השיטה run(). כל השאר מהקוד הוא בגלל הדרך שבה מבנים תוכניות ב-Java. ממשקי פונקציונליים ב-Java 8 וביטויי Lambda עוזרים לנו לכתוב קוד קטן ונקי יותר על ידי הסרת הרבה מהקוד המיותר.
ממשק פונקציונלי של Java 8
ממשק עם בדיוק אחת אפשרות המוגדרת כממשק פונקציונלי. הוספנו את האננוטציה @FunctionalInterface
כדי שנוכל לסמן את הממשק כממשק פונקציונלי. אין חובה להשתמש בו, אך זו פרקטיקה מומלצת להשתמש בו עם ממשקי פונקציונליים כדי למנוע תוספת של שיטות נוספות בטעות. אם הממשק מודגש באננוטציה @FunctionalInterface
ונכון לנסות להוסיף יותר מאשר שיטה אחת אבסטרקטית, הוא מזריק שגיאת קומפילציה. התועלת העיקרית של ממשקי פונקציונליות של ג'אווה 8 היא שאנו יכולים להשתמש בהם באמצעות ביטויי למבדר ולהימנע משימוש במימוש של מחלקות אנונימיות כבדות. API של מערכות האוסף של ג'אווה 8 נכתב מחדש ונכנס API של הזרם החדש שמשתמש בממשקי פונקציונליות רבים. בג'אווה 8 הוגדרו ממשקי פונקציונליות רבים בחבילת java.util.function
. כמה מהממשקים הפונקציונליים השימושיים של ג'אווה 8 הם Consumer
, Supplier
, Function
ו־Predicate
. ניתן למצוא מידע נוסף עליהם בדוגמה של Java 8 Stream. java.lang.Runnable
הוא דוגמה נהדרת לממשק פונקציונלי עם שיטה אבסטרקטית יחידה run()
. קטע הקוד למטה מספק מדריך לממשקי פונקציונליות:
interface Foo { boolean equals(Object obj); }
// לא פונקציונלי מאחר ו־equals כבר הוא חבר משתנה ממשי (ממחלקת Object)
interface Comparator {
boolean equals(Object obj);
int compare(T o1, T o2);
}
// פונקציונלי מאחר ו־Comparator יש לו רק פונקציה אחת אבסטרקטית שאינה Object
interface Foo {
int m();
Object clone();
}
// לא פונקציונלי מאחר ו־השיטה Object.clone אינה ציבורית
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// פונקציונלי: שתי השיטות, אך להן אותו חתימה
interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// פונקציונלי: Y.m הוא חתימת משנה וניתן להחלפת סוג ההחזרה
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// לא פונקציונלי: אין לאף שיטה חתימת משנה של כל השיטות האבסטרקטיות
interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class > c); }
interface Z extends X, Y {}
// לא פונקציונלי: אין לאף שיטה חתימת משנה של כל השיטות האבסטרקטיות
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// שגיאת המהדר: אין שיטה שניתנת להחלפת סוג ההחזרה
interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// שגיאת המהדר: חתימות שונות, אך אותו קירוב
ביטוי למבואה
ביטויי למבואה הם הדרך שבה אנו יכולים להתמקד בתכנות פונקציונלי בעולם המונחה עצמים של ג'אווה. עצמים הם בסיס שפת התכנות ג'אווה ואין לנו אפשרות ליצור פונקציה בלי עצם, ולכן שפת ג'אווה מספקת תמיכה בשימוש בביטויי למבואה רק עם ממשקי פונקציונליים. מאחר ויש רק פונקציה אבסטרקטית אחת בממשקי הפונקציונליים, אין בלבול בהחלקת הביטוי לשיטה. התחביר של ביטויי הלמבדה הוא (פרמטר) -> (גוף). עכשיו בואו נראה איך ניתן לכתוב את ה־Runnable האנונימי מעלה באמצעות ביטוי למבואה.
Runnable r1 = () -> System.out.println("My Runnable");
בואו ננסה להבין מה קורה בביטוי למבואה שמופיע למעלה.
- Runnable הוא ממשק פונקציונלי, ולכן נוכל להשתמש בביטוי Lambda ליצירת המופע שלו.
- מכיוון שפעולת run() אינה מקבלת ארגומנטים, הביטוי Lambda שלנו גם אינו מקבל ארגומנטים.
- דומה לבלוקי if-else, אנו יכולים להימנע מסוגריים ({}), מכיוון שיש לנו ביטוי יחיד בגוף השיטה. למספר ביטויים, נצטרך להשתמש בסוגריים כמו בכל שיטות אחרות.
למה נצטרך בביטוי Lambda
-
הפחתת שורות הקוד אחד היתרונות הברורים של שימוש בביטוי Lambda הוא שהכמות של קוד נמוכה, כבר ראינו כיצד ניתן ליצור מופע של ממשק פונקציונלי בקלות באמצעות ביטוי Lambda במקום בשימוש במחלקה אנונימית.
-
תמיכה בביצוע רציפי ופרולטי יתרון נוסף של שימוש בביטוי למבדה הוא שנוכל להשתמש בתמיכה בפעולות רציפות ופרולטיות של תחביר ה-Stream API. כדי להסביר זאת, בואו נקח מקרה פשוט שבו אנו צריכים לכתוב שיטה לבדיקה האם מספר המועבר הוא מספר ראשוני או לא. לרוב, היינו כותבים את הקוד כך:
//גישה מסורתית private static boolean isPrime(int number) { if(number < 2) return false; for(int i=2; i<number; i++){ if(number % i == 0) return false; } return true; }
הבעיה עם הקוד למעלה היא שהוא רציף לגמרי, אם המספר הוא עצום מאוד אז זה ייקח זמן רב. בעיה נוספת עם הקוד היא שיש הרבה נקודות יציאה וזה לא קריא. בואו נראה איך אפשר לכתוב את אותה השיטה בעזרת ביטויי למבדה ותחביר ה-Stream API.
//גישה דקלרטיבית private static boolean isPrime(int number) { return number > 1 && IntStream.range(2, number).noneMatch( index -> number % index == 0); }
IntStream
הוא רצף של איברי int פרימיטיביים התומך בפעולות אגרגציה רציפות ופרולטיות. זהו המיתוג המיוחד ל-int שלStream
. לקריאות נוספת, ניתן גם לכתוב את השיטה כך:private static boolean isPrime(int number) { IntPredicate isDivisible = index -> number % index == 0; return number > 1 && IntStream.range(2, number).noneMatch( isDivisible); }
אם אינך מכיר את IntStream, שיטת ה- range() שלו מחזירה IntStream מסודר רציף מ-startInclusive (כולל) עד-endExclusive (לא כולל) בצעד כולל של 1. שיטת ה- noneMatch() מחזירה אם לא קיימים איברים ברצף שתואמים את המופעל. יתכן שהיא לא תבדוק את המופעל על כל האיברים אם זה לא נדרש לקביעת התוצאה.
-
העברת התנהגויות לשיטות בואו נראה איך אפשר להשתמש בביטויי למבדר (Lambda Expressions) כדי להעביר התנהגות של שיטה עם דוגמה פשוטה. נניח שיש לנו לכתוב שיטה לסכום המספרים ברשימה אם הם עונים על קריטריון נתון. אפשר להשתמש ב-Predicate ולכתוב שיטה כמו בדוגמה הבאה.
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) { return numbers.parallelStream() .filter(predicate) .mapToInt(i -> i) .sum(); }
שימוש דוגמתי:
//סכום כל המספרים sumWithCondition(numbers, n -> true) //סכום כל המספרים הזוגיים sumWithCondition(numbers, i -> i%2==0) //סכום כל המספרים הגדולים מ-5 sumWithCondition(numbers, i -> i>5)
-
יעילות גבוהה עם עצלנות עוד יתרון של שימוש בביטוי למבדה הוא הערכה עצלנית, לדוגמה נניח שנצטרך לכתוב שיטה למציאת המספר האי זוגי המרבי בטווח 3 עד 11 ולהחזיר את הריבוע שלו. נרשום קוד לשיטה כזו כך:
private static int findSquareOfMaxOdd(List<Integer> numbers) { int max = 0; for (int i : numbers) { if (i % 2 != 0 && i > 3 && i < 11 && i > max) { max = i; } } return max * max; }
התוכנית לעיל תרוץ תמיד בסדר רציף אך נוכל להשתמש ב-Stream API כדי להשיג זאת ולהרוויח מחיפוש עצלן. בואו נראה כיצד נוכל לכתוב מחדש קוד זה בדרך של תכנות פונקציונלי באמצעות Stream API וביטויי למבדה.
public static int findSquareOfMaxOdd(List<Integer> numbers) { return numbers.stream() .filter(NumberTest::isOdd) //הפונקציה מהווה ממשק פונקציונלי ו .filter(NumberTest::isGreaterThan3) // אנו משתמשים בלמבדות כדי לאתחל אותו .filter(NumberTest::isLessThan11) // במקום מחלקות משובץ אנונימיות .max(Comparator.naturalOrder()) .map(i -> i * i) .get(); } public static boolean isOdd(int i) { return i % 2 != 0; } public static boolean isGreaterThan3(int i){ return i > 3; } public static boolean isLessThan11(int i){ return i < 11; }
אם אתם מתעקשים על סימן הנקודות הכפולות (::), הוא הוכנס ב-Java 8 ומשמש ל הפניית שיטה. מהדר Java טופל את העמדת הפרמטרים לשיטה הנקראת. זו גרסה קצרה של ביטויי למבדה
i -> isGreaterThan3(i)
אוi -> NumberTest.isGreaterThan3(i)
.
דוגמאות לביטויי למבדה
למטה אני מספק קטעי קוד עבור ביטויי למבדה עם הערות קטנות המסבירות אותם.
() -> {} // No parameters; void result
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
// גוף בלוק מורכב עם מחזורים מרובים
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1 // Single inferred-type argument, same as below
x -> x+1 // Parenthesis optional for single inferred-type case
(String s) -> s.length() // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length() // Single inferred-type argument
t -> { t.start(); } // Single inferred-type argument
(int x, int y) -> x+y // Multiple declared-type parameters
(x,y) -> x+y // Multiple inferred-type parameters
(x, final y) -> x+y // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
הפניות לשיטות ולבנאי
A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
זה הכל עבור המדריך על ממשקי פונקציונל וביטויי למבדר של Java 8. אני ממליץ בחום ללמוד ולהשתמש בו מכיוון שתחביב זה חדש ל-Java וידרוש זמן עד שתבינו אותו. כדאי גם לבדוק תכונות של Java 8 כדי ללמוד על כל השיפורים והשינויים בגרסה 8 של Java.
Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces