JavaRush /Java блог /Random UA /Функціональні інтерфейси в Java
Professor Hans Noodles
41 рівень

Функціональні інтерфейси в Java

Стаття з групи Random UA
Вітання! У квесті Java Syntax Pro ми вивчали лямбда-вирази та говорабо, що це ніщо інше як реалізація функціонального методу з функціонального інтерфейсу. Інакше кажучи, це реалізація якогось анонімного (невідомого) класу, його нереалізованого методу. І якщо в лекціях курсу ми заглиблювалися в маніпуляції з лямбда-виразами, зараз розглянемо, так би мовити, зворотний бік: а саме ці інтерфейси. Функціональні інтерфейси в 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;
   }
}
Отримавши цей метод, компілятор не став лаятись, а значить наш інтерфейс все ще функціональний.
  • default методи
До появи 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("Текущий об'єкт - " + t.toString());
   }
}
Знову ж таки ми бачимо, що компілятор не почав лаятися, і ми не вийшли за обмеження функціонального інтерфейсу.
  • методи класу Object
У лекції Порівняння об'єктів ми розповідали, що всі класи успадковуються від класу 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("Текущий об'єкт - " + 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

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

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

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

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
PS: якщо в рядок ми передамо не лише цифри, а й інші символи, вилетить exception - 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.

Метод з Predicate

Наприклад візьмемо метод класу Streamfilter, який як аргумент приймає Predicateі повертає Streamлише з тими елементами, які задовольняють умові Predicate. У контексті Stream-а це означає, що він пропускає лише ті елементи, які повертають 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.

Метод із Consumer

Одним з методом 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складатиметься з елементів "Elena", "John", "Alex", "Jim", "Sara". Також є метод, що часто використовується foreach, який аналогічний методу peek, але різниця полягає в тому, що він кінцевий - термінальний.

Метод із Supplier

Прикладом методу 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, інакше програма виводила рандомні імена в консоль нескінченно.

Метод з Function

Типовий приклад методу в Streamаргументом Function- метод map, який приймає елементи одного типу, щось з ними робить і передає далі, але це вже можуть бути елементи іншого типу. Як може виглядати приклад Functionз 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 Java!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ