JavaRush /Java блог /Java Developer /Функциональные интерфейсы в Java
Автор
Александр Мяделец
Руководитель команды разработчиков в CodeGym

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

Статья из группы Java Developer
Привет! В квесте 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);
}
В качестве примера возьмём Function, который конвертирует числа из формата строк (String) в формат чисел (Integer):

public static void main(String[] args) {
   Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
   System.out.println(valueConverter.apply("678"));
}
Запустив, получим вывод в консоль:

678
P.S.: если в строку мы передадим не только числа, но и другие символы, вылетит exceptionNumberFormatException.

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 c аргументом 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 как аргумент, возьмем метод класса 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!
Комментарии (17)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Dmitry Armarchuk Уровень 31
23 сентября 2023
автор молодцом! Пиши еще.
Dina Goncharova Уровень 39
9 сентября 2022
Лайк за инфу и за енота :)
Avgustin Уровень 28
27 марта 2022
Спасибо! А вот "в официальной документации Oracle" очень забавно, что отсортировано по имени... Уффф молодцы)))
Andrew Уровень 35
9 октября 2021
Зачем использовать Function<String, Integer> valueConverter = x -> Integer.valueOf(x); если можно использовать public Integer valueConverter(String x) { return Integer.valueOf(x); }
Alexander Уровень 33
2 февраля 2021
Не благодарите 😎

//Test.java
@FunctionalInterface
interface Converter {

    Raccoon convert(Dog t);

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

    default void writeToConsole(Dog t) {
        System.out.println("Текущий объект - " + t.toString());
    }

    boolean equals(Object obj);
}

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;
    }
}

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;
    }
}

class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("Bobbie", 5, 3);

        Converter converter = (Dog 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);
    }
}
LindX Уровень 12
18 декабря 2020
Интересно что за ....?

 Converter converter = x -> new Raccoon(x.name, x.age, x.weight);
почему не:

Converter<Raccoon, Dog> converter = x -> new Raccoon(x.name, x.age, x.weight);
?
LindX Уровень 12
18 декабря 2020

@FunctionalInterface
public interface Converter {

   N convert(T t);

   static boolean isNotNull(T t){
       return t != null;
   }
}
Я не очень понимаю почему не :

@FunctionalInterface
public interface Converter<N,T> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
?? isNotNull статический же
LindX Уровень 12
18 декабря 2020
> Вернёмся же к классу `Comparator`. Может все же к интерфейсу?
LindX Уровень 12
18 декабря 2020

@FunctionalInterface
public interface Converter {
   N convert(T t);
}
и как это интересно должно работать?
Viktor Vdovichenko Уровень 0
5 декабря 2020
неужели этот код написан верно:

 Converter converter = x -> new Raccoon(x.name, x.age, x.weight);