Привет! В квесте Java Syntax Pro мы изучали лямбда-выражения и говорили, что это ничто иное как реализация функционального метода из функционального интерфейса. Иными словами, это реализация некоего анонимного (неизвестного) класса, его нереализованного метода. И если в лекциях курса мы углублялись в манипуляции с лямбда-выражениями, сейчас рассмотрим, так сказать, обратную сторону: а именно — эти самые интерфейсы.Функциональные интерфейсы в Java - 1В восьмой версии Java появилось понятие функциональные интерфейсы. Что же это? Функциональным считается интерфейс с одним не реализованным (абстрактным) методом. Под это определение попадают многие интерфейсы с “коробки”, такие как, например, рассмотренный ранее интерфейс Comparator. А еще — интерфейсы, которые мы создаём сами, как, например:
@FunctionalInterface
public interface Converter {
   N convert(T t);
}
У нас получился интерфейс, задача которого — преобразовывать объекты одного типа в объекты другого (такой себе адаптер). Аннотация @FunctionalInterface не является чем-то сверхсложным и важным, так как её предназначение — сообщить компилятору, что данный интерфейс функциональный и должен содержать не более одного метода. Если же в интерфейсе с данной аннотацией более одного не реализованного (абстрактного) метода, компилятор не пропустит данный интерфейс, так как будет воспринимать его как ошибочный код. Интерфейсы и без данной аннотации могут считаться функциональными и будут работать, а @FunctionalInterface является не более чем дополнительной страховкой. Вернёмся же к классу Comparator. Если заглянуть в его код (или документацию), можно увидеть, что у него есть гораздо больше одного метода. Тогда вы спросите: как же в таком случае он может считаться функциональным интерфейсом? Абстрактные интерфейсы могут иметь методы, которые не входят в ограничения одного метода:
  • статические
Концепция интерфейсов подразумевает, что данная единица кода не может иметь реализованных методов. Но начиная с Java 8 появилась возможность использовать статические и дефолтные методы в интерфейсах. Статические методы привязаны непосредственно к классу, и для вызова такого метода не нужен конкретный объект данного класса. То есть данные методы гармонично вписываются в представления об интерфейсах. В качестве примера добавим в предыдущий класс статический метод проверки объекта на null:
@FunctionalInterface
public interface Converter {

   N convert(T t);

   static  boolean isNotNull(T t){
       return t != null;
   }
}
Получив этот метод, компилятор не стал ругаться, а значит наш интерфейс все еще функциональный.
  • default методы
До появления Java 8, если бы нам было нужно создать в интерфейсе какой-то метод, который наследуется другими классами, мы могли бы создать только абстрактный метод, реализуемый в каждом конкретном классе. Но а что если этот метод будет одинаковым у всех классов? В таком случае чаще всего использовали абстрактные классы. Но начиная с Java 8 есть опция использовать интерфейсы с реализованными методами — методами по умолчанию. При наследовании интерфейса можно переопределить эти методы или же оставить всё как есть (оставить логику по умолчанию). При создании метода по умолчанию мы должны добавить ключевое слово — default:
@FunctionalInterface
public interface Converter {

   N convert(T t);

   static  boolean isNotNull(T t){
       return t != null;
   }

   default void writeToConsole(T t) {
       System.out.println("Текущий объект - " + t.toString());
   }
}
Опять же мы видим, что компилятор не начал ругаться, и мы не вышли за ограничения функционального интерфейса.
  • методы класса Object
В лекции Сравнение объектов мы рассказывали о том, что все классы наследуются от класса Object. Это не касается интерфейсов. Но если у нас в интерфейсе будет абстрактный метод, совпадающий сигнатурой с каким-то методом класса Object, такой метод (или методы) не поломает наше ограничение функционального интерфейса:
@FunctionalInterface
public interface Converter {

   N convert(T t);

   static  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 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 {
   boolean test(T t);
}
В качестве примера рассмотрим создание Predicate, который будет проверять на чётность числа типа Integer:
public static void main(String[] args) {
   Predicate 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 {
   void accept(T t);
}
В качестве примера рассмотрим Consumer, задача которого — выводить в консоль приветствие с переданным строковым аргументом:
public static void main(String[] args) {
   Consumer greetings = x -> System.out.println("Hello " + x + " !!!");
   isgreetings.accept("Elena");
}
Вывод в консоль:
Hello Elena !!!

Supplier

Supplier (с англ. — поставщик) — функциональный интерфейс, который не принимает никаких аргументов, но возвращает некоторый объект типа T:
@FunctionalInterface
public interface Supplier {
   T get();
}
В качестве примера рассмотрим Supplier, который будет выдавать рандомные имена из списка:
public static void main(String[] args) {
   ArrayList nameList = new ArrayList<>();
   nameList .add("Elena");
   nameList .add("John");
   nameList .add("Alex");
   nameList .add("Jim");
   nameList .add("Sara");

   Supplier 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 {
   R apply(T t);
}
В качестве примера возьмём Function, который конвертирует числа из формата строк (String) в формат чисел (Integer):
public static void main(String[] args) {
   Function valueConverter = x -> Integer.valueOf(x);
   System.out.println(valueConverter.apply("678"));
}
Запустив, получим вывод в консоль:
678
P.S.: если в строку мы передадим не только числа, но и другие символы, вылетит exceptionNumberFormatException.

UnaryOperator

UnaryOperator — функциональный интерфейс, принимает в качестве параметра объект типа T, выполняет над ним некоторые операции и возвращает результат операций в виде объекта того же типа T:
@FunctionalInterface
public interface UnaryOperator {
   T apply(T t);
}
UnaryOperator, который своим методом apply возводит число в квадрат:
public static void main(String[] args) {
   UnaryOperator 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 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 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 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 c аргументом Function — метод map, который принимает элементы одного типа, что-то с ними делает и передает дальше, но это уже могут быть элементы другого типа. Как может выглядеть пример с Function в Stream:
public static void main(String[] args) {
   List values = Stream.of("32", "43", "74", "54", "3")
           .map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
В итоге мы получаем список чисел, но уже в формате Integer.

Метод с UnaryOperator

В качестве метода, использующего UnaryOperator как аргумент, возьмем метод класса Streamiterate. Данный метод схож с методом 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!Функциональные интерфейсы в Java - 5