Funksjonelle grensesnitt i Java 8

1. Introduksjon

Denne artikkelen er en guide til forskjellige funksjonelle grensesnitt som er tilstede i Java 8, deres generelle brukssaker og bruk i standard JDK-biblioteket.

2. Lambdas i Java 8

Java 8 brakte en kraftig ny syntaktisk forbedring i form av lambdauttrykk. En lambda er en anonym funksjon som kan håndteres som en førsteklasses språkborger, for eksempel overført til eller returnert fra en metode.

Før Java 8, ville du vanligvis lage en klasse for hvert tilfelle der du trengte å kapsle inn et enkelt stykke funksjonalitet. Dette antydet mye unødvendig kjeleplatekode for å definere noe som fungerte som en primitiv funksjonsrepresentasjon.

Lambdas, funksjonelle grensesnitt og beste praksis for å jobbe med dem, er generelt beskrevet i artikkelen “Lambda Expressions and Functional Interfaces: Tips and Best Practices”. Denne guiden fokuserer på noen spesielle funksjonelle grensesnitt som er tilstede i java.util.funksjon pakke.

3. Funksjonelle grensesnitt

Alle funksjonelle grensesnitt anbefales å ha en informativ @FunctionalInterface kommentar. Dette kommuniserer ikke bare formålet med dette grensesnittet, men tillater også en kompilator å generere en feil hvis det merkede grensesnittet ikke tilfredsstiller betingelsene.

Ethvert grensesnitt med en SAM (Single Abstract Method) er et funksjonelt grensesnitt, og implementeringen av den kan behandles som lambdauttrykk.

Merk at Java 8-er misligholde metoder er ikke abstrakt og ikke tell: et funksjonelt grensesnitt kan fortsatt ha flere misligholde metoder. Du kan observere dette ved å se på Funksjoner dokumentasjon.

4. Funksjoner

Det mest enkle og generelle tilfellet med en lambda er et funksjonelt grensesnitt med en metode som mottar en verdi og returnerer en annen. Denne funksjonen til et enkelt argument er representert av Funksjon grensesnitt som parametriseres av typene av argumentet og en returverdi:

offentlig grensesnittfunksjon {…}

En av bruken av Funksjon skriv inn standardbiblioteket er Map.computeIfAbsent metode som returnerer en verdi fra et kart etter nøkkel, men beregner en verdi hvis en nøkkel ikke allerede er til stede i et kart. For å beregne en verdi bruker den bestått funksjonsimplementering:

Map nameMap = nytt HashMap (); Heltallverdi = nameMap.computeIfAbsent ("John", s -> s.length ());

En verdi, i dette tilfellet, vil bli beregnet ved å bruke en funksjon på en nøkkel, plassert i et kart og også returnert fra et metodeanrop. Forresten, vi kan erstatte lambda med en metodereferanse som samsvarer med passerte og returnerte verdityper.

Husk at et objekt som metoden påberopes er faktisk det implisitte første argumentet til en metode, som gjør det mulig å kaste en forekomstmetode lengde henvisning til a Funksjon grensesnitt:

Heltallverdi = nameMap.computeIfAbsent ("John", streng :: lengde);

De Funksjon grensesnittet har også en standard komponere metode som gjør det mulig å kombinere flere funksjoner i en og utføre dem sekvensielt:

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

De quoteIntToString funksjon er en kombinasjon av sitat funksjon brukt på et resultat av intToString funksjon.

5. Primitive funksjons spesialiseringer

Siden en primitiv type ikke kan være et generisk argument, finnes det versjoner av Funksjon grensesnitt for mest brukte primitive typer dobbelt, int, lang, og deres kombinasjoner i argument- og returtyper:

  • IntFunction, LongFunction, DoubleFunction: argumentene er av spesifisert type, returtypen er parameterisert
  • ToIntFunction, ToLongFunction, ToDoubleFunction: returtype er av spesifisert type, argumenter parametreres
  • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction - å ha både argument og returtype definert som primitive typer, som spesifisert av deres navn

Det er ikke noe funksjonelt grensesnitt utenom boksen for for eksempel en funksjon som tar en kort og returnerer a byte, men ingenting hindrer deg i å skrive dine egne:

@FunctionalInterface offentlig grensesnitt ShortToByteFunction {byte applyAsByte (korte s); }

Nå kan vi skrive en metode som forvandler en rekke kort til en rekke byte ved hjelp av en regel definert av a 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]); } return transformedArray; }

Slik kan vi bruke den til å transformere en rekke shorts til en rekke byte multiplisert med 2:

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

6. Spesialisering med to aritetsfunksjoner

For å definere lambdas med to argumenter, må vi bruke flere grensesnitt som inneholder “Bi ” nøkkelord i navnene: BiFunction, ToDoubleBiFunction, ToIntBiFunction, og ToLongBiFunction.

BiFunction har både argumenter og en returtype generert, mens ToDoubleBiFunction og andre lar deg returnere en primitiv verdi.

Et av de typiske eksemplene på bruk av dette grensesnittet i standard API er i Map.replaceAll metode, som gjør det mulig å erstatte alle verdier på et kart med en beregnet verdi.

La oss bruke en BiFunction implementering som mottar en nøkkel og en gammel verdi for å beregne en ny verdi for lønnen og returnere den.

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

7. Leverandører

De Leverandør funksjonelt grensesnitt er enda et annet Funksjon spesialisering som ikke tar noen argumenter. Det brukes vanligvis til lat generering av verdier. La oss for eksempel definere en funksjon som kvadrater a dobbelt verdi. Den vil ikke motta en verdi i seg selv, men en Leverandør av denne verdien:

offentlig dobbelt kvadratLazy (Leverandør lazyValue) {return Math.pow (lazyValue.get (), 2); }

Dette lar oss lat generere argumentet for påkallelse av denne funksjonen ved hjelp av a Leverandør gjennomføring. Dette kan være nyttig hvis genereringen av dette argumentet tar lang tid. Vi simulerer det ved hjelp av Guava soveUavbrutt metode:

Leverandør lazyValue = () -> {Uninterruptibles.sleepUninterruptibly (1000, TimeUnit.MILLISECONDS); retur 9d; }; Dobbel verdiSquared = squareLazy (lazyValue);

En annen brukssak for leverandøren er å definere en logikk for sekvensgenerering. For å demonstrere det, la oss bruke en statisk Stream.generert metode for å lage en Strøm av Fibonacci-tall:

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

Funksjonen som blir overført til Stream.generert metoden implementerer Leverandør funksjonelt grensesnitt. Legg merke til at for å være nyttig som generator, kan Leverandør trenger vanligvis en slags ekstern tilstand. I dette tilfellet består tilstanden av to siste Fibonacci-sekvensnummer.

For å implementere denne tilstanden bruker vi en matrise i stedet for et par variabler, fordi alle eksterne variabler som brukes inne i lambda må være effektivt endelige.

Andre spesialiseringer av Leverandør funksjonelt grensesnitt inkluderer Boolsk leverandør, DoubleSupplier, LongSupplier og IntSupplier, hvis returtyper er tilsvarende primitiver.

8. Forbrukere

I motsetning til Leverandør, den Forbruker aksepterer et generert argument og returnerer ingenting. Det er en funksjon som representerer bivirkninger.

La oss for eksempel hilse på alle i en navneliste ved å skrive ut hilsenen i konsollen. Lambda gikk til List.forEach metoden implementerer Forbruker funksjonelt grensesnitt:

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

Det finnes også spesialversjoner av ForbrukerDoubleConsumer, IntConsumer og LongConsumer - som mottar primitive verdier som argumenter. Mer interessant er BiConsumer grensesnitt. En av brukssakene er å gjenta gjennom oppføringene på et kart:

Kartalder = nytt 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 annet sett med spesialiserte BiConsumer versjoner består av ObjDoubleConsumer, ObjIntConsumer, og ObjLongConsumer som mottar to argumenter hvorav den ene er generert, og en annen er en primitiv type.

9. Predikater

I matematisk logikk er et predikat en funksjon som mottar en verdi og returnerer en boolsk verdi.

De Predikere funksjonelt grensesnitt er en spesialisering av en Funksjon som mottar en generert verdi og returnerer en boolsk. Et typisk brukstilfelle for Predikere lambda er å filtrere en samling verdier:

Listenavn = 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 hjelp av Strøm API og behold bare navn som begynner med bokstaven “A”. Filtreringslogikken er innkapslet i Predikere gjennomføring.

Som i alle tidligere eksempler er det IntPredicate, DoublePredicate og LongPredicate versjoner av denne funksjonen som mottar primitive verdier.

10. Operatører

Operatør grensesnitt er spesielle tilfeller av en funksjon som mottar og returnerer samme verditype. De UnaryOperator grensesnitt mottar ett enkelt argument. En av brukssakene i Collections API er å erstatte alle verdier i en liste med noen beregnede verdier av samme type:

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

De List.replaceAll funksjonen returnerer tomrom, da den erstatter verdiene på plass. For å passe formålet må lambda som brukes til å transformere verdiene til en liste, returnere samme resultattype som den mottar. Dette er grunnen til at UnaryOperator er nyttig her.

Selvfølgelig, i stedet for navn -> navn.tilUpperCase (), kan du ganske enkelt bruke en metodereferanse:

names.replaceAll (String :: toUpperCase);

En av de mest interessante brukssakene til a BinaryOperator er en reduksjonsoperasjon. Anta at vi vil samle en samling med heltall i en sum av alle verdier. Med Strøm API, vi kan gjøre dette ved hjelp av en samler, men en mer generisk måte å gjøre det på ville være å bruke redusere metode:

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

De redusere metoden mottar en innledende akkumulatorverdi og en BinaryOperator funksjon. Argumentene for denne funksjonen er et par verdier av samme type, og en funksjon i seg selv inneholder en logikk for å koble dem til en enkelt verdi av samme type. Bestått funksjon må være assosiativ, noe som betyr at rekkefølgen på verdiaggregering ikke betyr noe, dvs. følgende vilkår skal være:

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

Den assosiative egenskapen til en BinaryOperator operatørfunksjon gjør det enkelt å parallellisere reduksjonsprosessen.

Selvfølgelig er det også spesialiseringer av UnaryOperator og BinaryOperator som kan brukes med primitive verdier, nemlig DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, og LongBinaryOperator.

11. Arv funksjonelle grensesnitt

Ikke alle funksjonelle grensesnitt dukket opp i Java 8. Mange grensesnitt fra tidligere versjoner av Java samsvarer med begrensningene for a FunctionalInterface og kan brukes som lambdas. Et fremtredende eksempel er Kjørbar og Kan kalles grensesnitt som brukes i samtidige API-er. I Java 8 er disse grensesnittene også merket med a @FunctionalInterface kommentar. Dette lar oss i stor grad forenkle samtidighetskoden:

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

12. Konklusjon

I denne artikkelen har vi beskrevet forskjellige funksjonelle grensesnitt som finnes i Java 8 API som kan brukes som lambdauttrykk. Kildekoden for artikkelen er tilgjengelig på GitHub.