funktionella gränssnitt i Java 8

introduktion

denna handledning är en guide till olika funktionella gränssnitt som finns i Java 8, liksom deras allmänna användningsfall och användning i standard JDK-biblioteket.

Vidare läsning:

Iterable att strömma i Java

artikeln förklarar hur man konverterar en Iterable att strömma och varför det Iterable gränssnittet inte stöder det direkt.läs mer om hur du använder if/else Logic i Java 8 Streams

Läs mer om hur du använder if/else logic i Java 8 Streams

lär dig hur du använder if/else logic i Java 8 Streams.
läs mer

Lambdas i Java 8

Java 8 gav en kraftfull ny syntaktisk förbättring i form av lambda-uttryck. En lambda är en anonym funktion som vi kan hantera som en förstklassig språkmedborgare. Till exempel kan vi skicka den till eller returnera den från en metod.

före Java 8 skulle vi vanligtvis skapa en klass för varje fall där vi behövde kapsla in en enda funktionalitet. Detta innebar mycket onödig standardkod för att definiera något som fungerade som en primitiv funktionsrepresentation.

artikeln ”Lambda-uttryck och funktionella gränssnitt: Tips och bästa praxis” beskriver mer detaljerat de funktionella gränssnitten och bästa metoderna för att arbeta med lambdas. Denna guide fokuserar på några speciella funktionella gränssnitt som finns i java.util.funktion paket.

funktionella gränssnitt

det rekommenderas att alla funktionella gränssnitt har en informativ @FunctionalInterface-anteckning. Detta kommunicerar tydligt syftet med gränssnittet och tillåter också en kompilator att generera ett fel om det kommenterade gränssnittet inte uppfyller villkoren.varje gränssnitt med en SAM (single Abstract Method) är ett funktionellt gränssnitt, och dess genomförande kan behandlas som lambda-uttryck.

Observera att Java 8: s standardmetoder inte är abstrakta och räknas inte; ett funktionellt gränssnitt kan fortfarande ha flera standardmetoder. Vi kan observera detta genom att titta på funktionens dokumentation.

funktioner

det enklaste och allmänna fallet med en lambda är ett funktionellt gränssnitt med en metod som tar emot ett värde och returnerar ett annat. Denna funktion av ett enda argument representeras av Funktionsgränssnittet, som parametreras av typerna av dess argument och ett returvärde:

public interface Function<T, R> { … }

en av användningarna av Funktionstypen i standardbiblioteket är kartan.computeifasent metod. Den här metoden returnerar ett värde från en karta efter nyckel, men beräknar ett värde om en nyckel inte redan finns i en karta. För att beräkna ett värde använder det den godkända funktionsimplementeringen:

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

i det här fallet beräknar vi ett värde genom att tillämpa en funktion på en nyckel, sätta in i en karta och returneras också från ett metodsamtal. Vi kan ersätta lambda med en metodreferens som matchar godkända och returnerade värdetyper.

Kom ihåg att ett objekt vi åberopar metoden på är faktiskt det implicita första argumentet för en metod. Detta gör det möjligt för oss att kasta en instans metod längd referens till en funktion gränssnitt:

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

Funktionsgränssnittet har också en standardkomponeringsmetod som gör att vi kan kombinera flera funktioner i en och köra dem i följd:

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-funktionen är en kombination av citatfunktionen som tillämpas på ett resultat av intToString-funktionen.

primitiva Funktionsspecialiseringar

eftersom en primitiv typ inte kan vara ett generiskt typargument finns det versioner av Funktionsgränssnittet för de mest använda primitiva typerna double, int, long och deras kombinationer i argument och 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: med både argument och returtyp definierad som primitiva typer, som specificeras av deras namn

som ett exempel finns det inget funktionellt gränssnitt för en funktion som tar en kort och returnerar en byte, men ingenting hindrar oss från att skriva vår egen:

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

Nu kan vi skriva en metod som omvandlar en matris med kort till en matris med byte med en regel definierad av en Shorttobytefunktion:

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å här kan vi använda den för att omvandla en rad shorts till en rad byte multiplicerat 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);

två-Arity funktion specialiseringar

för att definiera lambdas med två argument måste vi använda ytterligare gränssnitt som innehåller” Bi ” nyckelord i deras namn: Bifunktion, Todoublebifunktion, Tointbifunktion och Tolongbifunktion.

Bifunktion har både argument och en returtyp generifierad, medan Todoublebifunktion och andra tillåter oss att returnera ett primitivt värde.

ett av de typiska exemplen på att använda detta gränssnitt i standard API är på kartan.replaceAll-metoden, som gör det möjligt att ersätta alla värden i en karta med något beräknat värde.

Låt oss använda en bifunktionsimplementering som tar emot en nyckel och ett gammalt värde för att beräkna ett nytt värde för lönen och returnera det.

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

leverantörer

leverantörens funktionella gränssnitt är ännu en funktionsspecialisering som inte tar några argument. Vi använder det vanligtvis för lat generering av värden. Låt oss till exempel definiera en funktion som kvadrerar ett dubbelvärde. Det kommer inte att få ett värde själv, utan en leverantör av detta värde:

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

detta gör att vi enkelt kan generera argumentet för anrop av denna funktion med hjälp av en Leverantörsimplementering. Detta kan vara användbart om genereringen av argumentet tar mycket tid. Vi simulerar det med guavas sömnlös metod:

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

ett annat användningsfall för leverantören definierar logik för sekvensgenerering. För att visa det, låt oss använda en statisk ström.generera metod för att skapa en ström av Fibonacci-nummer:

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

funktionen som vi skickar till strömmen.generera metod implementerar leverantörens funktionella gränssnitt. Observera att för att vara användbar som generator behöver leverantören vanligtvis någon form av externt tillstånd. I detta fall innefattar dess tillstånd de två sista Fibonacci-sekvensnumren.

för att implementera detta tillstånd använder vi en matris istället för ett par variabler eftersom alla externa variabler som används i lambda måste vara effektivt slutliga.

andra specialiseringar av leverantörens funktionella gränssnitt inkluderar BooleanSupplier, DoubleSupplier, LongSupplier och IntSupplier, vars returtyper motsvarar primitiva.

konsumenter

i motsats till leverantören accepterar konsumenten ett generifierat argument och returnerar ingenting. Det är en funktion som representerar biverkningar.

till exempel, låt oss hälsa alla i en lista med namn genom att skriva ut hälsningen i konsolen. Lambda gick till listan.forEach-metoden implementerar konsumentens funktionella gränssnitt:

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

det finns också specialiserade versioner av konsumenten — DoubleConsumer, IntConsumer och LongConsumer — som får primitiva värden som argument. Mer intressant är BiConsumer-gränssnittet. Ett av dess användningsfall itererar genom posterna på en karta:

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

en annan uppsättning specialiserade bikonsumerversioner består av ObjDoubleConsumer, ObjIntConsumer och ObjLongConsumer, som får två argument; ett av argumenten är generifierat och det andra är en primitiv typ.

predikat

i matematisk logik är ett predikat en funktion som tar emot ett värde och returnerar ett booleskt värde.

predikatfunktionellt gränssnitt är en specialisering av en funktion som tar emot ett generifierat värde och returnerar en boolesk. Ett typiskt användningsfall för predikatet lambda är att filtrera en samling värden:

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 ovan filtrerar vi en lista med Stream API och behåller bara Namnen som börjar med bokstaven”a”. Implementeringen av predikat inkapslar filtreringslogiken.

som i alla tidigare exempel finns det IntPredicate, DoublePredicate och LongPredicate versioner av denna funktion som får primitiva värden.

operatorer

operatörsgränssnitt är speciella fall av en funktion som tar emot och returnerar samma värdetyp. Unaryoperator-gränssnittet får ett enda argument. En av dess användningsfall i samlingar API är att ersätta alla värden i en lista med vissa beräknade värden av samma typ:

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

listan.replaceAll-funktionen returnerar void eftersom den ersätter värdena på plats. För att passa syftet måste lambda som används för att omvandla värdena på en lista returnera samma resultattyp som den tar emot. Det är därför Unaryoperatorn är användbar här.

naturligtvis istället för namn -> namn.toUpperCase (), vi kan helt enkelt använda en metodreferens:

names.replaceAll(String::toUpperCase);

ett av de mest intressanta användningsfallen för en Binäroperator är en reduktionsoperation. Antag att vi vill aggregera en samling heltal i en summa av alla värden. Med Stream API kan vi göra detta med en samlare, men ett mer generiskt sätt att göra det skulle vara att använda reduce-metoden:

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

reduce-metoden får ett initialt ackumulatorvärde och en Binaryoperatorfunktion. Argumenten för denna funktion är ett par värden av samma typ; funktionen i sig innehåller också en logik för att ansluta dem till ett enda värde av samma typ. Den godkända funktionen måste vara associativ, vilket innebär att ordningen för värdeaggregering inte spelar någon roll, dvs. följande villkor bör innehålla:

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

den associativa egenskapen hos en Binaryoperatoroperatörsfunktion gör att vi enkelt kan parallellisera reduktionsprocessen.naturligtvis finns det också specialiseringar av UnaryOperator och BinaryOperator som kan användas med primitiva värden, nämligen DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator och LongBinaryOperator.

äldre funktionella gränssnitt

inte alla funktionella gränssnitt dök upp i Java 8. Många gränssnitt från tidigare versioner av Java överensstämmer med begränsningarna för ett funktionellt gränssnitt, och vi kan använda dem som lambdas. Framträdande exempel inkluderar de körbara och Kallbara gränssnitten som används i samtidiga API: er. I Java 8 är dessa gränssnitt också markerade med en @ FunctionalInterface-anteckning. Detta gör det möjligt för oss att kraftigt förenkla samtidighetskoden:

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

slutsats

Related Posts

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *