Funktionelle grænseflader i Java 8

1. Introduktion

Denne artikel er en guide til forskellige funktionelle grænseflader, der findes i Java 8, deres generelle brugssager og anvendelse i standard JDK-biblioteket.

2. Lambdas i Java 8

Java 8 bragte en kraftfuld ny syntaktisk forbedring i form af lambda-udtryk. En lambda er en anonym funktion, der kan håndteres som en førsteklasses sprogborger, for eksempel videregivet til eller returneret fra en metode.

Før Java 8 oprettede du normalt en klasse til alle tilfælde, hvor du havde brug for at indkapsle et enkelt stykke funktionalitet. Dette antydede en masse unødvendig kedelpladekode for at definere noget, der tjente som en primitiv funktionsrepræsentation.

Lambdas, funktionelle grænseflader og bedste praksis for at arbejde med dem er generelt beskrevet i artiklen "Lambda-udtryk og funktionelle grænseflader: tip og bedste praksis". Denne vejledning fokuserer på nogle særlige funktionelle grænseflader, der er til stede i java.util.function pakke.

3. Funktionelle grænseflader

Alle funktionelle grænseflader anbefales at have en informativ @FunktionelInterface kommentar. Dette kommunikerer ikke kun klart formålet med denne grænseflade, men tillader også en kompilator at generere en fejl, hvis den kommenterede grænseflade ikke opfylder betingelserne.

Enhver grænseflade med en SAM (Single Abstract Method) er en funktionel grænsefladeog dens implementering kan behandles som lambda-udtryk.

Bemærk, at Java 8'er Standard metoder er ikke abstrakt og tæl ikke med: en funktionel grænseflade kan stadig have flere Standard metoder. Du kan observere dette ved at se på Funktionens dokumentation.

4. 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 Fungere grænseflade, der er parametreret efter typerne af argumentet og en returværdi:

offentlig grænsefladesfunktion {…}

En af anvendelserne af Fungere skriv i standardbiblioteket er Map.computeIfAbsent metode, der returnerer en værdi fra et kort efter nøgle, men beregner en værdi, hvis en nøgle ikke allerede findes på et kort. For at beregne en værdi bruger den den beståede funktionsimplementering:

Map nameMap = ny HashMap (); Heltalsværdi = nameMap.computeIfAbsent ("John", s -> s.length ());

En værdi, i dette tilfælde, beregnes ved at anvende en funktion på en nøgle, placeres inde i et kort og returneres også fra et metodekald. I øvrigt, vi kan erstatte lambda med en metodehenvisning, der matcher bestået og returneret værditype.

Husk, at et objekt, hvor metoden påberåbes, faktisk er det implicitte første argument for en metode, der tillader casting af en instansmetode længde henvisning til en Fungere grænseflade:

Heltalsværdi = nameMap.computeIfAbsent ("John", String :: længde);

Det Fungere interface har også en standard komponere metode, der gør det muligt at kombinere flere funktioner i en og udføre dem sekventielt:

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

Det quoteIntToString funktion er en kombination af citere funktion anvendt på et resultat af intToString fungere.

5. Primitive funktions specialiseringer

Da en primitiv type ikke kan være et generisk typeargument, er der versioner af Fungere interface til de mest anvendte primitive typer dobbelt, int, langog deres kombinationer i argument- og returtyper:

  • IntFunktion, LongFunction, Dobbeltfunktion: argumenter er af specificeret type, returtype parametriseres
  • ToIntFunction, ToLongFunction, ToDoubleFunction: returtype er af specificeret type, argumenter parametreres
  • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction - at have både argument- og returtype defineret som primitive typer som specificeret af deres navne

Der er ingen out-of-the-box funktionel grænseflade til f.eks. En funktion, der tager en kort og returnerer a byte, men intet forhindrer dig i at skrive din egen:

@FunctionalInterface offentlig grænseflade ShortToByteFunction {byte applyAsByte (korte s); }

Nu kan vi skrive en metode, der transformerer en matrix af kort 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 [array.length]; for (int i = 0; i <array.length; i ++) {transformedArray [i] = function.applyAsByte (array [i]); } returner transformedArray; }

Sådan kan vi bruge det til at omdanne en række shorts til række af byte ganget med 2:

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

6. Specialiteter med to aritetsfunktioner

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

BiFunction har genereret både argumenter og en returtype, mens ToDoubleBiFunction og andre giver dig mulighed for at returnere en primitiv værdi.

Et af de typiske eksempler på brug af denne grænseflade i standard API er i Map.replaceAll metode, som gør det muligt at erstatte alle værdier på et kort med en eller anden beregnet værdi.

Lad os bruge en BiFunction implementering, der modtager en nøgle og en gammel værdi for at beregne en ny værdi for lønnen og returnere den.

Kortlønninger = nyt HashMap (); lønninger.put ("John", 40000); lønninger.put ("Freddy", 30000); lønninger.put ("Samuel", 50000); lønninger.replaceAll ((navn, oldValue) -> name.equals ("Freddy")? oldValue: oldValue + 10000);

7. Leverandører

Det Leverandør funktionel grænseflade er endnu en anden Fungere specialisering, der ikke tager nogen argumenter. Det bruges typisk til doven generering af værdier. Lad os for eksempel definere en funktion, der kvadrerer a dobbelt værdi. Den modtager ikke en værdi i sig selv, men en Leverandør af denne værdi:

offentlig dobbelt firkantetLazy (leverandør 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 a Leverandør implementering. Dette kan være nyttigt, hvis genereringen af ​​dette argument tager lang tid. Vi simulerer det ved hjælp af guavas sove uafbrudt metode:

Leverandør lazyValue = () -> {Uninterruptibles.sleepUninterruptibly (1000, TimeUnit.MILLISECONDS); returnere 9d; }; Double valueSquared = squareLazy (lazyValue);

En anden brugssag for leverandøren er at definere en logik til sekvensgenerering. Lad os bruge en statisk for at demonstrere det Stream.genereret metode til at oprette en Strøm af Fibonacci-tal:

int [] fibs = {0, 1}; Strømfibre = Stream.genereret (() -> {int-resultat = fibs [1]; int fib3 = fibs [0] + fibs [1]; fibs [0] = fibs [1]; fibs [1] = fib3; returneresultat;});

Funktionen, der overføres til Stream.genereret metoden implementerer Leverandør funktionel grænseflade. Bemærk, at det skal være nyttigt som en generator, at Leverandør har normalt brug for en slags ekstern tilstand. I dette tilfælde består dens tilstand af to sidste Fibonacci-sekvensnumre.

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

Andre specialiseringer af Leverandør funktionel grænseflade inkluderer Boolsk leverandør, DoubleSupplier, LongSupplier og IntSupplier, hvis returtyper er tilsvarende primitive.

8. Forbrugere

I modsætning til Leverandør, det Forbruger accepterer et genereret argument og returnerer intet. Det er en funktion, der repræsenterer bivirkninger.

Lad os f.eks. Hilse på alle på en liste med navne ved at udskrive hilsenen i konsollen. Lambda gik til List.forEach metoden implementerer Forbruger funktionel grænseflade:

Liste navne = Arrays.asList ("John", "Freddy", "Samuel"); names.forEach (navn -> System.out.println ("Hej" + navn));

Der er også specialiserede versioner af ForbrugerDoubleConsumer, IntConsumer og LongConsumer - der modtager primitive værdier som argumenter. Mere interessant er BiConsumer interface. Et af dets brugssager er iterering gennem posterne på et kort:

Kortalder = ny HashMap (); ages.put ("John", 25); ages.put ("Freddy", 24); ages.put ("Samuel", 30); ages.forEach ((navn, alder) -> System.out.println (navn + "er" + alder + "år gammel"));

Et andet sæt specialiserede BiConsumer versioner består af ObjDoubleConsumer, ObjIntConsumerog ObjLongConsumer som modtager to argumenter, hvoraf den ene genereres, og den anden er en primitiv type.

9. Prædikater

I matematisk logik er et predikat en funktion, der modtager en værdi og returnerer en boolsk værdi.

Det Prædikat funktionel grænseflade er en specialisering af en Fungere der modtager en genereret værdi og returnerer en boolsk. En typisk brugssag for Prædikat lambda skal filtrere en samling værdier:

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

I koden ovenfor filtrerer vi en liste ved hjælp af Strøm API og kun beholde navne, der starter med bogstavet “A”. Filtreringslogikken er indkapslet i Prædikat implementering.

Som i alle tidligere eksempler er der IntPredicate, DoublePredicate og LongPredicate versioner af denne funktion, der modtager primitive værdier.

10. Operatører

Operatør grænseflader er specielle tilfælde af en funktion, der modtager og returnerer den samme værditype. Det UnaryOperator interface modtager et enkelt argument. En af anvendelsestilfælde i Collections API er at erstatte alle værdier på en liste med nogle beregnede værdier af samme type:

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

Det List.replaceAll funktion vender tilbage ugyldig, da det erstatter værdierne på plads. For at passe til formålet skal lambda, der bruges til at omdanne værdierne på en liste, returnere den samme resultattype, som den modtager. Dette er grunden til, at UnaryOperator er nyttigt her.

Selvfølgelig i stedet for navn -> name.toUpperCase (), kan du blot bruge en metodehenvisning:

names.replaceAll (String :: toUpperCase);

En af de mest interessante brugstilfælde af en BinaryOperator er en reduktionsoperation. Antag, at vi ønsker at samle en samling af heltal i en sum af alle værdier. Med Strøm API, vi kunne 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 reducere metode:

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

Det reducere metoden modtager en indledende akkumuleringsværdi og en BinaryOperator fungere. Argumenterne for denne funktion er et par værdier af samme type, og en funktion i sig selv indeholder en logik til sammenføjning af dem til en enkelt værdi af samme type. Bestået funktion skal være associerende, hvilket betyder, at rækkefølgen af ​​aggregering af værdi ikke betyder noget, dvs. følgende betingelse skal gælde:

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

Den associerende egenskab ved en BinaryOperator operatørfunktion gør det 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, IntBinaryOperatorog LongBinaryOperator.

11. Æ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 for a FunctionalInterface og kan bruges som lambdas. Et fremtrædende eksempel er Kan køres og Kan kaldes grænseflader, der bruges i samtidige API'er. I Java 8 er disse grænseflader også markeret med en @FunktionelInterface kommentar. Dette giver os mulighed for i høj grad at forenkle samtidighedskode:

Trådtråd = ny tråd (() -> System.out.println ("Hej fra en anden tråd")); thread.start ();

12. Konklusion

I denne artikel har vi beskrevet forskellige funktionelle grænseflader, der findes i Java 8 API, der kan bruges som lambda-udtryk. Kildekoden til artiklen er tilgængelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found