Содержание
1. Введение
Функциональные интерфейсы — это мощный механизм, предоставляемый Java 8, который дает нам возможность передавать функции в качестве параметров другим методам. На самом деле, эта опция уже существовала в предыдущих версиях Java, например, с интерфейсом Comparator.
Что такое функциональный интерфейс? Функциональный интерфейс определяет «объекты», которые не хранят значения, подобные традиционным объектам, а только «функции». Заметьте, что и «объект», и «функция» заключены в кавычки, поскольку функциональные интерфейсы — это не реальные объекты или функции, а всего лишь механизм, позволяющий получить метод для получения функциональных элементов в качестве аргументов. Давайте посмотрим на интерфейс Comparator: что определяется с помощью Comparator? Определяются естественные критерии упорядочения таким образом, чтобы метод сравнения сообщал нам, какой из двух заданных объектов можно считать меньшим. Если объект Comparator передается методу, мы предоставляем этому методу функцию, способную определять порядок объектов. Этот метод является «универсальным» в отношении этого порядка и должен быть готов принять любые критерии и выполнить его функциональные возможности в соответствии с порядком ввода.
Таким образом, например, метод сортировки из класса Collections может получить объект типа Comparator. Ранее должен быть реализован класс типа Comparator для определения порядка объектов сравниваемого класса.
Пример. Класс Comparator, который заказывает рейсы в соответствии с их ценой от большего к меньшему:
1
2
3
4
5
6
|
public class ComparatorFlightPrice implements Comparator<Flight> { public int compare(Flight v1, Flight v2){ return v1.getPrice().compareTo(v2.getPrice()); } } |
И вызов может быть:
1
|
Collections.sort(flightList, new ComparatorFlightPrice()); |
Как видно, метод sort получает аргумент функционального типа, поскольку полученный объект Comparator сообщает сортировке, как следует упорядочивать значения из flightList. Код метода сортировки класса Collections, очевидно, должен быть подготовлен к заказу по различным критериям. Для этой цели метод сортировки имеет среди своих строк кода общий вызов метода сравнения объекта, который он получает в качестве параметра. Таким образом, если он получает Comparator для заказа по цене, он упорядочивает список ввода по цене полета, тогда как если переданный Comparator заказывает по номеру пассажира, это будет критерием заказа.
Что делает Java 8, так это расширяет количество функциональных интерфейсов и их удобство использования, определяя набор методов, аргументами которых являются эти интерфейсы.
2. Обоснование
Давайте рассмотрим другое возможное применение функциональных интерфейсов, отличных от Comparator. Существует множество алгоритмов, которые используют логическое условие в своей схеме. Одним из самых простых примеров является шаблон алгоритма счетчика, который возвращает количество элементов коллекции, которые удовлетворяют определенному условию: сколько полных рейсов вылетает сегодня? Сколько рейсов в Мадрид на этой неделе? и т.д. Мы знаем, что схема этого алгоритма следующая:
1
2
3
4
5
6
7
8
9
|
Scheme counter Initiate counter For each element in collection If condition Increment counter EndIf EndFor Return counter } |
Эта схема принимает в качестве входных данных коллекцию элементов и условие, которое они должны проверить, и счетчик в качестве выходных данных. Например, давайте посмотрим на методы из аэропорта, которые подсчитывают количество рейсов в направлении конкретного пункта назначения и количество рейсов после определенной даты.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
public Integer counterFlightsDate(Date f){ Integer counter= 0 ; for (Flight v:flights){ if (v.getDate().compareTo(f)> 0 ){ counter++; } } return counter; } public Integer counterFlightsDestination(String d){ Integer counter = 0 ; for (Flight v:flights){ if (v.getDestination().equals(d)){ counter ++; } } return counter; } |
Понятно, что код абсолютно одинаков для обоих методов, за исключением условия и типа параметра: даты f для первого случая и строки назначения d второго. Этот код будет вызываться следующим образом:
1
2
3
4
|
Integer cont1 = SVQairport.counterFlightsDate( "Madrid" ); System.out.println( "The number of flights to Madrid is " +cont1); Integer cont2 = SVQairport.counterFlightsDestination( new Date( 16 , 07 , 2014 )); System.out.println( "The number of flights later than 16th of July is " +cont2); |
Предположим, что мы можем передать условие, которое мы указываем для «если», в качестве параметра. Затем код метода можно обобщить примерно так:
1
2
3
4
5
6
7
8
9
|
public Integer genericalFlightCount(Condition<Flight> filter){ Integer counter= 0 ; for (Flight v:flights){ if (condition of filter over v){ counter ++; } } return counter; } |
Строка 1: позже этот гипотетический тип Condition будет фактически предикатом типа
Строка 4: на самом деле это выражение будет filter.test (v), поскольку этот метод реализован в Predicate
Таким образом, у нас будет общий метод счетчика для Airport, который после кодирования может вызываться с различными логическими выражениями и иметь разные функциональные возможности. Для того, чтобы можно было передать логическое выражение в качестве параметра, нам понадобится условие типа (интерфейса), которое будет реализовывать прием метода и объект соответствующего типа (в данном случае Flight) и вернет значение типа boolean с запрошенным условием. Эта возможность является функциональным интерфейсом Predicate, который будет первым примером, который будет изучен в следующем разделе.
Вызов этого общего метода будет:
1
2
3
4
5
6
7
|
Integer cont1 = SVQairport.genericalFlightCount(v-> v.getDestination.equals( "Madrid" )); System.out.println( "The number of flights from Madrid is " +cont1); Date f = new Date( 16 , 07 , 2014 ) Integer cont2 = SVQairport.genericalFlightCount (v->v.getDate().compareTo(f)> 0 ); System.out.println( "The number of flights later than 16th of July is " +cont2); |
Строка 1: это лямбда-выражение (см. Следующий раздел), которое указывает, что «условие» или «фильтр», передаваемый в качестве параметра, указывает, что каждый полет v возвращает условие, независимо от того, является ли его пункт назначения Мадридским
Строка 6: это еще одно лямбда-выражение, которое для каждого Рейса v возвращает значение true, если дата вылета позднее 14 июля
3. Лямбда-выражения
Есть еще один момент, который меняется в Java 8, и это то, как функциональные интерфейсы предоставляются в качестве параметров для методов. Как мы видели в предыдущем примере, чтобы использовать Comparator в Java7, определяется внешний класс, содержащий метод сравнения, и объект этого класса передается методу, который требует его, либо при вызове, либо с ранее созданным объект. Java 8 имеет другие, более гибкие механизмы для определения функциональных интерфейсов: лямбда-выражения и ссылки на методы.
Лямбда-выражение — это упрощение метода, в котором входные параметры и выходное выражение разделяются оператором стрелки «->». Входные параметры записываются в скобках и разделяются запятыми. Если интерфейс имеет единственный входной аргумент, скобки не нужны. Таким образом, форма первой части лямбда-выражения будет похожа на () -> если нет входных параметров, x-> если есть только один или (x, y) -> для двух аргументов. Обычно нет необходимости определять тип аргументов, потому что Java 8 может выводить их из контекста. После оператора стрелки -> мы должны написать выражение, которое будет возвращаемым значением для интерфейса, который мы объявляем.
Пример 1. Функциональный интерфейс, принимающий Flight и возвращающий его цену:
1
|
x-> x.getPrice(); |
Пример 2. Функциональный интерфейс, получающий String, представляющий число, и возвращающий Integer с соответствующим значением, будет записан в виде:
1
|
x -> new Integer(x); |
Пример 3. Приведем еще один пример с интерфейсом Comparator. Вызов метода sort из Collections может быть выполнен путем создания необходимого Comparator с лямбда-выражением непосредственно в коде вызова:
1
|
Collections.sort(flightList,(x,y)->x.getPrice().compareTo(y.getPrice())); |
Лямбда-выражение формируется его аргументами, в данном случае двумя: x и y между скобками и разделяются запятой. Есть два аргумента, потому что метод сравнения из интерфейса Comparator также нуждается в двух аргументах. Как мы видим, x и y являются ссылками на тип Flight, даже если нам не нужно формально указывать это, потому что компилятор Java 8 способен «понять» его из контекста, потому что, поскольку мы упорядочиваем List <Flight> Компаратор должен иметь тип Flight, и, следовательно, аргументы для метода сравнения также должны быть этого типа. Затем, после символа стрелки ‘->’ мы пишем выражение, которое должен вернуть метод сравнения; в нашем случае выражение типа int со сравнением цены рейсов.
Еще один способ ссылки на Comparator — это вызов метода, который возвращает свойство, которое мы хотим сравнить. Например, Java 8 позволяет этот другой вызов:
1
|
Collections.sort(flights,Comparator.comparing(Flight::getPrice)); |
При этом втором вызове интерфейс Comparator вызывает сравнение статического метода, аргумент которого является функциональным интерфейсом типа Function, который можно определить, просто ссылаясь на метод, который возвращает свойство, которое мы хотим сравнить.
Лямбда-выражения чаще всего используются непосредственно при вызове метода, который требует функционального интерфейса, но если определенное лямбда-выражение будет использоваться несколько раз, оно может быть объявлено с идентификатором.
Пример. Чтобы определить, заполнен ли рейс, напишем:
1
2
|
Predicate<Flight> flightFull = x-> x.getNumPassengers().equals(x.getNumSeats()); |
Таким образом, идентификатор flightFull может заменить интерфейс Predicate для всех вызовов, имеющих аргумент типа Predicate.
Если функциональному интерфейсу потребуются входные аргументы типа, отличного от того, который указан для самого интерфейса, лучший способ определить его, как если бы это был метод.
Пример. Если нам нужно определить предикат для рейса, который получает аргумент типа Date и сообщает, вылетает ли рейс после указанной даты, мы определяем:
1
2
3
|
Predicate<Flight> laterDate(Date f){ return x -> x.getDate().compareTo(f)> 0 ; } |
4. Предикат <T>
Как уже указывалось ранее, интерфейс Predicate реализует логическое условие для методов, которые требуют некоторого фильтра или условия. Предикат реализует метод с именем test, который возвращает логическое значение от объекта типа T. Следовательно, тип Predicate используется для классификации объектов типа T в зависимости от того, удовлетворяют ли они определенному свойству. Например, если задано «Полетное сообщение», если оно завершено, дано «Книжное», если в его названии содержится какое-то конкретное слово, дано «Песенное», если оно длится более x секунд, или дано «Строковое», если оно начинается с определенного символа.
Пример 1. Предикат, который говорит, что полет полон:
1
2
|
Predicate<Flight> flightFull = x-> x.getNumPassengers().equals(x.getNumSeats()); |
Пример 2. Если нам нужно определить условие на основе параметра, функциональному интерфейсу может быть задан входной аргумент. Например, если нам нужно узнать, произошел ли определенный рейс с определенной даты, мы бы определили:
1
2
3
|
Predicate<Flight> equalDate(Date f){ return x -> x.getDate().equals(f); } |
Также доступны специализированные интерфейсы, такие как DoublePredicate, IntPredicate и LongPredicate, для получения логического значения из объектов базовых типов данных.
Методы по умолчанию
Интерфейс Predicate также имеет три метода, реализующих логические операции: negate (), и (Predicate) и или (Predicate). Например, если нам нужен аргумент типа Predicate, который сообщает, соответствует ли Flight определенной дате и является ли он полным, он будет записан в виде:
1
|
equalDate(f).and(flightFull) |
5. BiPredicate <T, U>
Интерфейс BiPredicate выдает логическое значение из двух параметров разного типа. Например, если строка String, представляющая пункт назначения, и Flight, она будет возвращать, будет ли этот Flight направлен в этот пункт назначения, если дана песня и продолжительность, она сообщит, если продолжительность песни меньше указанной, и т. Д.
Пример. Интерфейс, который возвращается, если Рейс взлетает в указанную дату, будет:
1
2
|
BiPredicate<Flight, Date> getCoincidence = (x,y)-> y.equals(x.getFecha()); |
6. Функция <T, R>
Функция — это интерфейс с методом apply, который получает аргумент типа T и возвращает объект типа R. Он в основном используется для преобразования объектов из типа другого производного или комбинированного; например, автор книги, продолжительность песни, количество пассажиров рейса и т. д. Java 8 предоставляет набор специализированных интерфейсов, различающихся по типу входных или выходных параметров. Например, ToDoubleFunction <T>, ToIntFunction <T>, ToLongFunction <T> специализируются на получении объекта типа T и возврате типа, указанного в имени интерфейса. Эти интерфейсы реализуют метод applyAsX, где X будет Double, Int или Long, в зависимости от случая. И наоборот, функции LongFunction <R>, IntFunction <R> и DoubleFunction <R> получают значение типа, указанного в имени, и возвращают другое значение типа R, используя метод apply . Наконец, есть еще шесть интерфейсов с именем XToYFunction, где X, Y принимают значения Double, Int или Long, где X является типом входного аргумента, а Y — тип возвращаемого значения. Реализуемый ими метод — ApplyAsY, где Y — тип возвращаемого значения.
Пример 1: функция, которая определяет полет, определяет его продолжительность и может быть определена как:
1
|
Function<Flight, Duration> functionDuration = x->x.getDuration(); |
В этом случае, если для типа Flight определен метод getDuration, оператор :: может использоваться при вызове метода, который использует тип Function в качестве входного параметра:
1
|
Flight::getDuration |
Конечно, если выражение Function не будет использоваться более одного раза, лямбда-выражение x-> x.getDuration () может быть входным параметром метода, для которого требуется эта функция.
Пример 2. Для данного Полета функция, которая возвращает коэффициент занятости:
1
|
Function<Flight,Double>functOccRatio=x-> 1 .*x.getNumPasengers()/x.getNumSeats(); |
Этот случай является ярким примером специализированной функции:
1
2
3
|
ToDoubleFunction<Flight> functOccRatio(){ return x-> 1 .*x.getNumPassengers()/x.getNumSeats(); } |
Методы по умолчанию
Интерфейс Function имеет два метода, которые позволяют нам управлять функциями с композицией: compose (Function) и andThen (Function). Разница между ними заключается в порядке применения задействованных функций. Функция, полученная в результате применения метода f.compose (g), сначала применяет g, а затем f, тогда как f.andThen (g) — результат первого применения f, а затем g.
Пример. Предположим, у нас есть функция, которая, учитывая объект типа Duration, возвращает преобразование в минуты:
1
|
Function<Duration,Integer> inMinutes=x->x.getMinutes()+x.getHours()* 60 ; |
И еще одна функция, которая возвращает продолжительность полета:
1
|
Function<Flight,Duration> getDuration = Flight::getDuration; |
Тогда функция, которая возвращает продолжительность в минутах полета, будет иметь вид:
1
|
Function<Flight,Integer> getDurationInMinutes=inMinutes.compose(getDuration); |
Или также:
1
|
Function<Flight,Integer> getDurationInMinutes =getDuration.andThen(inMinutes); |
7. BiFunction <T, U, R>
BiFunction — это функция, которая получает два аргумента типов T и U и возвращает результат типа R, используя метод apply . Существуют также три интерфейса, специализирующихся на возврате определенного типа: ToDoubleBiFunction, ToIntBiFunction и ToLongBiFunction, которые реализуют метод applyAsX, где X может быть Double, Int или Long.
Пример. Чтобы получить функцию, которая с учетом даты и рейса возвращает количество дней, оставшихся между данной датой и моментом вылета рейса, было бы:
1
2
3
|
ToIntBiFunction<Flight, Date> getDays(Flight v, Date f){ return (x,y)->y.subtract(x.getDate()); } |
8. Потребитель <T>
Интерфейс Consumer — это вариант Function, в котором значение не возвращается, что означает, что он модифицирует данный объект с помощью метода с именем accept, который получает объект типа T и возвращает void. Они используются для определения действия над объектом. Например, увеличение цены полета на определенный процент, вычитание даты из числа дней или вывод на консоль значения. Java 8 также предоставляет специализированные интерфейсы DoubleConsumer, LongConsumer или IntConsumer, которые также реализуют метод accept .
Пример 1. Если мы хотим увеличить цену полета на 10%, мы бы определили Потребителя:
1
|
Consumer<Flight> incrementPrice10p = x->x.setPrice(x.getPrice()* 1.1 ); |
Пример 2. Если мы хотим, чтобы это приращение выполнялось в процентах, передаваемых в качестве параметра, мы могли бы написать следующий метод для типа Flight:
1
2
3
|
Consumer<Flight> incrementPrice(Double p){ return x->x.setPrice(x.getPrice()*( 1 +p/ 100 .)); } |
Пример 3. Очень часто встречается следующий потребитель для замены выражения System.out.println:
1
|
Consumer<Flight> printFlight = x->System.out.println(x); |
Пример 4. Если бы мы хотели иметь метод Flight, который бы реализовывал определенное действие над объектом типа Flight в зависимости от условия, мы могли бы написать:
1
2
3
4
5
|
public void applyAction(Predicate<Flight> cond, Consumer<Flight> act){ if (cond.test( this )){ act.accept( this ); } } |
Как только у нас будет объект v типа Flight, вызов предыдущего метода для увеличения цены v, если количество пассажиров меньше 50, будет:
1
|
v.applyAction(x->x.getNumPassengers()< 50 , x->x.incrementPrice( 10 .)); |
где incrementPrice — это Потребитель, определенный в Примере 2.
9. BiConsumer <T, U>
BiConsumer — это интерфейс для определения действия над двумя входными аргументами разного типа. Он используется для представления действий, которые изменяют объект, получающий объект другого типа. Его специализированные интерфейсы: ObjDoubleConsumer, ObjIntConsumer и ObjLongConsumer, которые получают объект типа T и еще один тип, указанный в имени. Все они реализуют функциональный метод, называемый accept .
Пример. Чтобы изменить продолжительность полета, мы могли бы написать следующий код:
1
|
BiConsumer<Flight, Duration> changeDuration = (x,y)->x.setDuration(y); |
10. Поставщик <T>
Поставщик — это интерфейс, который предоставляет объект типа T без аргументов, используя метод get . Кроме того, существуют специализированные интерфейсы, такие как BooleanSupplier, DoubleSupplier, IntSupplier и LongSupplier для предоставления объектов указанного типа. В этих случаях реализуемый ими метод называется getAsX, где X — Boolean, Double, Int или Long соответственно.
Обычно интерфейсы типа Supplier просто вызывают конструкторы. Таким образом, лямбда-выражение для вызова конструктора Flight, если предположить, что FlightImpl имеет конструктор по умолчанию, будет:
1
|
Supplier<Flight> giveMeFlight = ()-> new FlightImpl(); |
Если мы хотим, чтобы у поставщика был аргумент, нам нужно написать:
1
2
3
|
Supplier<Flight> giveMeFlight (String s) { return ()-> new FlightImpl(s); } |
Другой обычный способ создания поставщиков — использование выражения метода:
1
|
Supplier<Set<Integer>> giveMeSet = HashSet:: new ; |
11. UnaryOperator <T>
Интерфейс UnaryOperator представляет операцию, которая получает один параметр типа T и возвращает другой объект того же типа, используя метод apply . Это частный случай интерфейса Function с тем же типом для входных и выходных значений, и Java реализует его как подынтерфейс Function. Java 8 также имеет специализированные интерфейсы DoubleUnaryOperator, IntUnaryOperator и LongUnaryOperator, которые реализуют метод applyAsX, представляющий собой X цепочку символов Double, Int или Long соответственно. Поскольку этот интерфейс является подынтерфейсом Function, он также реализует методы по умолчанию compose и andThen с тем же поведением.
Пример 1. Если нам нужен оператор для изменения Duration, добавив в него определенное количество минут, заданных параметром, мы бы написали:
1
2
3
|
public UnaryOperator<Duration> addMinutes(Integer m){ return x -> x.sum( new DurationImpl( 0 ,m)); } |
12. Бинарный оператор <T>
Интерфейс BinaryOperator представляет операцию, которая получает два операнда типа T и возвращает результат того же типа, используя метод apply . Как мы видим, это частный случай интерфейса BiFunction, где три типа T, U и R одинаковы, а Java 8 реализует его как подынтерфейс BiFunction. Существуют также специализации, такие как DoubleBinaryOperator, IntBinaryOperator и LongBinaryOperator для работы с числовыми значениями. В этих интерфейсах метод, который они реализуют, — applyAsX , где X может принимать имена Double, Int или Long соответственно.
Пример 1. У нас определен тип Duration, который хранит продолжительность полета в часах и минутах. Если тип Duration уже определил метод sum:
1
2
3
4
5
|
public Duration sum(Duration d) { Integer min = getMinutes() + d.getMinutes(); Integer hour = getHours() + d.getHours(); return new DurationImpl(hour+min/ 60 ,min% 60 ); } |
Тогда мы можем переопределить его как BinaryOperator:
1
|
BinaryOperator<Duration> addDur = (x,y) -> x.sum(y); |
Эквивалент этому другому выражению:
1
|
BinaryOperator<Duration> addDur = Duration::sum; |
Если сумма метода не была определена для Duration, мы могли бы непосредственно определить:
1
2
3
4
5
|
BinaryOperator<Duration> addDur = (x,y)-> { Integer min = x.getMinutes() + y.getMinutes(); Integer hour = x.getHours() + y.getHours(); return new DurationImpl(hour+min/ 60 ,min% 60 ); }; |
Пример 2. Интерфейс DoubleBinaryOperator позволяет нам определять реальные функции как композицию двух других. Например, если мы хотим определить функцию h как частное от двух других неизвестных функций f и g, мы бы написали код:
1
2
3
4
|
public DoubleBinaryOperator functionH(DoubleBinaryOperator f, DoubleBinaryOperator g){ return (x,y)->f.applyAsDouble(x,y)/g.applyAsDouble(x,y); } |
Таким образом, возможный вызов для отношения между сложением и произведением двух чисел будет:
1
2
3
|
public Double callFunctionH(Double x, Double y){ return functionH((a,b)->a+b,(a,b)->a*b).applyAsDouble(x,y); } |