funktionelle grænseflader i Java 8

introduktion

denne vejledning er en guide til forskellige funktionelle grænseflader, der findes i Java 8, samt deres generelle brugssager og brug i standard JDK-biblioteket.

yderligere læsning:

Iterable at streame i Java

artiklen forklarer, hvordan man konverterer en Iterable til Stream, og hvorfor den Iterable grænseflade ikke understøtter den direkte.
læs mere

Sådan bruges If/else Logic i Java 8 Streams

Lær hvordan du anvender if / else logic til Java 8 Streams.
Læs mere Kris

Lambdas i Java 8

Java 8 bragte en kraftig ny syntaktisk forbedring i form af lambda udtryk. En lambda er en anonym funktion, som vi kan håndtere som førsteklasses sprogborger. For eksempel kan vi sende det til eller returnere det fra en metode.

før Java 8 ville vi normalt oprette en klasse for hvert tilfælde, hvor vi havde brug for at indkapsle et enkelt stykke funktionalitet. Dette indebar en masse unødvendig kedelpladekode for at definere noget, der tjente som en primitiv funktionsrepræsentation.

artiklen “Lambda-udtryk og funktionelle grænseflader: tip og bedste praksis” beskriver mere detaljeret de funktionelle grænseflader og bedste praksis ved at arbejde med lambdas. Denne vejledning fokuserer på nogle særlige funktionelle grænseflader, der er til stede i java.util.funktion pakke.

funktionelle grænseflader

det anbefales, at alle funktionelle grænseflader har en informativ @FunctionalInterface annotation. Dette kommunikerer klart formålet med grænsefladen og tillader også en kompilator at generere en fejl, hvis den kommenterede grænseflade ikke opfylder betingelserne.

enhver grænseflade med en SAM(enkelt abstrakt metode) er en funktionel grænseflade, og dens implementering kan behandles som lambda-udtryk.

Bemærk, at Java 8 ‘ s standardmetoder ikke er abstrakte og ikke tæller; en funktionel grænseflade kan stadig have flere standardmetoder. Vi kan observere dette ved at se på funktionens dokumentation.

funktioner

det mest enkle og generelle tilfælde af en lambda er en funktionel grænseflade med en metode, der modtager en værdi og returnerer en anden. Denne funktion af et enkelt argument er repræsenteret af Funktionsgrænsefladen, som parameteriseres af typerne af dets argument og en returværdi:

public interface Function<T, R> { … }

en af anvendelserne af Funktionstypen i standardbiblioteket er kortet.computeifasent metode. Denne metode returnerer en værdi fra et kort efter nøgle, men beregner en værdi, hvis en nøgle ikke allerede findes i et kort. For at beregne en værdi bruger den den beståede funktionsimplementering:

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

i dette tilfælde beregner vi en værdi ved at anvende en funktion på en tast, sætte inde i et kort og også returneres fra et metodeopkald. Vi kan erstatte lambda med en metode reference, der matcher bestået og returnerede værdi typer.

Husk, at et objekt, vi påberåber metoden på, faktisk er det implicitte første argument for en metode. Dette giver os mulighed for at kaste en instansmetodelængdehenvisning til en Funktionsgrænseflade:

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

Funktionsgrænsefladen har også en standard komponeringsmetode, der giver os mulighed for at kombinere flere funktioner i en og udføre dem sekventielt:

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

citatfunktionen er en kombination af citatfunktionen anvendt på et resultat af intToString-funktionen.

Primitive Funktionsspecialiseringer

da en primitiv type ikke kan være et generisk type argument, er der versioner af Funktionsgrænsefladen for de mest anvendte primitive typer dobbelt, int, lang og deres kombinationer i argument-og returtyper:

  • 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: at have både argument og returtype defineret som primitive typer, som specificeret af deres navne

som et eksempel er der ingen funktionel grænseflade uden for boksen til en funktion, der tager en kort og returnerer en byte, men intet forhindrer os i at skrive vores egen:

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

nu kan vi skrive en metode, der omdanner en række korte til en række byte ved hjælp af en regel defineret af en ShortToByteFunction:

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;}

sådan kan vi bruge det til at omdanne en række shorts til en række bytes ganget med 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);

to-Arity funktion specialiseringer

for at definere lambdas med to argumenter skal vi bruge yderligere grænseflader, der indeholder “Bi” nøgleord i deres navne: BiFunction, ToDoubleBiFunction, ToIntBiFunction og ToLongBiFunction.

BiFunction har både argumenter og en returtype generificeret, mens ToDoubleBiFunction og andre tillader os at returnere en primitiv værdi.

et af de typiske eksempler på at bruge denne grænseflade i Standard API er på kortet.replaceAll metode, som gør det muligt at erstatte alle værdier i et kort med nogle beregnede værdi.

lad os bruge en Bifunktionsimplementering, der modtager en nøgle og en gammel værdi til at beregne en ny værdi for lønnen og returnere den.

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);

leverandører

leverandørens funktionelle grænseflade er endnu en Funktionsspecialisering, der ikke tager nogen argumenter. Vi bruger det typisk til doven generering af værdier. Lad os for eksempel definere en funktion, der kvadrerer en dobbelt værdi. Det vil ikke modtage en værdi selv, men en leverandør af denne værdi:

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

dette giver os mulighed for dovent at generere argumentet for påkaldelse af denne funktion ved hjælp af en Leverandørimplementering. Dette kan være nyttigt, hvis genereringen af argumentet tager en betydelig mængde tid. Vi simulerer det ved hjælp af guavas sleepuninterruptible metode:

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

en anden brugssag for leverandøren definerer logik for sekvensgenerering. For at demonstrere det, lad os bruge en statisk strøm.generer metode til at oprette en strøm af Fibonacci-numre:

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

den funktion, vi videregiver til strømmen.generer metode implementerer leverandøren funktionel grænseflade. Bemærk, at leverandøren normalt har brug for en slags ekstern tilstand for at være nyttig som generator. I dette tilfælde omfatter dens tilstand de sidste to Fibonacci-sekvensnumre.

for at implementere denne tilstand bruger vi et array i stedet for et par variabler, fordi alle eksterne variabler, der bruges inde i lambda, skal være effektivt endelige.

andre specialiseringer af leverandørens funktionelle interface inkluderer BooleanSupplier, DoubleSupplier, LongSupplier og IntSupplier, hvis returtyper er tilsvarende primitiver.

forbrugere

I modsætning til leverandøren accepterer forbrugeren et generaliseret argument og returnerer intet. Det er en funktion, der repræsenterer bivirkninger.

lad os for eksempel hilse alle på en liste med navne ved at udskrive hilsenen i konsollen. Lambda gik til listen.forehver metode implementerer forbrugerens funktionelle grænseflade:

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

Der er også specialiserede versioner af forbrugeren — DoubleConsumer, IntConsumer og LongConsumer — der modtager primitive værdier som argumenter. Mere interessant er BiConsumer-grænsefladen. En af dens brugssager gentager gennem posterne på et kort:

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"));

et andet sæt specialiserede BiConsumer-versioner består af ObjDoubleConsumer, ObjIntConsumer og ObjLongConsumer, der modtager to argumenter; et af argumenterne er generificeret, og det andet er en primitiv type.

prædikater

i matematisk logik er et prædikat en funktion, der modtager en værdi og returnerer en boolsk værdi.

Prædikatfunktionsgrænsefladen er en specialisering af en funktion, der modtager en generificeret værdi og returnerer en boolsk. Et typisk brugstilfælde af prædikatet lambda er at filtrere en samling værdier:

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

i koden ovenfor filtrerer vi en liste ved hjælp af Stream API og beholder kun de Navne, der starter med bogstavet “a”. Implementeringen af prædikatet indkapsler filtreringslogikken.

som i alle de foregående eksempler er der IntPredicate, DoublePredicate og LongPredicate versioner af denne funktion, der modtager primitive værdier.

operatorer

Operatørgrænseflader er særlige tilfælde af en funktion, der modtager og returnerer den samme værditype. Unaryoperator-grænsefladen modtager et enkelt argument. Et af dets brugssager i collections API er at erstatte alle værdier på en liste med nogle beregnede værdier af samme type:

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

listen.replaceAll-funktionen returnerer ugyldig, da den erstatter de værdier, der er på plads. For at passe til formålet skal den lambda, der bruges til at transformere værdierne på en liste, returnere den samme resultattype, som den modtager. Derfor er Unaryoperatoren nyttig her.

selvfølgelig, i stedet for navn -> navn.toUpperCase (), kan vi blot bruge en metode reference:

names.replaceAll(String::toUpperCase);

et af de mest interessante brugssager af en BinaryOperator er en reduktionsoperation. Antag, at vi vil samle en samling heltal i en sum af alle værdier. Med Stream API kunne vi gøre dette ved hjælp af en samler, men en mere generisk måde at gøre det på ville være at bruge reducer-metoden:

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

reducer-metoden modtager en indledende akkumulatorværdi og en Binaryoperatorfunktion. Argumenterne for denne funktion er et par værdier af samme type; selve funktionen indeholder også en logik for at forbinde dem i en enkelt værdi af samme type. Den beståede funktion skal være associativ, hvilket betyder, at rækkefølgen af værdiaggregering ikke betyder noget, dvs.følgende betingelse skal holde:

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

den associative egenskab ved en Binaryoperatoroperatørfunktion giver os mulighed for let at parallelisere reduktionsprocessen.

selvfølgelig er der også specialiseringer af UnaryOperator og BinaryOperator, der kan bruges med primitive værdier, nemlig DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator og LongBinaryOperator.

Ældre funktionelle grænseflader

ikke alle funktionelle grænseflader dukkede op i Java 8. Mange grænseflader fra tidligere versioner af Java overholder begrænsningerne i en Funktionelinterface, og vi kan bruge dem som lambdas. Fremtrædende eksempler inkluderer de kørbare og konverterbare grænseflader, der bruges i samtidige API ‘ er. I Java 8 er disse grænseflader også markeret med en @FunctionalInterface annotation. Dette giver os mulighed for i høj grad at forenkle samtidighedskoden:

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

konklusion

Related Posts

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *