C, PHP, VB, .NET

Дневникът на Филип Петров


* Вградени функционални интерфейси

Публикувано на 11 март 2015 в раздел ПИК3 Java.

В Java 8 има една поредица от вградени функционални интерфейси, които имат важна роля при използването на ламбда изрази в библиотечните функции. Използването на тези интерфейси е силно насърчително (вместо създаването на собствени еквивалентни такива). В предишната статия вече говорихме за добре познатите Comparator, ActionListener и Runnable. Сега ще обърнем внимание на някои от новите интерфейси, които идват с Java 8.

java.util.function.Function

Function е може би най-стандартния функционален интерфейс - той получава обект за входен параметър, обработва данните от него и връща друг обект като резултат. Дефиницията му е следната:

package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Function<T, R> {
 R apply(T t);

 default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
 }

 default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
 }

 static <T> Function<T, T> identity() {
    return t -> t;
 }
}

Виждате, че освен основния метод "apply" има и методи по подразбиране. Основната идея на този интерфейс е да извършваме трансформации на входните данни - например превръщане на едни мерни единици в други и т.н. Ето един пример - функции, които превръщат миля в километър и километър в миля, като приемат за входен параметър Integer и връщат като резултат Double:

Function<Integer,Double> kmToMiles = x -> new Double(x*0.621371);
Function<Integer,Double> milesToKm = x -> new Double(x*1.60934);
 
System.out.println("2 miles to km test: "+milesToKm.apply(2));
System.out.println("10 km to miles test: "+kmToMiles.apply(10));

Функциите по подразбиране могат да се използват за т.нар. "верижни операции". Нека дадем един пример - имаме клас "Address" с член променливи "city" (град) и "country" (държава). Имаме и клас Person с член променливи "name" (име) и "address" (от тип Address). Следният пример демонстрира създаване на Function обекти personToAddress (по подаден обект от тип Person връща неговия Address), addressToCountry (по подаден Address връща държавата като String) и използвайки верижна операция personToCountry (по подаден Person връща държавата като String):

import java.util.function.*;

public class Test{
 public static void main(String[] args){
   Function<Person, Address> personToAddress = Person::getAddress;
   Function<Address, String> addressToCountry = Address::getCountry;
   Function<Person, String> personToCountry = personToAddress.andThen(addressToCountry);
 
   Address a = new Address("Sofia", "Bulgaria");
   Person p = new Person("Ivan", a);
 
   System.out.println(p.getName()+" country is "+personToCountry.apply(p));
 }
}

// having classes Address and Person
class Address {
 private String country;
 private String city;
 Address(String city, String country){
   this.country = country;
   this.city = city;
 }
 String getCountry(){
   return this.country;
 }
 String getCity(){
   return this.city;
 }
}

class Person {
 private String name;
 private Address address;
 public Person(String name, Address address){
   this.name = name;
   this.address = address;
 }
 Address getAddress(){
   return address;
 }
 String getName(){
   return this.name;
 }
}

Напълно аналогичен резултат ще получим ако използваме "обърнатия" метод на andThen - метод compose:

Function<Person, String> personToCountry = addressToCountry.compose(personToAddress);

При compose първо се изпълнява подадената вътре в скобите операция, а после извикващата. Затова и казахме, че е "обратна" на andThen, която може би ще ви се стори по-естествена. Например ако искаме да направим математическа операция sin(log(cos(PI/2))), можем да постъпим по следния начин:

Function<Double , Double> sin = Math::sin;
Function<Double , Double> log = Math::log;
Function<Double , Double> cos = Math::cos;
System.out.println(cos.andThen(log).andThen(sin).apply(Math.PI/2));

java.util.function.Predicate

Предикатите се използват като методи за проверка на дадено условие. Те получават един входен параметър - обект - и връщат винаги булев резултат (променлива от тип boolean). Дефиницията на интерфейса е следната:

package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Predicate<T> {
 boolean test(T t);

 default Predicate<T> and(Predicate<? super T> other) {
   Objects.requireNonNull(other);
   return (t) -> test(t) && other.test(t);
 }

 default Predicate<T> negate() {
   return (t) -> !test(t);
 }

 default Predicate<T> or(Predicate<? super T> other) {
   Objects.requireNonNull(other);
   return (t) -> test(t) || other.test(t);
 }

 static <T> Predicate<T> isEqual(Object targetRef) {
   return (null == targetRef)
   ? Objects::isNull
   : object -> targetRef.equals(object);
 }
}

Виждате основния метод "test", както и трите метода по подразбиране - "and" (за логическо "И"), "or" (за логическо "ИЛИ") и "negate" (за обръщане на резултата). Нека се върнем към нашия пример с хората и адресите. Искаме да добавим предикат (метод), с който да "тестваме" дали даден човек е от България или не е. В долния код представяме само main метода на примера - дефинициите на класовете Person и Address ги пропускаме (вземете ги от предишния пример в тази статия):

// Това са старите интерфейси, за които вече дадохме пример 
Function<Person, Address> personToAddress = Person::getAddress;
Function<Address, String> addressToCountry = Address::getCountry;
Function<Person, String> personToCountry = personToAddress.andThen(addressToCountry);

// Създаваме си примерни обекти
Address a1 = new Address("Sofia", "Bulgaria");
Person p1 = new Person("Ivan", a1);
Address a2 = new Address("Paris", "France");
Person p2 = new Person("Jean", a2);

// Предикат, с който се проверява дали даден човек е от държава Bulgaria
Predicate<Person> isFromBulgaria = p -> personToCountry.apply(p).equals("Bulgaria");
// Пробваме предиката
System.out.println(isFromBulgaria.test(p1));

Ето как можем да разширим с верижен тест и да добавим още един предикат - вече ще правим проверка по град и по държава едновременно:

Predicate<Person> isFromSofia = p -> personToAddress.apply(p).getCity().equals("Sofia");
System.out.println(isFromSofia.and(isFromBulgaria).test(p1));

Предикатите играят основна роля при филтрирането на информация в потоци (нещо, за което ще покажем пример в следваща статия).

java.util.function.Supplier

"Доставчиците" са най-елементарните вградени функционални интерфейси - те просто създават нов обект от даден тип (клас):

package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
  T get();
}

Както виждате не приемат никакви входни аргументи. Стандартно се използват за извикване на конструктори по подразбиране (но нищо не пречи да извикват какъвто и да е метод, който генерира обект от търсения тип). Ако например имаме клас "item", можем да създаваме стандартен обект от този клас по следния начин:

Supplier<Item> itemSupplier = Item::new;
Item i = itemSupplier.get();

java.util.function.Consumer

"Консуматорите" пък извършват последователност от операции върху подаден обект и не връщат резултат (void функции са):

package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Consumer<T> {
 void accept(T t);

 default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
 }
}

Или по друг начин казано - това са Function без връщан резултат. Помните ли метод "forEach" от предишната статия за методи по подразбиране? Е той беше реализиран като за входен параметър му се подаваше именно обект Consumer!

Supplier и Consumer могат да се срещнат в често срещана комбинация - познатите ни от статиите за паралелно програмиране Producer-Consumer проблем. Единият обект ще произвежда, а другия ще консумира дадена продукция. Нека имаме клас Food със статичен метод "cook", който връща инстанция на обект от тип Food (правим това просто за игра, за да покажем как се използва private конструктор - не, че има голям смисъл за примера). Нека имаме и метод dispatch, който употребява храната - например изпраща я някъде и я нулира за текущия обект. В нашия пример просто ще печати съобщение в конзолата. Ето и комбинацията supplier-consumer:

import java.util.function.*;

public class Test{
 public static void main(String[] args){
    Supplier<Food> supplier = Food::cook;
    Consumer<Food> consumer = Food::dispatch;
 
    Food f = supplier.get();
    consumer.accept(f);
 }
}

class Food{
 int quantity;
 private Food(){
    this.quantity = 10;
 }
 static Food cook(){
    return new Food();
 }
 void dispatch(){
    System.out.println(this.quantity+" food items delivered");
    this.quantity = 0;
 }
}

Други

Има много голямо количество други функционални интерфейси, които са много сходни с горните:

  • BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier - това са "доставчици" на примитивни типове данни boolean, int, long или съответно double;
  • IntConsumer, LongConsumer, DoubleConsumer - аналогично на доставчиците - консуматори на примитивни типове int, long и double;
  • IntFunction, LongFunction, DoubleFunction - алтернативи на Function, които да могат да работят с входни данни от примитивен тип int, long или съответно double;
  • IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction, DoubleToIntFunction, DoubleToLongFunction - функции, които приемат примитивен тип и връщат примитивен тип данни;
  • IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator - функции, които приемат и връщат примитивни типове int, long или съответно double данни;
  • IntPredicate, LongPredicate, DoublePredicate - предикати, които могат да работят с int, long и double за входни аргументи на test метода;

Причината за съществуването на горните интерфейси е липсата на "value types" в Java. Не можете да направите Function<String, int> func, просто защото методът на Function очаква рефенерция към обект, а вие му подавате примитивен тип. Това определено е проблем за JDK на този етап, защото води до видимо неудобно "размножаване" (spawning, pollution) на почти еднакви интерфейси (получава се дублиране на код - code duplication).

Има и други интерфейси, които заслужават внимание:

  • BiConsumer - консуматор, който приема не един, а два обекта за входни параметри, т.е. метод accept ще бъде accept(Object, Object);
  • BiFunction - работи като Function, но има вместо един - два обекти за входни параметри (и един за връщан резултат);
  • IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator - алтернативи на BiFunction, които приемат за входни данни два примитивни типа int, long или съответно double и връщат резултат от същия примитивен тип;
  • BiPredicate - предикат, който приема два входни параметъра - методът му е test(Object, Object);
  • ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer - консуматори, които приемат два аргумента за входни данни - обект и съответен примитивен тип. Както и другите консуматори - няма връщан резултат (метода е void);
  • ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction - приемат два обекта за входни параметри и връщат резултат от примитивен тип int, long или double;
  • UnaryOperator - функция, която работи с входен и изходен обекти от един и същи тип.

Казаното накрая няма как да не се счете за сериозна критика. Може би трябва все пак да сме доволни, че не са включили интерфейси и за другите примитивни типове данни - byte, short, char - защото комбинациите щяха да станат драстично много :)

 



Добави коментар

Адресът на електронната поща няма да се публикува


*