ממשקים פונקציונליים ב- Java 8

1. הקדמה

מאמר זה הוא מדריך לממשקים פונקציונליים שונים הקיימים ב- Java 8, מקרי השימוש הכלליים בהם והשימוש בהם בספריית JDK הרגילה.

2. Lambdas בג'אווה 8

Java 8 הביא לשיפור תחבירי חדש ועוצמתי בצורת ביטויים למבדה. למבדה היא פונקציה אנונימית שניתן לטפל בה כאזרח שפה ממדרגה ראשונה, למשל להעביר או להחזיר אותה משיטה.

לפני Java 8, בדרך כלל היית יוצר מחלקה לכל מקרה בו אתה צריך לתמצת חתיכת פונקציונליות אחת. זה מרמז על הרבה קוד מיותר לדוד כדי להגדיר משהו ששימש ייצוג פונקציות פרימיטיבי.

Lambdas, ממשקים פונקציונליים ושיטות עבודה מומלצות לעבודה איתם, באופן כללי, מתוארים במאמר "Lambda ביטויים וממשקים פונקציונליים: טיפים ושיטות עבודה מומלצות". מדריך זה מתמקד בכמה ממשקים פונקציונליים מסוימים הקיימים ב- java.util.function חֲבִילָה.

3. ממשקים פונקציונליים

מומלץ לכל הממשקים הפונקציונליים להיות אינפורמטיבי @FunctionalInterface ביאור. זה לא רק מתקשר באופן ברור למטרת ממשק זה, אלא גם מאפשר למהדר ליצור שגיאה אם ​​הממשק המאושר אינו עומד בתנאים.

כל ממשק עם SAM (שיטת תקציר יחיד) הוא ממשק פונקציונליוניתן להתייחס ליישום שלה כביטויים למבדה.

שים לב ש- Java 8 בְּרִירַת מֶחדָל שיטות אינן תַקצִיר ואל תספור: ממשק פונקציונלי עדיין יכול להכיל מספר רב בְּרִירַת מֶחדָל שיטות. אתה יכול להתבונן בכך על ידי התבוננות ב פונקציות תיעוד.

4. פונקציות

המקרה הפשוט והכללי ביותר של למבה הוא ממשק פונקציונלי עם שיטה שמקבלת ערך אחד ומחזירה ערך אחר. פונקציה זו של טיעון יחיד מיוצגת על ידי ה- פוּנקצִיָה ממשק שעליו פרמטרים לפי סוגי הארגומנט שלו וערך החזרה:

פונקציית ממשק ציבורי {...}

אחד השימושים של פוּנקצִיָה הקלד בספרייה הרגילה Map.computeIfAbsent שיטה המחזירה ערך ממפה לפי מפתח אך מחשבת ערך אם מפתח כבר לא קיים במפה. כדי לחשב ערך, הוא משתמש ביישום הפונקציה שעבר:

Map nameMap = HashMap חדש (); ערך שלם = nameMap.computeIfAbsent ("ג'ון", s -> s.length ());

ערך, במקרה זה, יחושב על ידי החלת פונקציה על מקש, יוכנס למפה ויוחזר גם משיחת שיטה. דרך אגב, אנו עשויים להחליף את הלמבה בהפניה לשיטה שתואמת את סוגי הערכים שהועברו והוחזרו.

זכור שאובייקט עליו מופעלת השיטה הוא למעשה הטיעון הראשון הגלום של שיטה, המאפשר השלכת שיטת מופע אורך התייחסות לא פוּנקצִיָה מִמְשָׁק:

ערך שלם = nameMap.computeIfAbsent ("ג'ון", מחרוזת :: אורך);

ה פוּנקצִיָה לממשק יש גם ברירת מחדל לְהַלחִין שיטה המאפשרת לשלב מספר פונקציות לאחת ולבצע אותן ברצף:

פונקציה intToString = אובייקט :: toString; ציטוט פונקציה = s -> "'" + s + "'"; פונקציה quoteIntToString = quote.compose (intToString); assertEquals ("'5'", quoteIntToString.apply (5));

ה quoteIntToString פונקציה היא שילוב של ציטוט פונקציה מוחלת על תוצאה של intToString פוּנקצִיָה.

5. התמחויות פונקציות פרימיטיביות

מכיוון שסוג פרימיטיבי לא יכול להיות טיעון מסוג גנרי, ישנן גרסאות ל- פוּנקצִיָה ממשק לסוגים הפרימיטיביים הנפוצים ביותר לְהַכפִּיל, int, ארוך, והשילובים שלהם בסוגי ויכוח והחזרה:

  • IntFunction, LongFunction, DoubleFunction: ארגומנטים הם מהסוג שצוין, סוג ההחזרה הוא פרמטרי
  • ToIntFunction, ToLongFunction, ToDoubleFunction: סוג ההחזרה הוא מהסוג שצוין, הארגומנטים הם פרמטרים
  • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction - שיש גם טיעון וגם סוג החזרה המוגדרים כסוגים פרימיטיביים, כפי שצוין בשמותיהם

אין ממשק פונקציונאלי מהקופסה לפונקציה שלוקחת a קצר ומחזירה א בתים, אבל שום דבר לא מונע ממך לכתוב בעצמך:

ממשק ציבורי @FunctionalInterface ShortToByteFunction {byte applyAsByte (short s); }

כעת נוכל לכתוב שיטה שהופכת מערך של קצר למערך של בתים באמצעות כלל המוגדר על ידי ShortToByteFunction:

בתים ציבוריים [] transformArray (מערך [] קצר, פונקציית ShortToByteFunction) {בייט [] transformedArray = בתים חדשים [array.length]; עבור (int i = 0; i <array.length; i ++) {transformedArray [i] = function.applyAsByte (array [i]); } להחזיר transformedArray; }

כך נוכל להשתמש בו כדי להפוך מערך מכנסיים קצרים למערך בתים כפול 2:

קצר [] מערך = {(קצר) 1, (קצר) 2, (קצר) 3}; בתים [] transformedArray = transformArray (מערך, s -> (בתים) (s * 2)); בתים [] expectArray = {(בתים) 2, (בתים) 4, (בתים) 6}; assertArrayEquals (expectArray, transformedArray);

6. התמחויות פונקציה דו-ארציות

כדי להגדיר lambdas עם שני טיעונים, עלינו להשתמש בממשקים נוספים המכילים "דוּ" מילת מפתח בשמותיהן: BiFunction, ToDoubleBiFunction, ToIntBiFunction, ו ToLongBiFunction.

BiFunction יש גם ארגומנטים וגם סוג החזרה שנוצר, בעוד ToDoubleBiFunction ואחרים מאפשרים לך להחזיר ערך פרימיטיבי.

אחת הדוגמאות האופייניות לשימוש בממשק זה בממשק ה- API הסטנדרטי היא ב- Map.replaceAll שיטה, המאפשרת להחליף את כל הערכים במפה בערך מחושב כלשהו.

בואו נשתמש ב- BiFunction יישום שמקבל מפתח וערך ישן לחישוב ערך חדש למשכורת ולהחזרתו.

משכורות מפה = HashMap חדש (); משכורות.פלט ("ג'ון", 40000); משכורות.פיט ("פרדי", 30000); משכורות.פלט ("שמואל", 50000); משכורות.מקום כל ((שם, oldValue) -> name.equals ("פרדי")? oldValue: oldValue + 10000);

7. ספקים

ה ספק ממשק פונקציונלי הוא עוד אחד פוּנקצִיָה התמחות שאינה דורשת שום טיעון. זה משמש בדרך כלל לייצור עצלים של ערכים. למשל, בואו נגדיר פונקציה בריבוע a לְהַכפִּיל ערך. הוא לא יקבל ערך עצמו, אלא א ספק של ערך זה:

ריבוע כפול ציבוריLazy (ספק lazyValue) {החזר Math.pow (lazyValue.get (), 2); }

זה מאפשר לנו ליצור בעצלתיים את הטיעון להפעלת פונקציה זו באמצעות a ספק יישום. זה יכול להיות שימושי אם הדור של טיעון זה לוקח זמן רב. אנו מדמים זאת באמצעות זה של גויאבה שינה ללא הפרעה שיטה:

ספק lazyValue = () -> {Uninterruptibles.sleepUninterruptibly (1000, TimeUnit.MILLISECONDS); החזר 9d; }; ערך כפול קוואדרד = squareLazy (lazyValue);

מקרה שימוש אחר עבור הספק הוא הגדרת לוגיקה לייצור רצפים. כדי להדגים זאת, בואו נשתמש בסטטי Stream.generate שיטה ליצור זרם של מספרי פיבונאצ'י:

int [] fibs = {0, 1}; זרם פיבונאצי = Stream.generate (() -> {int result = fibs [1]; int fib3 = fibs [0] + fibs [1]; fibs [0] = fibs [1]; fibs [1] = fib3; תוצאת החזרה;});

הפונקציה שעוברת ל Stream.generate השיטה מיישמת את ספק ממשק פונקציונלי. שימו לב שכדי להיות שימושי כגנרטור, ה- ספק בדרך כלל זקוק למצב חיצוני כלשהו. במקרה זה, מצבו מורכב משני מספרי רצף פיבונאצ'י אחרונים.

כדי ליישם את המצב הזה, אנו משתמשים במערך במקום בכמה משתנים, כי כל המשתנים החיצוניים המשמשים בתוך הלמדה צריכים להיות סופיים באופן יעיל.

התמחויות אחרות של ספק ממשק פונקציונלי כולל ספק בוליאני, DoubleSupplier, LongSupplier ו ספקית Int, שסוגי ההחזרה שלהם הם פרימיטיבים תואמים.

8. צרכנים

בניגוד ל ספק, ה צרכן מקבל טיעון שנוצר ולא מחזיר דבר. זו פונקציה המייצגת תופעות לוואי.

לדוגמה, בואו לברך את כולם ברשימת שמות על ידי הדפסת הברכה במסוף. למבדה עבר ל List.forEach השיטה מיישמת את צרכן ממשק פונקציונלי:

שמות רשימה = Arrays.asList ("ג'ון", "פרדי", "סמואל"); names.forEach (שם -> System.out.println ("שלום" + שם));

ישנן גם גרסאות מיוחדות של צרכןDoubleConsumer, IntConsumer ו LongConsumer - המקבלים ערכים פרימיטיביים כוויכוחים. מעניין יותר הוא BiConsumer מִמְשָׁק. אחד ממקרי השימוש בו הוא איטרציה דרך ערכי המפה:

גילאי מפה = HashMap חדש (); age.put ("ג'ון", 25); age.put ("פרדי", 24); age.put ("שמואל", 30); ages.forEach ((שם, גיל) -> System.out.println (שם + "הוא" + גיל + "שנים"));

קבוצה נוספת של מתמחים BiConsumer גרסאות מורכבות מ ObjDoubleConsumer, ObjIntConsumer, ו ObjLongConsumer המקבלים שני טיעונים שאחד מהם נוצר, ואחר הוא סוג פרימיטיבי.

9. מנבא

בלוגיקה מתמטית, פרדיקט הוא פונקציה המקבלת ערך ומחזירה ערך בוליאני.

ה לְבַסֵס ממשק פונקציונלי הוא התמחות של פוּנקצִיָה שמקבל ערך מופק ומחזיר בוליאני. מקרה שימוש טיפוסי של לְבַסֵס למבדה הוא לסנן אוסף ערכים:

שמות רשימה = Arrays.asList ("אנג'לה", "אהרון", "בוב", "קלייר", "דייוויד"); רשימת namesWithA = names.stream () .filter (שם -> name.startsWith ("A")) .collect (Collectors.toList ());

בקוד שלמעלה אנו מסננים רשימה באמצעות ה- זרם API ושמור רק שמות שמתחילים באות "A". לוגיקת הסינון נסתרת ב- לְבַסֵס יישום.

כמו בכל הדוגמאות הקודמות, ישנן IntPredicate, DoublePredicate ו LongPredicate גרסאות של פונקציה זו המקבלות ערכים פרימיטיביים.

10. מפעילים

מַפעִיל ממשקים הם מקרים מיוחדים של פונקציה שמקבלים ומחזירים את אותו סוג ערך. ה UnaryOperator ממשק מקבל טיעון יחיד. אחד ממקרי השימוש בו ב- API של אוספים הוא להחליף את כל הערכים ברשימה בערכים מחושבים מאותו סוג:

שמות רשימה = Arrays.asList ("bob", "josh", "megan"); names.replaceAll (שם -> name.toUpperCase ());

ה List.replaceAll הפונקציה מחזירה בָּטֵל, מכיוון שהוא מחליף את הערכים במקום. כדי להתאים למטרה, על הלמבה המשמשת לשינוי ערכי הרשימה להחזיר את אותו סוג התוצאה כפי שהוא מקבל. זו הסיבה ש UnaryOperator שימושי כאן.

כמובן, במקום שם -> name.toUpperCase (), אתה יכול פשוט להשתמש בהתייחסות לשיטה:

names.replaceAll (מחרוזת :: toUpperCase);

אחד ממקרי השימוש המעניינים ביותר של א BinaryOperator היא פעולת צמצום. נניח שאנחנו רוצים לצבור אוסף של מספרים שלמים בסכום של כל הערכים. עם זרם ממשק API, נוכל לעשות זאת באמצעות אספן, אך דרך כללית יותר לעשות זאת תהיה שימוש ב- לְהַפחִית שיטה:

ערכי רשימה = Arrays.asList (3, 5, 8, 9, 12); int sum = values.stream () .reduce (0, (i1, i2) -> i1 + i2); 

ה לְהַפחִית השיטה מקבלת ערך צבר ראשוני ו- BinaryOperator פוּנקצִיָה. הטיעונים של פונקציה זו הם צמד ערכים מאותו סוג, ופונקציה עצמה מכילה לוגיקה להצטרפותם לערך יחיד מאותו סוג. הפונקציה שעברה חייבת להיות אסוציאטיבית, שמשמעותו שסדר צבירת הערך אינו משנה, כלומר התנאי הבא צריך להחזיק:

op.apply (a, op.apply (b, c)) == op.apply (op.apply (a, b), c)

הקניין האסוציאטיבי של א BinaryOperator פונקציית המפעיל מאפשרת להקביל בקלות את תהליך ההפחתה.

כמובן, יש גם התמחויות של UnaryOperator ו BinaryOperator שניתן להשתמש בהם עם ערכים פרימיטיביים, כלומר DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, ו LongBinaryOperator.

11. ממשקים פונקציונליים מדור קודם

לא כל הממשקים הפונקציונליים הופיעו ב- Java 8. ממשקים רבים מגירסאות קודמות של Java תואמים את האילוצים של a ממשק פונקציונלי ויכול לשמש כמבדות. דוגמא בולטת היא ניתן לרוץ ו ניתן להתקשר ממשקים המשמשים בממשקי API מקבילים. ב- Java 8 ממשקים אלה מסומנים גם ב- @FunctionalInterface ביאור. זה מאפשר לנו לפשט מאוד את קוד המקביל:

חוט הברגה = שרשור חדש (() -> System.out.println ("שלום משרשור אחר")); thread.start ();

12. מסקנה

במאמר זה, תיארנו ממשקים פונקציונליים שונים הקיימים ב- Java 8 API שיכולים לשמש ביטויים למבדה. קוד המקור של המאמר זמין באתר GitHub.