Functionele interfaces in Java 8

1. Inleiding

Dit artikel is een gids voor verschillende functionele interfaces die aanwezig zijn in Java 8, hun algemene gebruiksscenario's en gebruik in de standaard JDK-bibliotheek.

2. 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 kan worden afgehandeld als een eersteklas taalburger, bijvoorbeeld doorgegeven aan of geretourneerd door een methode.

Vóór Java 8 maakte u gewoonlijk een klasse voor elk geval waarin u een enkel stuk functionaliteit moest inkapselen. Dit impliceerde veel onnodige standaardcode om iets te definiëren dat diende als een primitieve functierepresentatie.

Lambda's, functionele interfaces en best practices om ermee te werken, worden in het algemeen beschreven in het artikel "Lambda Expressions and Functional Interfaces: Tips and Best Practices". Deze handleiding richt zich op enkele specifieke functionele interfaces die aanwezig zijn in het java.util.function pakket.

3. Functionele interfaces

Alle functionele interfaces worden aanbevolen om informatief te hebben @FunctioneleInterface annotatie. Dit communiceert niet alleen duidelijk het doel van deze interface, maar stelt een compiler ook in staat 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 Java 8's standaard methoden zijn dat niet abstract en tellen niet mee: een functionele interface kan er nog meerdere hebben standaard methoden. U kunt dit zien door naar de Functie's documentatie.

4. Functies

Het meest eenvoudige en algemene geval van een lambda is een functionele interface met een methode die de ene waarde ontvangt en een andere retourneert. Deze functie van een enkel argument wordt vertegenwoordigd door de Functie interface die wordt geparametriseerd door de typen van zijn argument en een retourwaarde:

openbare interface Functie {…}

Een van de toepassingen van de Functie type in de standaardbibliotheek is de Map.computeIfAbsent methode die een waarde retourneert van een kaart op sleutel, maar een waarde berekent als een sleutel niet al aanwezig is in een kaart. Om een ​​waarde te berekenen, gebruikt het de doorgegeven functie-implementatie:

Map nameMap = nieuwe HashMap (); Geheel getal waarde = nameMap.computeIfAbsent ("John", s -> s.length ());

In dit geval wordt een waarde berekend door een functie op een sleutel toe te passen, in een kaart te plaatsen en ook te retourneren via een methodeaanroep. Trouwens, we kunnen de lambda vervangen door een methodeverwijzing die overeenkomt met doorgegeven en geretourneerde waardetypes.

Onthoud dat een object waarop de methode wordt aangeroepen, in feite het impliciete eerste argument van een methode is, waarmee een instantiemethode kan worden gecast lengte verwijzing naar een Functie koppel:

Geheel getal waarde = nameMap.computeIfAbsent ("John", String :: lengte);

De Functie interface heeft ook een standaard componeren methode die het mogelijk maakt om verschillende functies in één te combineren en ze opeenvolgend uit te voeren:

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

De quoteIntToString functie is een combinatie van de citaat functie toegepast op een resultaat van de intToString functie.

5. Primitieve functie-specialisaties

Omdat een primitief type geen generiek type-argument kan zijn, zijn er versies van de Functie interface voor de meest gebruikte primitieve typen dubbele, int, lang, en hun combinaties in argument- en retourtypen:

  • IntFunctie, Lange functie, Dubbele functie: argumenten zijn van een gespecificeerd type, het retourtype is geparametriseerd
  • ToIntFunction, ToLongFunction, Functie Dubbel: retourtype is van het opgegeven type, argumenten zijn geparametriseerd
  • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction - met zowel het argument als het retourtype gedefinieerd als primitieve typen, zoals gespecificeerd door hun namen

Er is geen kant-en-klare functionele interface voor bijvoorbeeld een functie waarvoor een kort en retourneert een byte, maar niets houdt je tegen om je eigen te schrijven:

@FunctionalInterface openbare interface ShortToByteFunction {byte applyAsByte (korte s); }

Nu kunnen we een methode schrijven die een reeks kort naar een reeks van byte met behulp van een regel gedefinieerd door een ShortToByteFunction:

openbare byte [] transformArray (short [] array, ShortToByteFunction-functie) {byte [] transformedArray = nieuwe byte [array.length]; for (int i = 0; i <array.length; i ++) {transformedArray [i] = function.applyAsByte (array [i]); } return transformedArray; }

Hier is hoe we het kunnen gebruiken om een ​​reeks korte broekjes om te zetten in een reeks bytes vermenigvuldigd met 2:

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

6. Twee-Arity Functiespecialisaties

Om lambda's met twee argumenten te definiëren, moeten we extra interfaces gebruiken die “Bi" trefwoord in hun naam: BiFunction, ToDoubleBiFunction, ToIntBiFunction, en ToLongBiFunction.

BiFunction heeft zowel argumenten als een retourtype gegenereerd, while ToDoubleBiFunction en anderen staan ​​je toe om een ​​primitieve waarde terug te geven.

Een van de typische voorbeelden van het gebruik van deze interface in de standaard-API is in de Map.replaceAll methode, waarmee alle waarden in een kaart kunnen worden vervangen door een bepaalde berekende waarde.

Laten we een BiFunction implementatie die een sleutel en een oude waarde ontvangt om een ​​nieuwe waarde voor het salaris te berekenen en terug te geven.

Kaartsalarissen = nieuwe HashMap (); salarissen.put ("John", 40000); salarissen.put ("Freddy", 30000); salarissen.put ("Samuel", 50000); salarissen.replaceAll ((name, oldValue) -> name.equals ("Freddy")? oldValue: oldValue + 10000);

7. Leveranciers

De Leverancier functionele interface is nog een andere Functie specialisatie die geen argumenten accepteert. Het wordt meestal gebruikt voor het lui genereren van waarden. Laten we bijvoorbeeld een functie definiëren die een vierkant maakt dubbele waarde. Het krijgt zelf geen waarde, maar een Leverancier van deze waarde:

openbare dubbele squareLazy (leverancier lazyValue) {return Math.pow (lazyValue.get (), 2); }

Dit stelt ons in staat om lui het argument voor het aanroepen van deze functie te genereren met behulp van een Leverancier implementatie. Dit kan handig zijn als het genereren van dit argument veel tijd kost. We simuleren dat met Guava's slaap ononderbroken methode:

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

Een andere use case voor de leverancier is het definiëren van een logica voor het genereren van sequenties. Laten we om het te demonstreren een statisch gebruiken Stream. Genereren methode om een Stroom van Fibonacci-nummers:

int [] fibs = {0, 1}; Stream fibonacci = Stream.generate (() -> {int resultaat = fibs [1]; int fib3 = fibs [0] + fibs [1]; fibs [0] = fibs [1]; fibs [1] = fib3; resultaat teruggeven;});

De functie die wordt doorgegeven aan het Stream. Genereren methode implementeert de Leverancier functionele interface. Merk op dat om als generator bruikbaar te zijn, de Leverancier heeft meestal een soort externe toestand nodig. In dit geval bestaat de status uit twee laatste 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, moeten effectief definitief zijn.

Andere specialisaties van Leverancier functionele interface omvatten BooleanSupplier, DoubleSupplier, LongSupplier en IntSupplier, waarvan de retourtypen corresponderende primitieven zijn.

8. Consumenten

In tegenstelling tot de Leverancier, de Klant accepteert een gegenereerd argument en geeft 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. Voor elk methode implementeert de Klant functionele interface:

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

Er zijn ook gespecialiseerde versies van de KlantDoubleConsumer, IntConsumer en Lange Consument - die primitieve waarden ontvangen als argumenten. Interessanter is de BiConsumer koppel. Een van de use-cases is het itereren door de invoer van een kaart:

Kaartleeftijden = nieuwe HashMap (); leeftijden.put ("John", 25); leeftijden.put ("Freddy", 24); leeftijden.put ("Samuel", 30); leeftijden.forEach ((naam, leeftijd) -> System.out.println (naam + "is" + leeftijd + "jaar oud"));

Nog een set gespecialiseerd BiConsumer versies bestaat uit ObjDoubleConsumer, ObjIntConsumer, en ObjLongConsumer die twee argumenten ontvangen waarvan er één wordt gegenereerd, en een ander van een primitief type.

9. Predikaten

In wiskundige logica is een predikaat een functie die een waarde ontvangt en een booleaanse waarde retourneert.

De Predikaat functionele interface is een specialisatie van een Functie die een gegenereerde waarde ontvangt en een booleaanse waarde retourneert. Een typische use case van de Predikaat lambda is om een ​​verzameling waarden te filteren:

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

In de bovenstaande code filteren we een lijst met behulp van de Stroom API en bewaar alleen namen die beginnen met de letter "A". De filterlogica is ingekapseld in de Predikaat implementatie.

Zoals in alle voorgaande voorbeelden zijn er IntPrediceren, DoublePredicate en LongPredicaat versies van deze functie die primitieve waarden ontvangen.

10. Operatoren

Operator interfaces zijn speciale gevallen van een functie die hetzelfde waardetype ontvangen en retourneren. De UnaryOperator interface ontvangt een enkel argument. Een van de use-cases in de Collections API is om alle waarden in een lijst te vervangen door enkele berekende waarden van hetzelfde type:

Lijstnamen = Arrays.asList ("bob", "josh", "megan"); names.replaceAll (naam -> naam.toUpperCase ());

De List.replaceAll functie retourneert leegte, omdat het de bestaande waarden vervangt. 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. Dit is waarom de UnaryOperator is hier handig.

Natuurlijk in plaats van naam -> naam.toUpperCase (), kunt u eenvoudig een methodeverwijzing gebruiken:

names.replaceAll (String :: toUpperCase);

Een van de meest interessante use-cases van een BinaryOperator is een reductieoperatie. Stel dat we een verzameling gehele getallen willen aggregeren in een som van alle waarden. Met Stroom API, we zouden dit kunnen doen met behulp van een verzamelaar, maar een meer algemene manier om dit te doen zou zijn om de verminderen methode:

Lijstwaarden = Arrays.asList (3, 5, 8, 9, 12); int som = waarden. stroom (). verminderen (0, (i1, i2) -> i1 + i2); 

De verminderen methode ontvangt een initiële accumulatorwaarde en een BinaryOperator functie. De argumenten van deze functie zijn een paar waarden van hetzelfde type, en een functie zelf bevat een logica om ze samen te voegen tot een enkele waarde van hetzelfde type. De doorgegeven functie moet associatief zijn, wat betekent dat de volgorde van waardeaggregatie er niet toe doet, d.w.z. dat de volgende voorwaarde moet gelden:

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

De associatieve eigenschap van a BinaryOperator operatorfunctie maakt het mogelijk om het reductieproces gemakkelijk te parallelliseren.

Er zijn natuurlijk ook specialisaties van UnaryOperator en BinaryOperator die kunnen worden gebruikt met primitieve waarden, namelijk DoubleUnaryOperator, IntUnary Operator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator, en LongBinaryOperator.

11. Legacy functionele interfaces

Niet alle functionele interfaces zijn verschenen in Java 8. Veel interfaces uit eerdere versies van Java voldoen aan de beperkingen van een Functionele interface en kunnen als lambda's worden gebruikt. Een prominent voorbeeld is de Runnable en Oproepbaar interfaces die worden gebruikt in gelijktijdige API's. In Java 8 zijn deze interfaces ook gemarkeerd met een @FunctioneleInterface annotatie. Dit stelt ons in staat om gelijktijdigheidscode aanzienlijk te vereenvoudigen:

Thread thread = nieuwe thread (() -> System.out.println ("Hallo van een andere thread")); thread.start ();

12. Conclusie

In dit artikel hebben we verschillende functionele interfaces beschreven die aanwezig zijn in de Java 8 API en die kunnen worden gebruikt als lambda-expressies. De broncode voor het artikel is beschikbaar op GitHub.


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