JavaRush /בלוג Java /Random-HE /ממשקים פונקציונליים ב-Java

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

פורסם בקבוצה
שלום! ב-Java Syntax Pro Quest, למדנו ביטויי למבדה ואמרנו שהם לא יותר מיישום של שיטה פונקציונלית מממשק פונקציונלי. במילים אחרות, זהו יישום של מחלקה אנונימית (לא ידועה), השיטה הלא ממומשת שלה. ואם בהרצאות הקורס התעמקנו במניפולציות עם ביטויי למבדה, כעת נשקול, כביכול, את הצד השני: כלומר, ממשקים אלו ממש. ממשקים פונקציונליים ב-Java - 1הגרסה השמינית של Java הציגה את הרעיון של ממשקים פונקציונליים . מה זה? ממשק עם שיטה אחת לא מיושמת (מופשטת) נחשב פונקציונלי. ממשקים רבים מחוץ לקופסה נופלים תחת הגדרה זו, כגון, למשל, הממשק שנדון קודם לכן Comparator. וגם ממשקים שאנו יוצרים בעצמנו, כגון:
@FunctionalInterface
public interface Converter<T, N> {
   N convert(T t);
}
יש לנו ממשק שתפקידו להמיר אובייקטים מסוג אחד לאובייקטים של אחר (מעין מתאם). ההערה @FunctionalInterfaceאינה משהו סופר מורכב או חשוב, שכן מטרתו היא לומר למהדר שהממשק הזה פונקציונלי ואמור להכיל לא יותר משיטה אחת. אם לממשק עם הערה זו יש יותר משיטה אחת לא מיושמת (מופשטת), המהדר לא ידלג על ממשק זה, מכיוון שהוא יתפוס אותו כקוד שגוי. ממשקים ללא ביאור זה יכולים להיחשב פונקציונליים ויעבדו, אבל @FunctionalInterfaceזה לא יותר מביטוח נוסף. בוא נחזור לכיתה Comparator. אם תסתכל על הקוד שלו (או התיעוד ), תוכל לראות שיש לו הרבה יותר משיטה אחת. ואז אתה שואל: איך, אם כן, זה יכול להיחשב ממשק פונקציונלי? לממשקים מופשטים יכולים להיות שיטות שאינן בהיקף של שיטה אחת:
  • סטָטִי
מושג הממשקים מרמז שליחידת קוד נתונה לא ניתן ליישם מתודות כלשהן. אבל החל מ-Java 8, אפשר היה להשתמש בשיטות סטטיות ובמתודות ברירת מחדל בממשקים. שיטות סטטיות קשורות ישירות למחלקה ואינן מצריכות אובייקט ספציפי של אותה מחלקה כדי לקרוא למתודה כזו. כלומר, שיטות אלו משתלבות בצורה הרמונית בקונספט של ממשקים. כדוגמה, בואו נוסיף שיטה סטטית לבדיקת אובייקט עבור null למחלקה הקודמת:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
לאחר שקיבל את השיטה הזו, המהדר לא התלונן, מה שאומר שהממשק שלנו עדיין פונקציונלי.
  • שיטות ברירת מחדל
לפני Java 8, אם היינו צריכים ליצור מתודה בממשק שעבר בירושה למחלקות אחרות, נוכל ליצור רק מתודה אבסטרקטית שהוטמעה בכל מחלקה ספציפית. אבל מה אם השיטה הזו זהה לכל המחלקות? במקרה זה , לרוב נעשה שימוש בשיעורים מופשטים . אבל החל מ-Java 8, ישנה אפשרות להשתמש בממשקים עם שיטות מיושמות - שיטות ברירת המחדל. בעת בירושה של ממשק, אתה יכול לעקוף את השיטות הללו או להשאיר הכל כפי שהוא (להשאיר את ההיגיון המוגדר כברירת מחדל). בעת יצירת שיטת ברירת מחדל, עלינו להוסיף את מילת המפתח - 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 - 2

ממשקים פונקציונליים בסיסיים של 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);
}
כדוגמה, ניקח את , הממיר מספרים מתבנית מחרוזת ( ) לתבנית מספר ( ): FunctionStringInteger
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. איך, אתם שואלים? ממשקים פונקציונליים ב-Java - 3וכזה ששיטות רבות Streamעובדות במיוחד עם הממשקים הפונקציונליים הללו. בואו נסתכל כיצד ניתן להשתמש בממשקים פונקציונליים ב Stream.

שיטה עם פרדיקט

לדוגמה, ניקח את שיטת המחלקה Stream- filterאשר לוקחת כארגומנט Predicateומחזירה Streamרק את האלמנטים העומדים בתנאי Predicate. בהקשר של- Streama, פירוש הדבר שהוא עובר רק דרך אותם אלמנטים שמוחזרים 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שלוקחת אלמנטים מסוג אחד, עושה איתם משהו ומעבירה אותם הלאה, אבל אלו כבר יכולים להיות אלמנטים מסוג אחר. איך דוגמה עם Functionin עשויה להיראות 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
כלומר, כל אחד מהאלמנטים שלנו מוכפל בעצמו, וכך הלאה עבור ארבעת המספרים הראשונים. ממשקים פונקציונליים ב-Java - 4זה הכל! זה יהיה נהדר אם לאחר קריאת מאמר זה תהיו צעד אחד קרוב יותר להבנה ושליטה ב- Stream API בג'אווה!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION