Lambda-expressies en functionele interfaces: tips en best practices

1. Overzicht

Nu Java 8 op grote schaal wordt gebruikt, zijn er patronen en best practices ontstaan ​​voor enkele van de headliner-functies. In deze tutorial gaan we dieper in op functionele interfaces en lambda-expressies.

2. Geef de voorkeur aan standaard functionele interfaces

Functionele interfaces, die zijn verzameld in het java.util.function pakket, voldoen aan de behoeften van de meeste ontwikkelaars bij het leveren van doeltypes voor lambda-expressies en methodereferenties. Elk van deze interfaces is algemeen en abstract, waardoor ze gemakkelijk aan bijna elke lambda-uitdrukking kunnen worden aangepast. Ontwikkelaars moeten dit pakket verkennen voordat ze nieuwe functionele interfaces maken.

Overweeg een interface Foo:

@FunctionalInterface openbare interface Foo {String-methode (String-string); }

en een methode toevoegen() in een bepaalde klas UseFoo, die deze interface als parameter gebruikt:

public String add (String string, Foo foo) {return foo.method (string); }

Om het uit te voeren, zou je schrijven:

Foo foo = parameter -> parameter + "van lambda"; String resultaat = useFoo.add ("Bericht", foo);

Kijk dichterbij en je zult dat zien Foo is niets meer dan een functie die één argument accepteert en een resultaat oplevert. Java 8 biedt al zo'n interface in Functie uit het pakket java.util.function.

Nu kunnen we de interface verwijderen Foo volledig en verander onze code in:

public String add (String string, Function fn) {return fn.apply (string); }

Om dit uit te voeren, kunnen we schrijven:

Functie fn = parameter -> parameter + "van lambda"; String resultaat = useFoo.add ("Bericht", fn);

3. Gebruik de @FunctioneleInterface Annotatie

Annoteer uw functionele interfaces met @FunctionalInterface. In eerste instantie lijkt deze annotatie nutteloos. Zelfs zonder dit wordt uw interface als functioneel behandeld zolang deze maar één abstracte methode heeft.

Maar stel je een groot project voor met meerdere interfaces - het is moeilijk om alles handmatig te besturen. Een interface die ontworpen is om functioneel te zijn, kan per ongeluk worden gewijzigd door andere abstracte methoden / methoden toe te voegen, waardoor deze onbruikbaar wordt als functionele interface.

Maar met behulp van de @FunctioneleInterface annotatie, zal de compiler een fout veroorzaken als reactie op elke poging om de vooraf gedefinieerde structuur van een functionele interface te doorbreken. Het is ook een erg handig hulpmiddel om uw applicatiearchitectuur begrijpelijker te maken voor andere ontwikkelaars.

Gebruik dit dus:

@FunctionalInterface openbare interface Foo {String method (); }

in plaats van alleen:

openbare interface Foo {String-methode (); }

4. Gebruik niet te veel standaardmethoden in functionele interfaces

We kunnen eenvoudig standaardmethoden toevoegen aan de functionele interface. Dit is acceptabel voor het functionele interfacecontract, zolang er maar één abstracte methodeverklaring is:

@FunctionalInterface openbare interface Foo {String-methode (String-string); default void defaultMethod () {}}

Functionele interfaces kunnen worden uitgebreid met andere functionele interfaces als hun abstracte methoden dezelfde signatuur hebben.

Bijvoorbeeld:

@FunctionalInterface openbare interface FooExtended breidt Baz uit, Bar {} @FunctionalInterface openbare interface Baz {String-methode (String-string); default String defaultBaz () {}} @FunctionalInterface openbare interface Bar {String-methode (String-string); default String defaultBar () {}}

Net als bij reguliere interfaces, het uitbreiden van verschillende functionele interfaces met dezelfde standaardmethode kan problematisch zijn.

Laten we bijvoorbeeld het defaultCommon () methode naar de Bar en Baz interfaces:

@FunctionalInterface openbare interface Baz {String-methode (String-string); default String defaultBaz () {} default String defaultCommon () {}} @FunctionalInterface openbare interface Bar {String-methode (String-string); default String defaultBar () {} standaard String defaultCommon () {}}

In dit geval krijgen we een compilatietijdfout:

interface FooExtended erft niet-gerelateerde standaardwaarden voor defaultCommon () van de types Baz en Bar ...

Om dit op te lossen, defaultCommon () methode moet worden overschreven in de FooExtended koppel. Uiteraard kunnen wij voor een maatwerk implementatie van deze methode zorgen. Echter, we kunnen de implementatie ook hergebruiken vanuit de bovenliggende interface:

@FunctionalInterface openbare interface FooExtended breidt Baz uit, Bar {@Override default String defaultCommon () {return Bar.super.defaultCommon (); }}

Maar we moeten voorzichtig zijn. Het toevoegen van te veel standaardmethoden aan de interface is geen erg goede architecturale beslissing. Dit moet worden beschouwd als een compromis, alleen te gebruiken wanneer dat nodig is, voor het upgraden van bestaande interfaces zonder de achterwaartse compatibiliteit te verbreken.

5. Instantiëren van functionele interfaces met Lambda-expressies

Met de compiler kunt u een innerlijke klasse gebruiken om een ​​functionele interface te instantiëren. Dit kan echter leiden tot zeer uitgebreide code. U zou de voorkeur moeten geven aan lambda-uitdrukkingen:

Foo foo = parameter -> parameter + "van Foo";

over een innerlijke klasse:

Foo fooByIC = nieuwe Foo () {@Override public String method (String string) {return string + "from Foo"; }}; 

De lambda-expressiebenadering kan worden gebruikt voor elke geschikte interface uit oude bibliotheken. Het is bruikbaar voor interfaces zoals Runnable, Comparator, enzovoorts. Dit betekent niet dat je je hele oudere codebase moet herzien en alles moet veranderen.

6. Voorkom overbelastingsmethoden met functionele interfaces als parameters

Gebruik methoden met verschillende namen om botsingen te voorkomen; laten we naar een voorbeeld kijken:

openbare interface Processor {String-proces (opvraagbaar c) genereert uitzondering; Stringproces (leverancier en); } public class ProcessorImpl implementeert Processor {@Override public String process (Callable c) genereert uitzondering {// implementatiedetails} @Override public String process (leveranciers) {// implementatiedetails}}

Op het eerste gezicht lijkt dit redelijk. Maar elke poging om een ​​van de ProcessorImpl‘S methoden:

String resultaat = processor.process (() -> "abc");

eindigt met een foutmelding met de volgende melding:

verwijzing naar het proces is dubbelzinnig, zowel het methodeproces (java.util.concurrent.Callable) in com.baeldung.java8.lambda.tips.ProcessorImpl als het methodeproces (java.util.function.Supplier) in com.baeldung.java8.lambda. tips.ProcessorImpl wedstrijd

Om dit probleem op te lossen, hebben we twee opties. De eerste is om methoden met verschillende namen te gebruiken:

String processWithCallable (Callable c) genereert uitzondering; String processWithSupplier (leverancier);

De tweede is om handmatig te casten. Dit heeft niet de voorkeur.

String resultaat = processor.process ((Leverancier) () -> "abc");

7. Behandel Lambda-uitdrukkingen niet als innerlijke klassen

Ondanks ons vorige voorbeeld, waar we in wezen inner class hebben vervangen door een lambda-uitdrukking, zijn de twee concepten op een belangrijke manier verschillend: scope.

Wanneer u een innerlijke klasse gebruikt, wordt er een nieuwe scope gecreëerd. U kunt lokale variabelen voor het omsluitende bereik verbergen door nieuwe lokale variabelen met dezelfde naam te instantiëren. U kunt ook het trefwoord gebruiken dit in je innerlijke klasse als een verwijzing naar zijn instantie.

Lambda-expressies werken echter met een omsluitend bereik. U kunt variabelen niet verbergen voor de omringende reikwijdte in de body van de lambda. In dit geval het trefwoord dit is een verwijzing naar een omsluitende instantie.

Bijvoorbeeld in de klas UseFoo je hebt een instantievariabele waarde:

private String value = "Omsluitende bereikwaarde";

Plaats vervolgens in een of andere methode van deze klasse de volgende code en voer deze methode uit.

public String scopeExperiment () {Foo fooIC = nieuwe Foo () {String value = "Inner class value"; @Override public String-methode (String-string) {retourneer this.value; }}; String resultIC = fooIC.method (""); Foo fooLambda = parameter -> {String value = "Lambda-waarde"; retourneer this.value; }; String resultLambda = fooLambda.method (""); return "Results: resultIC =" + resultIC + ", resultLambda =" + resultLambda; }

Als u het scopeExperiment () methode, krijgt u het volgende resultaat: Resultaten: resultIC = Inner class value, resultLambda = Omsluitende scope-waarde

Zoals u kunt zien, door te bellen deze.waarde in IC hebt u vanuit zijn instantie toegang tot een lokale variabele. Maar in het geval van de lambda, deze.waarde call geeft je toegang tot de variabele waarde die is gedefinieerd in de UseFoo class, maar niet naar de variabele waarde gedefinieerd in het lichaam van de lambda.

8. Houd Lambda-uitdrukkingen kort en spreekt voor zich

Gebruik indien mogelijk constructies van één regel in plaats van een groot blok code. Onthouden lambdas moet eenuitdrukking, geen verhaal. Ondanks de beknopte syntaxis, lambda's moeten de functionaliteit die ze bieden precies weergeven.

Dit is voornamelijk stilistisch advies, aangezien de prestaties niet drastisch zullen veranderen. Over het algemeen is het echter veel gemakkelijker om dergelijke code te begrijpen en ermee te werken.

Dit kan op veel manieren worden bereikt - laten we het eens nader bekijken.

8.1. Vermijd codeblokken in Lambda's lichaam

In een ideale situatie zouden lambda's in één regel code moeten worden geschreven. Met deze benadering is de lambda een vanzelfsprekende constructie, die aangeeft welke actie moet worden uitgevoerd met welke gegevens (in het geval van lambda's met parameters).

Als je een groot blok code hebt, is de functionaliteit van de lambda niet meteen duidelijk.

Met dit in gedachten doet u het volgende:

Foo foo = parameter -> buildString (parameter);
private String buildString (String-parameter) {String-resultaat = "Something" + parameter; // veel regels code retourneren het resultaat; }

in plaats van:

Foo foo = parameter -> {String resultaat = "Something" + parameter; // veel regels code retourneren het resultaat; };

Gebruik deze 'one-line lambda'-regel echter niet als dogma. Als je twee of drie regels in lambda's definitie hebt, is het misschien niet waardevol om die code in een andere methode te extraheren.

8.2. Vermijd het specificeren van parametertypen

Een compiler kan in de meeste gevallen het type lambda-parameters oplossen met behulp van type gevolgtrekking. Daarom is het toevoegen van een type aan de parameters optioneel en kan het worden weggelaten.

Doe dit:

(a, b) -> a.toLowerCase () + b.toLowerCase ();

in plaats van dit:

(String a, String b) -> a.toLowerCase () + b.toLowerCase ();

8.3. Vermijd haakjes rond een enkele parameter

Lambda-syntaxis vereist alleen haakjes rond meer dan één parameter of wanneer er helemaal geen parameter is. Daarom is het veilig om uw code een beetje korter te maken en haakjes uit te sluiten als er maar één parameter is.

Dus doe dit:

een -> a.toLowerCase ();

in plaats van dit:

(a) -> a.toLowerCase ();

8,4. Vermijd retourverklaring en accolades

Een beugel en terugkeer statements zijn optioneel in lambda-lichamen van één regel. Dit betekent dat ze kunnen worden weggelaten voor duidelijkheid en beknoptheid.

Doe dit:

een -> a.toLowerCase ();

in plaats van dit:

a -> {retourneer a.toLowerCase ()};

8.5. Gebruik methodeverwijzingen

Heel vaak, zelfs in onze vorige voorbeelden, roepen lambda-expressies alleen methoden aan die al elders zijn geïmplementeerd. In deze situatie is het erg handig om een ​​andere Java 8-functie te gebruiken: methode referenties.

Dus de lambda-uitdrukking:

een -> a.toLowerCase ();

kan worden vervangen door:

String :: toLowerCase;

Dit is niet altijd korter, maar het maakt de code beter leesbaar.

9. Gebruik "Effectief definitieve" variabelen

Toegang tot een niet-definitieve variabele binnen lambda-expressies zal de compilatietijdfout veroorzaken. Maar het betekent niet dat u elke doelvariabele moet markeren als laatste.

Volgens de "effectief definitief”, Behandelt een compiler elke variabele als laatste, zolang het maar één keer wordt toegewezen.

Het is veilig om dergelijke variabelen binnen lambdas te gebruiken, omdat de compiler hun status controleert en een compileerfout veroorzaakt onmiddellijk na elke poging om ze te wijzigen.

De volgende code zal bijvoorbeeld niet compileren:

public void method () {String localVariable = "Lokaal"; Foo foo = parameter -> {String localVariable = parameter; retourneer localVariable; }; }

De compiler zal u informeren dat:

Variabele 'localVariable' is al gedefinieerd in het bereik.

Deze benadering zou het proces van het thread-safe maken van lambda-uitvoering moeten vereenvoudigen.

10. Bescherm objectvariabelen tegen mutatie

Een van de belangrijkste doelen van lambda's is het gebruik in parallel computing - wat betekent dat ze erg handig zijn als het gaat om thread-veiligheid.

Het "effectief laatste" paradigma helpt hier veel, maar niet in alle gevallen. Lambdas kan een waarde van een object niet wijzigen in een bereik. Maar in het geval van veranderlijke objectvariabelen, zou een toestand binnen lambda-expressies kunnen worden gewijzigd.

Beschouw de volgende code:

int [] totaal = nieuwe int [1]; Runnable r = () -> totaal [0] ++; r.run ();

Deze code is legaal, zoals totaal variabele blijft "effectief definitief". Maar zal het object waarnaar het verwijst dezelfde toestand hebben na uitvoering van de lambda? Nee!

Bewaar dit voorbeeld als herinnering om code te vermijden die onverwachte mutaties kan veroorzaken.

11. Conclusie

In deze tutorial hebben we enkele best practices en valkuilen gezien in de lambda-expressies en functionele interfaces van Java 8. Ondanks het nut en de kracht van deze nieuwe functies, zijn het slechts hulpmiddelen. Elke ontwikkelaar moet opletten tijdens het gebruik ervan.

Het complete broncode want het voorbeeld is beschikbaar in dit GitHub-project - dit is een Maven- en Eclipse-project, dus het kan worden geïmporteerd en gebruikt zoals het is.