1.4-Software-Development-Principles

Lambda expressies deel 1

Leerdoelen

Vereisten: Om dit concept te begrijpen dien je comfortabel te zijn met het idee van Interfaces in Java:

public interface Printer {
	void print(String text);
}

Normaal gebruik van een een interface to zover

Wanneer de bovengenoemde Printer interface wordt geimplementeerd, kan de print methode bepalen wat het moet doen met de geleverde attribuut.

Bijvoorbeeld, print naar de SaxionApp. Later kan ik er misschien voor kiezen om te printen op een andere manier zonder dat ik de rest van de code hoef aan te passen.

public class MyPrinter implements Printer {
	@Override
    public void print(String text) {
		SaxionApp.printLine(text);
    }
}

Nu kan ik de instance van deze klasse een andere methode geven om iets te printen.

public class Main {
	public static void main(String[] args) {
		Printer printer = new MyPrinter();
		doSomethingComplicated(printer);
	}
	
	private doSomethingComplicated(Printer printer) {
		// In deze methode gebruiken we printer.print("message") 
		// om berichten weer te geven.
    }
}

Gedrag doorgeven (Passing behaviour)

Nu kunnen we er voor kiezen om doSomethingComplicated to voorzien van een willekeurige instantie, zolang de methode print(String text) is geimplementeerd:

public class SilentPrinter implements Printer {
	@Override
	public void print(String text) {
		// Doe helemaal niks.
	}	
}

public class Main {
	public static void main(String[] args) {
		Printer printer = new SilentPrinter();
		doSomethingComplicated(printer);
	}
}

Door eenvoudigweg een andere klasse aan te biende die de Printer interface te gebruikt in dit tweede voorbeeld, zal dezelfde functie doSomthingComplicated niks printen.

Dit maakt de doSomethingComplicated method heel erg flexibel. Door een andere Printer aan te leveren kan de doSomethingComplicated method naar een bestand schrijven, database, System.out, of niks doen.

Deze techniek heet code doorgeven (passing code) of gedrag doorgeven (passing behaviour).

Anonieme Klasse (Anonymous class)

In het vorige voorbeeld hadden we een hele klasse gemaakt, waarschijnlijk in een eigen bestand:

public class MyPrinter implements Printer {
	@Override
	public void print(String text) {
		/* You choose */
	}	
}

Maar dit is niet altijd nodig. Je kunt het volgende doen:

public class Main {
	public static void main(String[] args) {
		Printer p = new Printer() {
		    @Override
            public void print(String text) {
				System.out.println(text);
            }
        };
		
		p.print("Look mom! No actual class!");
		doSomethingComplicated(p);
    }
}

Deze code definieert niet een klasse met een eigen naam maar plaatst de implementatie gelijk achter de new Printer() statement.

De code boven p is geinstantieerd als een anonieme klasse. (anonymous class)

Voor anonieme klassen geld het volgende:

Lambda expressies

Wanneer een interface een enkele methode definieerd, dan heb je de mogelijkheid om de code nog korter te maken.

// Anonymous class
Printer p = new Printer() {
    @Override
    public void print(String text) {
        System.out.println(text);
    }
};
p.print("This works!");

kan verandert worden in

// Lambda expression
Printer p = (text) -> { 
    System.out.println(text); 
};
p.print("This STILL works!");

Wacht! Wat!?

Herinner de inteface definitie:

@FunctionalInterface
public interface Printer {
	void print(String text); // One single method!!!
}

Omdat de Printer interface alleen een enkele methode definieerd, weet de compiler al welke functie je wil gaan implementeren. Dit heet een functional interface. Je kunt zelfs de annotatie @FunctionalInterface toevoegen om aan te geven dat het jouw intentie is dat deze gebruikt moet worden als lambda expressie.

(Al kun je nog steeds klasses maken welke deze interfaces implementeren als je dat echt wil.)

Nu wordt het patroon:

Printer p = (text) -> { /* Doe iets met de text. */ };

De compiler weet ook het type en het aantal argumenten van deze methode. Dus (text) wordt gebruikt als enkele String argument van de methode.

Tussen de accolades { /* hier */} kun je nu elke willekeurie instructie plaatsen die je nodig hebt.

Een enkele regel code.

Wanneer je implementatie gebruik maakt van een enkele regel code, dan kun je zelfs de accolades weglaten en de instructie direct typen:

// Lambda expressie zonder accolades.
Printer p = (t) ->  System.out.println(t); 
p.print("Dit werkt nog STEEDS!");

Variabelenamen met een enkele letter zijn Heel Slecht, maar dit toont aan dat je elke variabele naam kunt gebruiken om iets door te geven aan de lambda expressie. In dit geval q in plaats van test;

Maar een parameter?

Gooi de haakjes weg:

Printer p = t ->  System.out.println(t); 
p.print("Dit werkt nog STEEDS!");

Hier zijn de haakjes verwijdert rondom het argument t.

Waarom zouden we daar stoppen?

De uiteindelijke vorm van een lambda expressie ziet er als volgt uit:

Printer p = System.out::println; 
p.print("Dit werkt nog STEEDS!");

Deze code zegt effectief: De println methode in System.out heeft dezelfde definitie als de print methode gedefinieerd in de Printer interface, dus gebruik die methode.

Je kunt ook refereren naar statische methode ClassName :: methodName:

De :: operator wordt ook wel de methode referentie genoemd.

Printer p = MyClass::myStaticPrintMethod;
p.print("Dit werkt met statische methodes in een statische class.");

Bedenk hoe ongelogelijk krachtig dit is: Je kunt eenvoudigweg de refereren naar een methode als een parameter in je eigen code, zodat jouw code het gedrag veranderd afhankelijk van de methode die wordt doorgegeven. Dit heet *gedrag doorgeven (passing behaviour).

Meer informatie

Voor meer informattie hierover: We waren geinspireerd door dit boek: “Java 8 in Action” (ISBN: 9781617291999)

Anonieme klassen vs. Lambda expressies

Een belangrijk verschil tussen lambda expressies en anonieme klassen.

Bestudeer de volgende code:

// Anonymous class
Printer p = new Printer() {
    private int nrOfPrints = 0;

    @Override
    public void print(String text) {
        nrOfPrints++;
        System.out.println("[" + nrOfPrints + "] " + text);
    }
};
p.print("Test");

De anonieme implementatie van Printer slaat een attribuut op dat elke regel telt. Elke keer wanneer de print methode wordt aangeroepen wordt zal de interne staat van de class bijgewerkt worden.

Er is geen manier om dit te doen met lambda expressies!