- Inleiding
- verder lezen:
- Iterable to Stream in Java
- hoe if / else logica te gebruiken in Java 8 Streams
- lambda ‘ s in Java 8
- functionele Interfaces
- functies
- primitieve Functiespecialisaties
- twee-Arity Functiespecialisaties
- leveranciers
- consumenten
- predicaten
- Operators
- oudere functionele Interfaces
- conclusie
Inleiding
Deze tutorial is een gids voor verschillende functionele interfaces aanwezig in Java 8, evenals hun algemene use cases, en het gebruik in de standaard JDK bibliotheek.
verder lezen:
Iterable to Stream in Java
hoe if / else logica te gebruiken in Java 8 Streams
lambda ‘ s in Java 8
Java 8 bracht een krachtige nieuwe syntactische verbetering in de vorm van lambda-expressies. Een lambda is een anonieme functie die we als eersteklas taalburger aankunnen. We kunnen het bijvoorbeeld doorgeven aan of retourneren van een methode.
voor Java 8, zouden we meestal een klasse maken voor elk geval waarin we een enkel stuk functionaliteit moesten inkapselen. Dit impliceerde een hoop onnodige standaardcode om iets te definiëren dat diende als een primitieve functie representatie.
Het artikel “Lambda Expressions and Functional Interfaces: Tips and Best Practices” beschrijft in meer detail de functionele interfaces en best practices van het werken met lambda ‘ s. Deze gids richt zich op een aantal specifieke functionele interfaces die aanwezig zijn in de java.util.functiepakket.
functionele Interfaces
Het wordt aanbevolen dat alle functionele interfaces een informatieve @FunctionalInterface-annotatie hebben. Dit communiceert duidelijk het doel van de interface, en staat ook een compiler toe om een fout te genereren als de geannoteerde interface niet aan de voorwaarden voldoet.
elke interface met een SAM (Single Abstract Method) is een functionele interface, en de implementatie ervan kan worden behandeld als lambda-expressies.
merk op dat de standaardmethoden van Java 8 niet abstract zijn en niet tellen; een functionele interface kan nog steeds meerdere standaardmethoden hebben. We kunnen dit waarnemen door te kijken naar de documentatie van de functie.
functies
het meest eenvoudige en algemene geval van een lambda is een functionele interface met een methode die een waarde ontvangt en een andere retourneert. Deze functie van een enkel argument wordt vertegenwoordigd door de functie-interface, die wordt geparametreerd door de typen van het argument en een retourwaarde:
public interface Function<T, R> { … }
een van de toepassingen van het functietype in de standaardbibliotheek is de Map.computeIfAbsent methode. Deze methode retourneert een waarde van een kaart per sleutel, maar berekent een waarde als een sleutel nog niet aanwezig is in een kaart. Om een waarde te berekenen, gebruikt het de doorgegeven Functieimplementatie:
Map<String, Integer> nameMap = new HashMap<>();Integer value = nameMap.computeIfAbsent("John", s -> s.length());
In dit geval berekenen we een waarde door een functie toe te passen op een sleutel, in een kaart te plaatsen en ook terug te keren van een methodeaanroep. We kunnen de lambda vervangen door een methode referentie die overeenkomt met doorgegeven en geretourneerde waarde types.
onthoud dat een object waarop we de methode aanroepen in feite het impliciete eerste argument van een methode is. Dit stelt ons in staat om een instantie methode lengte verwijzing naar een functie-interface cast:
Integer value = nameMap.computeIfAbsent("John", String::length);
De functie-interface heeft ook een standaard composeermethode die ons in staat stelt om meerdere functies in één te combineren en ze achtereenvolgens uit te voeren:
Function<Integer, String> intToString = Object::toString;Function<String, String> quote = s -> "'" + s + "'";Function<Integer, String> quoteIntToString = quote.compose(intToString);assertEquals("'5'", quoteIntToString.apply(5));
de quoteintostring-functie is een combinatie van de quote-functie die wordt toegepast op een resultaat van de intToString-functie.
primitieve Functiespecialisaties
omdat een primitief type geen generiek typeargument kan zijn, zijn er versies van de functie-interface voor de meest gebruikte primitieve types double, int, long, en hun combinaties in argument en return types:
- 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: met beide argument en return-type gedefinieerd als primitieve typen, zoals gespecificeerd door hun namen
Als een voorbeeld, er is geen out-of-the-box functionele interface voor een functie waarvoor een korte en geeft een byte, maar niets weerhoudt ons van het schrijven van onze eigen:
@FunctionalInterfacepublic interface ShortToByteFunction { byte applyAsByte(short s);}
Nu kunnen we schrijven een methode die transformeert een reeks van korte naar een array van bytes met behulp van een regel worden gedefinieerd door een 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;}
Hier is hoe we kunnen gebruiken om te transformeren van een reeks van korte broek naar een array van bytes vermenigvuldigd met 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);
twee-Arity Functiespecialisaties
om lambda ’s met twee argumenten te definiëren, moeten we extra interfaces gebruiken die” Bi ” sleutelwoord in hun naam bevatten: BiFunction, ToDoubleBiFunction, ToIntBiFunction, en ToLongBiFunction.
BiFunction heeft beide argumenten en een geretourneerd type, terwijl ToDoubleBiFunction en anderen ons toestaan om een primitieve waarde te retourneren.
een van de typische voorbeelden van het gebruik van deze interface in de standaard API is in de kaart.replaceAll methode, die het mogelijk maakt om alle waarden in een kaart te vervangen door een bepaalde berekende waarde.
laten we een bifunctie implementatie gebruiken die een sleutel en een oude waarde ontvangt om een nieuwe waarde voor het salaris te berekenen en terug te geven.
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);
leveranciers
De functionele interface van de leverancier is een andere Functiespecialisatie die geen argumenten aanneemt. We gebruiken het meestal voor luie generatie van waarden. Laten we bijvoorbeeld een functie definiëren die een dubbele waarde kwadrateert. Het zal zelf geen waarde ontvangen, maar een leverancier van deze waarde:
public double squareLazy(Supplier<Double> lazyValue) { return Math.pow(lazyValue.get(), 2);}
Dit stelt ons in staat om het argument voor aanroep van deze functie te genereren met behulp van een leverancier implementatie. Dit kan nuttig zijn als het genereren van het argument een aanzienlijke hoeveelheid tijd in beslag neemt. We zullen simuleren dat met behulp van Guava ‘ s sleepUninterruptibly methode:
Supplier<Double> lazyValue = () -> { Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS); return 9d;};Double valueSquared = squareLazy(lazyValue);
een ander gebruik voor de leverancier is het definiëren van logica voor het genereren van sequenties. Laten we een statische stroom gebruiken om het te demonstreren.genereer methode om een stroom van Fibonacci-getallen aan te maken:
int fibs = {0, 1};Stream<Integer> fibonacci = Stream.generate(() -> { int result = fibs; int fib3 = fibs + fibs; fibs = fibs; fibs = fib3; return result;});
De functie die we aan de stroom doorgeven.genereer methode implementeert de leverancier functionele interface. Merk op dat om nuttig te zijn als een generator, de leverancier meestal een soort van externe staat nodig. In dit geval omvat de staat de laatste twee Fibonacci-volgnummers.
om deze toestand te implementeren, gebruiken we een array in plaats van een paar variabelen omdat alle externe variabelen die binnen de lambda worden gebruikt effectief definitief moeten zijn.
andere specialisaties van de functionele interface voor leveranciers zijn Boolean Supplier, Double Supplier, Long Supplier en Int Supplier, waarvan de retourtypen overeenkomstige primitieven zijn.
consumenten
In tegenstelling tot de leverancier accepteert de consument een generiek argument en geeft hij niets terug. Het is een functie die bijwerkingen vertegenwoordigt.
laten we bijvoorbeeld iedereen in een lijst met namen begroeten door de begroeting in de console af te drukken. De lambda ging naar de lijst.forEach method implementeert de functionele interface voor consumenten:
List<String> names = Arrays.asList("John", "Freddy", "Samuel");names.forEach(name -> System.out.println("Hello, " + name));
Er zijn ook gespecialiseerde versies van de consument — DoubleConsumer, IntConsumer en LongConsumer — die primitieve waarden als argumenten ontvangen. Interessanter is de biconsumer interface. Een van de use cases is itereren door de items van een map:
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"));
een andere set van gespecialiseerde Tweepersoonsversies bestaat uit ObjDoubleConsumer, ObjIntConsumer en ObjLongConsumer, die twee argumenten ontvangen; een van de argumenten wordt gegenereerd, en de andere is een primitief type.
predicaten
in de wiskundige logica is een predicaat een functie die een waarde ontvangt en een Booleaanse waarde retourneert.
De predicaat functionele interface is een specialisatie van een functie die een generified waarde ontvangt en een boolean retourneert. Een typisch gebruik van het predicaat lambda is om een verzameling waarden te filteren:
List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");List<String> namesWithA = names.stream() .filter(name -> name.startsWith("A")) .collect(Collectors.toList());
in de bovenstaande code filteren we een lijst met behulp van de Stream API en houden alleen de namen die beginnen met de letter”A”. De implementatie van predicaten omvat de Filterlogica.
zoals in alle voorgaande voorbeelden zijn er IntPredicate, DoublePredicate en LongPredicate versies van deze functie die primitieve waarden ontvangen.
Operators
Operatorinterfaces zijn speciale gevallen van een functie die hetzelfde waardetype ontvangen en retourneren. De unaryoperator interface krijgt een enkel argument. Een van de use cases in de Collections API is om alle waarden in een lijst te vervangen door een aantal berekende waarden van hetzelfde type:
List<String> names = Arrays.asList("bob", "josh", "megan");names.replaceAll(name -> name.toUpperCase());
De lijst.replaceAll functie retourneert void als het vervangt de waarden op zijn plaats. Om aan het doel te voldoen, moet de lambda die wordt gebruikt om de waarden van een lijst te transformeren, hetzelfde resultaattype retourneren als het ontvangt. Daarom is de UnaryOperator hier nuttig.
natuurlijk, in plaats van naam – > naam.toupercase (), we kunnen gewoon een methode referentie gebruiken:
names.replaceAll(String::toUpperCase);
een van de interessantste use cases van een BinaryOperator is een reductiebewerking. Stel dat we een verzameling gehele getallen willen samenvoegen in een som van alle waarden. Met Stream API kunnen we dit doen met behulp van een collector, maar een meer algemene manier om dit te doen zou zijn om de reduce methode te gebruiken:
List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);int sum = values.stream() .reduce(0, (i1, i2) -> i1 + i2);
De reduce methode ontvangt een initiële accumulator waarde en een BinaryOperator functie. De argumenten van deze functie zijn een paar waarden van hetzelfde type; de functie zelf bevat ook een logica voor het samenvoegen van hen in een enkele waarde van hetzelfde type. De doorgegeven functie moet associatief zijn, wat betekent dat de volgorde van waardeaggregatie er niet toe doet, dat wil zeggen dat de volgende voorwaarde moet gelden:
op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
de associatieve eigenschap van een BinaryOperator operator functie stelt ons in staat om het reductieproces gemakkelijk te parallelliseren.
natuurlijk zijn er ook specialisaties van UnaryOperator en BinaryOperator die kunnen worden gebruikt met primitieve waarden, namelijk DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator en LongBinaryOperator.
oudere functionele Interfaces
niet alle functionele interfaces verschenen in Java 8. Veel interfaces van eerdere versies van Java voldoen aan de beperkingen van een FunctionalInterface, en we kunnen ze gebruiken als lambda ‘ s. Prominente voorbeelden zijn de Runnable en Callable interfaces die worden gebruikt in concurrency API ‘ s. In Java 8 zijn deze interfaces ook gemarkeerd met een @ FunctionalInterface annotatie. Dit staat ons toe om concurrency code sterk te vereenvoudigen:
Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));thread.start();