Interfețe funcționale în Java 8

1. Introducere

Acest articol este un ghid pentru diferite interfețe funcționale prezente în Java 8, cazurile lor generale de utilizare și utilizarea în biblioteca standard JDK.

2. Lambdas în Java 8

Java 8 a adus o nouă îmbunătățire sintactică puternică sub forma expresiilor lambda. O lambda este o funcție anonimă care poate fi tratată ca cetățean de limbă de primă clasă, de exemplu transmisă sau returnată dintr-o metodă.

Înainte de Java 8, de obicei creați o clasă pentru fiecare caz în care trebuia să încapsulați o singură funcționalitate. Acest lucru a implicat o mulțime de coduri inutile pentru a defini ceva care a servit ca o reprezentare a funcției primitive.

Lambdas, interfețele funcționale și cele mai bune practici de lucru cu ele, în general, sunt descrise în articolul „Expresii Lambda și interfețe funcționale: sfaturi și cele mai bune practici”. Acest ghid se concentrează pe anumite interfețe funcționale particulare care sunt prezente în pachetul java.util.function .

3. Interfețe funcționale

Se recomandă ca toate interfețele funcționale să aibă o adnotare informativă @FunctionalInterface . Acest lucru nu numai că comunică în mod clar scopul acestei interfețe, dar permite, de asemenea, unui compilator să genereze o eroare dacă interfața adnotată nu îndeplinește condițiile.

Orice interfață cu SAM (Single Abstract Method) este o interfață funcțională , iar implementarea sa poate fi tratată ca expresii lambda.

Rețineți că metodele implicite Java 8 nu sunt abstracte și nu contează: o interfață funcțională poate avea în continuare mai multe metode implicite . Puteți observa acest lucru uitându -vă la funcția de documentare.

4. Funcții

Cel mai simplu și general caz al unei lambda este o interfață funcțională cu o metodă care primește o valoare și returnează alta. Această funcție a unui singur argument este reprezentată de interfața Funcție , care este parametrizată de tipurile argumentului său și de o valoare returnată:

public interface Function { … }

Unul dintre uzurile Funcția tip din biblioteca standard este Map.computeIfAbsent metoda care returneaza o valoare dintr - o hartă de cheie , dar calculează o valoare dacă o cheie nu este deja prezentă într - o hartă. Pentru a calcula o valoare, folosește implementarea funcției trecute:

Map nameMap = new HashMap(); Integer value = nameMap.computeIfAbsent("John", s -> s.length());

O valoare, în acest caz, va fi calculată prin aplicarea unei funcții la o tastă, introdusă într-o hartă și, de asemenea, returnată dintr-un apel de metodă. Apropo, putem înlocui lambda cu o referință de metodă care se potrivește cu tipurile de valori trecute și returnate .

Amintiți-vă că un obiect pe care este invocată metoda este, de fapt, primul argument implicit al unei metode, care permite aruncarea unei referințe de lungime a metodei instanței la o interfață funcțională :

Integer value = nameMap.computeIfAbsent("John", String::length);

Funcția Interfața are , de asemenea , o valoare implicită de scriere metodă care permite să combine mai multe funcții într - una singură și să le execute succesiv:

Function intToString = Object::toString; Function quote = s -> "'" + s + "'"; Function quoteIntToString = quote.compose(intToString); assertEquals("'5'", quoteIntToString.apply(5));

Funcția quoteIntToString este o combinație a funcției de cotare aplicată unui rezultat al funcției intToString .

5. Specializări ale funcției primitive

Deoarece un tip primitiv nu poate fi un argument de tip generic, există versiuni ale interfeței Function pentru majoritatea tipurilor primitive utilizate double , int , long și combinațiile lor în tipuri de argument și returnare:

  • IntFunction , LongFunction , DoubleFunction: argumentele sunt de tip specificat, tipul de returnare este parametrizat
  • ToIntFunction , ToLongFunction , ToDoubleFunction: tipul de întoarcere este de tipul specificat, argumentele sunt parametrizate
  • DoubleToIntFunction , DoubleToLongFunction , IntToDoubleFunction , IntToLongFunction , LongToIntFunction , LongToDoubleFunction - având atât argumentul, cât și tipul returnat definit ca tipuri primitive, așa cum este specificat de numele lor

Nu există o interfață funcțională pentru a, să zicem, o funcție care durează scurt și returnează un octet , dar nimic nu te împiedică să scrii propria ta:

@FunctionalInterface public interface ShortToByteFunction { byte applyAsByte(short s); }

Acum putem scrie o metodă care transformă o matrice de scurt într-o matrice de octeți folosind o regulă definită de o funcție ShortToByte :

public byte[] transformArray(short[] array, ShortToByteFunction function) { byte[] transformedArray = new byte[array.length]; for (int i = 0; i < array.length; i++) { transformedArray[i] = function.applyAsByte(array[i]); } return transformedArray; }

Iată cum am putea să-l folosim pentru a transforma o serie de scurți în serie de octeți înmulțiți cu 2:

short[] array = {(short) 1, (short) 2, (short) 3}; byte[] transformedArray = transformArray(array, s -> (byte) (s * 2)); byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6}; assertArrayEquals(expectedArray, transformedArray);

6. Specializări de funcții cu două arități

Pentru a defini cu două argumente Lambda, trebuie să folosim interfețe suplimentare care conțin „ Bi“ cuvânt cheie în numele lor: bifunctiei , ToDoubleBiFunction , ToIntBiFunction și ToLongBiFunction .

Bifunctiei are atât argumente și un tip de întoarcere generified, în timp ce ToDoubleBiFunction și altele vă permit să returneze o valoare de tip primitiv.

Unul dintre exemplele tipice de utilizare a acestei interfețe în API-ul standard se află în metoda Map.replaceAll , care permite înlocuirea tuturor valorilor dintr-o hartă cu o anumită valoare calculată.

Să folosim o implementare BiFunction care primește o cheie și o valoare veche pentru a calcula o nouă valoare pentru salariu și a o returna.

Map salaries = new HashMap(); salaries.put("John", 40000); salaries.put("Freddy", 30000); salaries.put("Samuel", 50000); salaries.replaceAll((name, oldValue) -> name.equals("Freddy") ? oldValue : oldValue + 10000);

7. Furnizori

Furnizor Interfața funcțională este încă un alt Funcție specializare care nu ia nici un argument. Este de obicei folosit pentru generarea leneșă de valori. De exemplu, să definim o funcție care pătrează o valoare dublă . Acesta va primi nu o valoare în sine, ci un Furnizor de această valoare:

public double squareLazy(Supplier lazyValue) { return Math.pow(lazyValue.get(), 2); }

Acest lucru ne permite să generăm leneș argumentul pentru invocarea acestei funcții folosind o implementare a Furnizorului . Acest lucru poate fi util dacă generarea acestui argument durează o perioadă considerabilă de timp. Vom simula acest lucru folosind metoda Guava's sleep Uninterruptibly :

Supplier lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d; }; Double valueSquared = squareLazy(lazyValue);

Un alt caz de utilizare pentru Furnizor este definirea unei logici pentru generarea de secvențe. Pentru a o demonstra, să folosim o metodă static Stream.generate pentru a crea un flux de numere Fibonacci:

int[] fibs = {0, 1}; Stream fibonacci = Stream.generate(() -> { int result = fibs[1]; int fib3 = fibs[0] + fibs[1]; fibs[0] = fibs[1]; fibs[1] = fib3; return result; });

Funcția care este transmisă metodei Stream.generate implementează interfața funcțională a Furnizorului . Observați că pentru a fi util ca generator, Furnizorul are de obicei nevoie de un fel de stare externă. În acest caz, starea sa este alcătuită din două ultime numere de ordine Fibonacci.

Pentru a implementa această stare, folosim o matrice în loc de câteva variabile, deoarece toate variabilele externe utilizate în lambda trebuie să fie efectiv finale .

Alte specializări ale interfeței funcționale a furnizorului includ BooleanSupplier , DoubleSupplier , LongSupplier și IntSupplier , ale căror tipuri de returnare sunt primitive corespunzătoare.

8. Consumatorii

Spre deosebire de Furnizor , Consumatorul acceptă un argument generat și nu returnează nimic. Este o funcție care reprezintă efecte secundare.

De exemplu, să salutăm pe toată lumea dintr-o listă de nume imprimând mesajul de salut în consolă. Lambda trecută către metoda List.forEach implementează interfața funcțională a consumatorului :

List names = Arrays.asList("John", "Freddy", "Samuel"); names.forEach(name -> System.out.println("Hello, " + name));

There are also specialized versions of the ConsumerDoubleConsumer, IntConsumer and LongConsumer — that receive primitive values as arguments. More interesting is the BiConsumer interface. One of its use cases is iterating through the entries of a map:

Map ages = new HashMap(); ages.put("John", 25); ages.put("Freddy", 24); ages.put("Samuel", 30); ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));

Another set of specialized BiConsumer versions is comprised of ObjDoubleConsumer, ObjIntConsumer, and ObjLongConsumer which receive two arguments one of which is generified, and another is a primitive type.

9. Predicates

In mathematical logic, a predicate is a function that receives a value and returns a boolean value.

The Predicate functional interface is a specialization of a Function that receives a generified value and returns a boolean. A typical use case of the Predicate lambda is to filter a collection of values:

List names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David"); List namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());

In the code above we filter a list using the Stream API and keep only names that start with the letter “A”. The filtering logic is encapsulated in the Predicate implementation.

As in all previous examples, there are IntPredicate, DoublePredicate and LongPredicate versions of this function that receive primitive values.

10. Operators

Operator interfaces are special cases of a function that receive and return the same value type. The UnaryOperator interface receives a single argument. One of its use cases in the Collections API is to replace all values in a list with some computed values of the same type:

List names = Arrays.asList("bob", "josh", "megan"); names.replaceAll(name -> name.toUpperCase());

The List.replaceAll function returns void, as it replaces the values in place. To fit the purpose, the lambda used to transform the values of a list has to return the same result type as it receives. This is why the UnaryOperator is useful here.

Of course, instead of name -> name.toUpperCase(), you can simply use a method reference:

names.replaceAll(String::toUpperCase);

One of the most interesting use cases of a BinaryOperator is a reduction operation. Suppose we want to aggregate a collection of integers in a sum of all values. With Stream API, we could do this using a collector, but a more generic way to do it would be to use the reduce method:

List values = Arrays.asList(3, 5, 8, 9, 12); int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2); 

The reduce method receives an initial accumulator value and a BinaryOperator function. The arguments of this function are a pair of values of the same type, and a function itself contains a logic for joining them in a single value of the same type. Passed function must be associative, which means that the order of value aggregation does not matter, i.e. the following condition should hold:

op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)

The associative property of a BinaryOperator operator function allows to easily parallelize the reduction process.

Of course, there are also specializations of UnaryOperator and BinaryOperator that can be used with primitive values, namely DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, and LongBinaryOperator.

11. Legacy Functional Interfaces

Nu toate interfețele funcționale au apărut în Java 8. Multe interfețe din versiunile anterioare de Java se conformează constrângerilor unei interfețe funcționale și pot fi utilizate ca lambdas. Un exemplu important este interfețele Runable și Callable care sunt utilizate în API-uri simultane. În Java 8 aceste interfețe sunt, de asemenea, marcate cu o adnotare @FunctionalInterface . Acest lucru ne permite să simplificăm mult codul concurenței:

Thread thread = new Thread(() -> System.out.println("Hello From Another Thread")); thread.start();

12. Concluzie

În acest articol, am descris diferite interfețe funcționale prezente în API-ul Java 8 care pot fi utilizate ca expresii lambda. Codul sursă pentru articol este disponibil pe GitHub.