Funksjonelle Grensesnitt I Java 8

Innledning

denne opplæringen er en veiledning til forskjellige funksjonelle grensesnitt som finnes I Java 8, samt deres generelle brukstilfeller og bruk i standard jdk-biblioteket.

Videre lesing:

Iterable Å Streame I Java

artikkelen forklarer hvordan du konverterer en Iterable Å Streame og hvorfor Det Iterable grensesnittet ikke støtter det direkte.
Les mer →

Hvordan Bruke if/else Logikk I Java 8 Strømmer

Lær hvordan du bruker if / else logikk Til Java 8 Strømmer.
les mer →

Lambdas I Java 8

Java 8 brakte en kraftig ny syntaktisk forbedring i form av lambda-uttrykk. En lambda er en anonym funksjon som vi kan håndtere som en førsteklasses språkborger. For eksempel kan vi sende den til eller returnere den fra en metode.

Før Java 8, ville vi vanligvis lage en klasse for hvert tilfelle der vi trengte å innkapsle et enkelt stykke funksjonalitet. Dette innebar mye unødvendig standardkode for å definere noe som fungerte som en primitiv funksjonsrepresentasjon.

artikkelen» Lambda-Uttrykk Og Funksjonelle Grensesnitt: Tips og Beste Praksis » beskriver mer detaljert de funksjonelle grensesnittene og beste praksis for å jobbe med lambdas. Denne guiden fokuserer på noen spesielle funksjonelle grensesnitt som er til stede i java.util.funksjon pakke.

Funksjonelle Grensesnitt

det anbefales at alle funksjonelle grensesnitt har en informativ @Functional Interface-merknad. Dette kommuniserer klart formålet med grensesnittet, og tillater også en kompilator å generere en feil hvis det annoterte grensesnittet ikke tilfredsstiller betingelsene.ethvert grensesnitt med EN SAM (Single Abstract Method) er et funksjonelt grensesnitt, og implementeringen kan behandles som lambda-uttrykk.

Merk At Java 8s standardmetoder ikke er abstrakte og ikke teller; et funksjonelt grensesnitt kan fortsatt ha flere standardmetoder. Vi kan observere dette ved å se På Funksjonens dokumentasjon.

Funksjoner

det mest enkle og generelle tilfellet av 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 Funksjonsgrensesnittet, som er parameterisert av argumenttypene og en returverdi:

public interface Function<T, R> { … }

En Av bruken Av Funksjonstypen i standardbiblioteket er Kartet.computeifraværende metode. Denne metoden returnerer en verdi fra et kart etter nøkkel, men beregner en verdi hvis en nøkkel ikke allerede finnes i et kart. For å beregne en verdi bruker den bestått Funksjonsimplementering:

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

i dette tilfellet beregner vi en verdi ved å bruke en funksjon til en nøkkel, sette inn et kart, og også returnert fra et metodekall. Vi kan erstatte lambda med en metodereferanse som samsvarer med passerte og returnerte verdityper.Husk at et objekt vi påkaller metoden på, faktisk er det implisitte første argumentet til en metode. Dette tillater oss å kaste en forekomst metode lengde referanse Til En Funksjon grensesnitt:

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

Funksjonsgrensesnittet har også en standard komponeringsmetode som gjør at vi kan kombinere flere funksjoner til en og utføre dem sekvensielt:

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

quoteIntToString-funksjonen er en kombinasjon av sitatfunksjonen som brukes på et resultat av intToString-funksjonen.

Primitive Funksjonsspesialiseringer

siden en primitiv type ikke kan være et generisk type argument, er det versjoner Av Funksjonsgrensesnittet for de mest brukte primitive typene double, int, long og deres kombinasjoner 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: som et eksempel er det ikke noe funksjonelt grensesnitt for en funksjon som tar kort og returnerer en byte, men ingenting stopper oss fra å skrive vår egen:
    @FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}

    nå kan Vi skrive en metode Som forvandler en rekke kort til en rekke byte ved hjelp av en regel definert av En Shorttobytefunksjon:

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

    slik kan vi bruke den til å forvandle en rekke shorts til en rekke byte multiplisert 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 Funksjon Spesialiseringer

    for å definere lambdas med to argumenter, må vi bruke flere grensesnitt som inneholder «Bi»søkeord i deres navn: BiFunction, ToDoubleBiFunction, ToIntBiFunction, Og ToLongBiFunction.

    Bifunksjon har både argumenter og en returtype generifisert, Mens Todobbifunksjon og andre tillater oss å returnere en primitiv verdi.Et av de typiske eksemplene på å bruke dette grensesnittet i STANDARD API er I Kartet.replaceAll metode, som lar erstatte alle verdier i et kart med noen beregnet verdi.

    La Oss bruke En Bifunksjonsimplementering som mottar en nøkkel og en gammel verdi for å beregne en ny verdi 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 funksjonelle grensesnitt er enda en funksjonspesialisering som ikke tar noen argumenter. Vi bruker vanligvis den til lat generering av verdier. For eksempel, la oss definere en funksjon som kvadrerer en dobbel verdi. Det vil ikke motta en verdi selv, men En Leverandør av denne verdien:

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

    Dette tillater oss å lazily generere argumentet for påkalling av denne funksjonen ved Hjelp Av En Leverandørimplementering. Dette kan være nyttig hvis genereringen av argumentet tar betydelig tid. Vi simulerer Det Ved Hjelp Av guavas sleepUninterruptibly metode:

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

    Et annet brukstilfelle for Leverandøren definerer logikk for sekvensgenerering. For å demonstrere det, la oss bruke en statisk Strøm.generer metode for å lage En Strøm Av Fibonacci-tall:

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

    funksjonen som vi sender Til Strømmen.generer metoden implementerer Leverandorens funksjonelle grensesnitt. Legg merke til at For å være nyttig som generator, Må Leverandøren vanligvis en slags ekstern tilstand. I dette tilfellet består staten av de to Siste Fibonacci-sekvensnumrene.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ørens funksjonelle grensesnitt inkluderer BooleanSupplier, DoubleSupplier, LongSupplier og IntSupplier, hvis returtyper er tilsvarende primitiver.

    Forbrukere

    I motsetning Til Leverandøren aksepterer Forbrukeren et generalisert argument og returnerer ingenting. Det er en funksjon som representerer bivirkninger.

    la oss for eksempel hilse på alle i en liste med navn ved å skrive ut hilsen i konsollen. Lambda gikk til Listen.forEach-metoden implementerer Forbrukerfunksjonsgrensesnittet:

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

    det finnes også spesialiserte versjoner Av Forbrukeren-DoubleConsumer, IntConsumer og LongConsumer – som mottar primitive verdier som argumenter. Mer interessant Er BiConsumer-grensesnittet. En av brukstilfellene er iterating gjennom oppføringene av et kart:

    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 annet sett med spesialiserte Bikonsumerversjoner består Av ObjDoubleConsumer, ObjIntConsumer og ObjLongConsumer, som mottar to argumenter; en av argumentene er generifisert, og den andre er en primitiv type.

    Predikater

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

    Predikatfunksjonsgrensesnittet er en spesialisering av En Funksjon som mottar en generifisert verdi og returnerer en boolsk. Et typisk brukstilfelle av predikatet lambda er å filtrere en samling av verdier:

    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 Hjelp Av Stream API og beholder bare navnene som begynner med bokstaven «A». Predikatimplementeringen innkapsler filtreringslogikken.

    Som i alle de tidligere eksemplene er Det IntPredicate, DoublePredicate Og LongPredicate versjoner av denne funksjonen som mottar primitive verdier.

    Operatorer

    Operatorgrensesnitt Er spesielle tilfeller av en funksjon som mottar og returnerer samme verditype. Unaryoperator-grensesnittet mottar et enkelt argument. Et av brukssakene i Collections API er å erstatte alle verdier i en liste med noen beregnede verdier av samme type:

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

    Listen.replaceAll funksjonen returnerer ugyldig som den erstatter verdiene på plass. For å passe til formålet må lambdaen som brukes til å transformere verdiene til en liste, returnere samme resultattype som den mottar. Det er derfor UnaryOperator er nyttig her.

    selvfølgelig, i stedet for navn -> navn.toUpperCase (), kan vi bare bruke en metodereferanse:

    names.replaceAll(String::toUpperCase);

    En Av De mest interessante brukssakene Til En BinaryOperator er en reduksjonsoperasjon. Anta at vi vil aggregere en samling av heltall i en sum av alle verdier. Med Stream API kunne vi gjøre dette ved hjelp av en samler, men en mer generisk måte å gjøre det på ville være å bruke reduce-metoden:

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

    reduce-metoden mottar en innledende akkumulatorverdi og En Binaryoperatorfunksjon. Argumentene til denne funksjonen er et par verdier av samme type; funksjonen i seg selv inneholder også en logikk for å bli med i en enkelt verdi av samme type. Følgende betingelse skal holde:

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

    den assosiative egenskapen Til En Binaryoperatoroperatørfunksjon gjør at vi enkelt kan parallelisere 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.

    Eldre Funksjonelle Grensesnitt

    Ikke alle funksjonelle grensesnitt dukket opp I Java 8. Mange grensesnitt fra tidligere Versjoner av Java samsvarer med begrensningene Til Et Funksjonelt Grensesnitt, og vi kan bruke dem som lambdas. Fremtredende eksempler er runnable og Callable grensesnitt som brukes i samtidige Apier. I Java 8 er disse grensesnittene også merket med en @FunctionalInterface annotasjon. Dette gjør at vi i stor grad kan forenkle samtidighetskoden:

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

    Konklusjon

Related Posts

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *