- Introducere
- lecturi suplimentare:
- Iterabil pentru a transmite în flux în Java
- cum se utilizează logica if/else în fluxurile Java 8
- Lambdas în Java 8
- interfețe funcționale
- funcții
- specializări de funcții Primitive
- specializări cu două funcții
- furnizori
- consumatori
- predicate
- operatorii
- interfețe funcționale moștenite
- concluzie
Introducere
acest tutorial este un ghid pentru diferite interfețe funcționale prezente în Java 8, precum și cazurile lor de utilizare generală și utilizarea în biblioteca standard JDK.
lecturi suplimentare:
Iterabil pentru a transmite în flux în Java
cum se utilizează logica if/else în fluxurile Java 8
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ă pe care o putem gestiona ca cetățean de limbă de primă clasă. De exemplu, îl putem transmite sau returna dintr-o metodă.
înainte de Java 8, am crea de obicei o clasă pentru fiecare caz în care trebuia să încapsulăm o singură bucată de funcționalitate. Acest lucru a implicat o mulțime de cod de șabloane inutile pentru a defini ceva care a servit ca reprezentare primitivă a funcției.
Articolul „expresii Lambda și interfețe funcționale: sfaturi și cele mai bune practici” descrie mai detaliat interfețele funcționale și cele mai bune practici de lucru cu Lambda. Acest ghid se concentrează pe unele interfețe funcționale particulare care sunt prezente în java.util.pachet de funcții.
interfețe funcționale
se recomandă ca toate interfețele funcționale să aibă o adnotare informativă @FunctionalInterface. Acest lucru comunică în mod clar scopul interfeței și permite, de asemenea, unui compilator să genereze o eroare dacă interfața adnotată nu îndeplinește condițiile.
orice interfață cu SAM(metoda abstractă unică) este o interfață funcțională, iar implementarea ei 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. Putem observa acest lucru uitându-ne la documentația funcției.
funcții
cel mai simplu și general caz al unui 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ției, care este parametrizată de tipurile argumentului său și de o valoare returnată:
public interface Function<T, R> { … }
una dintre utilizările tipului de funcție din biblioteca standard este harta.metoda computeIfAbsent. Această metodă returnează o valoare dintr-o hartă După 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<String, Integer> nameMap = new HashMap<>();Integer value = nameMap.computeIfAbsent("John", s -> s.length());
În acest caz, vom calcula o valoare aplicând o funcție unei chei, pusă în interiorul unei hărți și, de asemenea, returnată dintr-un apel de metodă. Putem înlocui lambda cu o metodă de referință care se potrivește cu tipurile de valori trecute și returnate.amintiți-vă că un obiect pe care invocăm metoda este, de fapt, primul argument implicit al unei metode. Acest lucru ne permite să arunce o referință lungime metodă instanță la o interfață funcție:
Integer value = nameMap.computeIfAbsent("John", String::length);
interfața funcției are, de asemenea, o metodă implicită de compunere care ne permite să combinăm mai multe funcții într-una singură și să le executăm secvențial:
Function<Integer, String> intToString = Object::toString;Function<String, String> quote = s -> "'" + s + "'";Function<Integer, String> quoteIntToString = quote.compose(intToString);assertEquals("'5'", quoteIntToString.apply(5));
funcția quoteIntToString este o combinație a funcției quoteinttostring aplicată unui rezultat al funcției intToString.
specializări de funcții Primitive
deoarece un tip primitiv nu poate fi un argument de tip generic, există versiuni ale interfeței funcționale pentru cele mai utilizate tipuri primitive double, int, long și combinațiile lor în tipuri de argumente și returnări:
- IntFunction, LongFunction, DoubleFunction: arguments are of specified type, return type is parameterized
- ToIntFunction, ToLongFunction, ToDoubleFunction: return type is of specified type, arguments are parameterized
- DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction: având atât argumentul, cât și tipul de retur definite ca tipuri primitive, așa cum sunt specificate de numele lor
ca exemplu, nu există o interfață funcțională out-of-the-box pentru o funcție care ia un scurt și returnează un octet, dar nimic nu ne oprește să scriem propriul nostru:
@FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}
acum putem scrie o metodă care transformă o matrice de scurt într-o serie de octeți folosind o regulă definită de o/p>
public byte transformArray(short array, ShortToByteFunction function) { byte transformedArray = new byte; for (int i = 0; i < array.length; i++) { transformedArray = function.applyAsByte(array); } return transformedArray;}
iată cum l-am putea folosi pentru a transforma o serie de pantaloni scurți într-o 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);
specializări cu două funcții
pentru a defini Lambda-urile cu două argumente, trebuie să folosim interfețe suplimentare care conțin cuvinte cheie „Bi” în numele lor: Bifuncție, Todoublebifuncție, Tointbifuncție și Tolongbifuncție.Bifuncția are atât argumente, cât și un tip de returnare generificat, în timp ce Todoublebifuncția și altele ne permit să returnăm o valoare primitivă.
unul dintre exemplele tipice de utilizare a acestei interfețe în API-ul standard este în hartă.metoda replaceAll, care permite înlocuirea tuturor valorilor dintr-o hartă cu o anumită valoare calculată.
să folosim o implementare Bifuncțională care primește o cheie și o valoare veche pentru a calcula o nouă valoare pentru salariu și a o returna.
Map<String, Integer> 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);
furnizori
interfața funcțională furnizor este încă o Altă specializare funcție care nu ia nici un argument. De obicei îl folosim pentru generarea leneșă de valori. De exemplu, să definim o funcție care pătrate o valoare dublă. Nu va primi o valoare în sine, ci un furnizor de această valoare:
public double squareLazy(Supplier<Double> 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 argumentului durează o perioadă considerabilă de timp. Vom simula că folosind metoda sleepuninterruptibil de Guava:
Supplier<Double> lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d;};Double valueSquared = squareLazy(lazyValue);
Un alt caz de utilizare pentru furnizor este definirea logicii pentru generarea secvenței. Pentru a demonstra acest lucru, să folosim un flux static.generați metoda pentru a crea un flux de numere Fibonacci:
int fibs = {0, 1};Stream<Integer> fibonacci = Stream.generate(() -> { int result = fibs; int fib3 = fibs + fibs; fibs = fibs; fibs = fib3; return result;});
funcția pe care o trecem la Flux.metoda generare 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 cuprinde ultimele două numere de secvență Fibonacci.
pentru a implementa această stare, folosim o matrice în loc de câteva variabile, deoarece toate variabilele externe utilizate în interiorul 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.
consumatori
spre deosebire de furnizor, consumatorul acceptă un argument generificat ș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 salutul în consolă. Lambda a trecut pe listă.metoda forEach implementează interfața funcțională a consumatorului:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");names.forEach(name -> System.out.println("Hello, " + name));
există, de asemenea, versiuni specializate ale consumatorului — DoubleConsumer, IntConsumer și LongConsumer — care primesc valori primitive ca argumente. Mai interesant este interfața BiConsumer. Unul dintre cazurile sale de utilizare este iterarea prin intrările unei hărți:
Map<String, Integer> 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"));
Un alt set de versiuni BiConsumer specializate este format din ObjDoubleConsumer, ObjIntConsumer și ObjLongConsumer, care primesc două argumente; unul dintre argumente este generificat, iar celălalt este un tip primitiv.
predicate
în logica matematică, un predicat este o funcție care primește o valoare și returnează o valoare booleană.
interfața funcțională predicat este o specializare a unei funcții care primește o valoare generificată și returnează un boolean. Un caz tipic de utilizare a predicatului lambda este de a filtra o colecție de valori:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");List<String> namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
în codul de mai sus, filtrăm o listă folosind API-ul Stream și păstrăm doar Numele care încep cu litera „a”. Implementarea predicatului încapsulează logica de filtrare.
ca în toate exemplele anterioare, există versiuni IntPredicate, DoublePredicate și LongPredicate ale acestei funcții care primesc valori primitive.
operatorii
interfețele Operatorului sunt cazuri speciale ale unei funcții care primesc și returnează același tip de valoare. Interfața UnaryOperator primește un singur argument. Unul dintre cazurile sale de utilizare în API colecții este de a înlocui toate valorile dintr-o listă cu unele valori calculate de același tip:
List<String> names = Arrays.asList("bob", "josh", "megan");names.replaceAll(name -> name.toUpperCase());
lista.funcția replaceAll returnează void deoarece înlocuiește valorile în loc. Pentru a se potrivi scopului, lambda folosit pentru a transforma valorile unei liste trebuie să returneze același tip de rezultat pe care îl primește. Acesta este motivul pentru care UnaryOperator este util aici.
desigur, în loc de nume- > nume.toUpperCase (), putem folosi pur și simplu o referință metodă:
names.replaceAll(String::toUpperCase);
unul dintre cele mai interesante cazuri de utilizare a unui Binaroperator este o operație de reducere. Să presupunem că dorim să agregăm o colecție de numere întregi într-o sumă a tuturor valorilor. Cu Stream API, am putea face acest lucru folosind un colector, dar o modalitate mai generică de a face acest lucru ar fi utilizarea metodei reduce:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2);
metoda reduce primește o valoare inițială a acumulatorului și o funcție BinaryOperator. Argumentele acestei funcții sunt o pereche de valori de același tip; funcția în sine conține, de asemenea, o logică pentru a le uni într-o singură valoare de același tip. Funcția trecută trebuie să fie asociativă, ceea ce înseamnă că ordinea agregării valorii nu contează, adică următoarea condiție ar trebui să dețină:
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
proprietatea asociativă a unei funcții de operator BinaryOperator ne permite să paralelizăm cu ușurință procesul de reducere.
desigur, există și specializări de UnaryOperator și BinaryOperator care pot fi utilizate cu valori primitive, și anume DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator și LongBinaryOperator.
interfețe funcționale moștenite
nu toate interfețele funcționale au apărut în Java 8. Multe interfețe din versiunile anterioare de Java sunt conforme cu constrângerile unei Funcționaleinterfață, și le putem folosi ca Lambda-uri. Exemple proeminente includ interfețele rulabile și apelabile care sunt utilizate în API-urile de concurență. În Java 8, aceste interfețe sunt, de asemenea, marcate cu o adnotare @FunctionalInterface. Acest lucru ne permite să simplificăm foarte mult Codul de concurență:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));thread.start();