Привет!
В квесте Java Syntax Pro мы изучали лямбда-выражения и говорили, что это ничто иное как реализация функционального метода из функционального интерфейса.
Иными словами, это реализация некоего анонимного (неизвестного) класса, его нереализованного метода.
И если в лекциях курса мы углублялись в манипуляции с лямбда-выражениями, сейчас рассмотрим, так сказать, обратную сторону: а именно — эти самые интерфейсы.
В восьмой версии Java появилось понятие функциональные интерфейсы.
Что же это?
Функциональным считается интерфейс с одним не реализованным (абстрактным) методом. Под это определение попадают многие интерфейсы с “коробки”, такие как, например, рассмотренный ранее интерфейс ![Функциональные интерфейсы в Java - 2]()
Predicate
Consumer
Supplier
Function
UnaryOperator
А таким, что многие методы
На этом всё! Будет отлично, если после прочтения данной статьи вы станете на шаг ближе к пониманию и освоению Stream API в Java!

Comparator
.
А еще — интерфейсы, которые мы создаём сами, как, например:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
У нас получился интерфейс, задача которого — преобразовывать объекты одного типа в объекты другого (такой себе адаптер).
Аннотация @FunctionalInterface
не является чем-то сверхсложным и важным, так как её предназначение — сообщить компилятору, что данный интерфейс функциональный и должен содержать не более одного метода.
Если же в интерфейсе с данной аннотацией более одного не реализованного (абстрактного) метода, компилятор не пропустит данный интерфейс, так как будет воспринимать его как ошибочный код.
Интерфейсы и без данной аннотации могут считаться функциональными и будут работать, а @FunctionalInterface
является не более чем дополнительной страховкой.
Вернёмся же к классу Comparator
. Если заглянуть в его код (или документацию), можно увидеть, что у него есть гораздо больше одного метода.
Тогда вы спросите: как же в таком случае он может считаться функциональным интерфейсом?
Абстрактные интерфейсы могут иметь методы, которые не входят в ограничения одного метода:
- статические
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
Получив этот метод, компилятор не стал ругаться, а значит наш интерфейс все еще функциональный.
- default методы
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 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.: если в строку мы передадим не только числа, но и другие символы, вылетит 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. Каким же образом, спросите вы?
Stream
работают именно с данными функциональными интерфейсами. Давайте рассмотрим, как можно применять функциональные интерфейсы в методах Stream
.Метод с Predicate
Для примера возьмем метод классаStream
— filter
, который в качестве аргумента принимает 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
как аргумент, возьмем метод класса 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
То есть каждый наш элемент умножен на самого себя, и так для первых четырёх чисел.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ