1.4-Software-Development-Principles

Lambda expressies deel 2

Leerdoelen

Voorwaarde: Om dit concept goed te kunnen begrijpen moet je bekend zijn met interfaces en lambda expressies in Java. Je moet ook weten hoe generics gebruikt worden in code.

Inleiding

Ervan uitgaande dat je je enigszins comfortabe voelt met lambda expressies en streams, is de volgende stap je code zo op te zetten zodat andere ontwikkelaars je code makkelijk kunnen gebruiken op een flexibele manier, met hun eigen lambda expressies.

@FunctionalInterface

Dit is een voorbeeld van een functionele interface. Een dergelijke interface heeft slechts één methode.

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

Om aan te geven dat deze interface bedoeld is om gedrag door te geven door het gebruik van lambda expressies, voegen we de @FunctionalInterface annotation toe. Dit zorgt ervoor dat de compiler gaat klagen als iemand het een goed idee vindt om een extra methode toe te voegen aan de interface.

Het toevoegen van de annotatie is niet verplicht, maar het is een goed gebruik om je bedoelingen kenbaar te maken. Dat is met @Override eigenlijk hetzelfde.

Veelgebruikte functional interfaces

De Java library definieert een lange lijst van functional interfaces. Hieronder worden er een aantal besproken.

Predicate

“Vertel me alsjeblieft of ‘X’ wordt geaccepteerd.”

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Deze interface neemt een object van type T en stelt dan vast of dit object ‘geaccepteerd’ wordt. Je zou bijvoorbeeld alleen groene appels die meer dan 200 gram wegen kunnen accepteren:

public class AppleSelector implements Predicate<Apple> {
    @Override
    boolean test(Apple t) {
        return t.getColor().equals("green") && t.getWeight()>200;
    }
}

Of, in de vorm van een lambda expression:

List<Apple> selection = 
    apples
        .stream().
        .filter(a -> a.getColor().equals("green") && a.getWeight()>200)
        .collect(Collectors.toList());

Deze interface is ook handig als je je eigen functie wilt kunnen opgeven:

public static <T> List<T> selector(List<T> original, Predicate<T> p) {
    return original.stream().filter(p).collect(Collectors.toList());
}

Consumer

“Accepteer ‘X’ en doe er vervolgens iets mee.”

@FunctionalInterface
public interface Consumer<T> {
	void accept(T t);
}

Een Consumer neemt een object en doet er iets mee, zonder dat daarvan een resultaat wordt teruggegeven.

Je kunt dit bijvoorbeeld gebruiken om alle objecten te printen, of om een methode op al die objecten aan te roepen:

public static <T> void processList(List<T> list, Consumer<T> consumer) {
    for (T t:list) {
        consumer.accept(t);
    }
}

public static void example() {
    ArrayList<Apple> apples = readApples();
    processList(apples, System.out::println);
}

Zoals je ziet is println(Object o) een consumer functie.

Function

“Converteer ‘X’ naar ‘Y’”

Deze interface functioneert als een mapping van het ene type naar het andere.

@FunctionalInterface
public interface Function<T, R> {
	R apply(T t);
}

Een simpel voorbeeld is het omzetten van een lijst met appels in een lijst met het gewicht van die appels:

public static <T,R> void processList(List<T> list, Function<T,R> function) {
    List<R> result = new ArrayList<>();
    for (T t:list) {
        R converted = function.apply(t):
        result.add(converted);
    }
    return result;
}

public static <T,R> void processStream(List<T> list, Function<T,R> function) {
    return original.stream().map(function).collect(Collectors.toList());
}

Je zou een dergelijke functie bijvoorbeeld kunnen gebruiken om je studentenadministratie te voorzien van een lijst van studentnummers om daarmee de daadwerkelijke Student objecten op te vragen.

Supplier

“Geef me ‘X’”

Elke keer wanneer je de get() method van de supplier aanroept, geeft die je een object van het gevraagde type.

@FunctionalInterface
public interface Supplier<T> {
	T get();
}

Je zou dit kunnen gebrruiken om door een lijst van records of door een tabel te gaan, of om elke keer een nieuwe random waarde te genereren, et cetera.

Het voornaamste idee hier is dat je niet de hele lijst die gebruikt wordt op hoeft te slaan in het geheugen. Alleen wanneer de methode wordt aangeroepen wordt er een nieuw element in het geheugen geladen.

Primitieve specialisaties

Bij het gebruiken van interfaces zoals Function<T,R> moeten beide type referentietypen zijn.

Er zijn ook alternatieven voor primitieve types beschikbaar: IntFunction<T> geeft een primitieve int wanneer deze gebruikt wordt met type parameter T.

Overzicht

img.png

Afkomstig uit ‘Java 8 in Action’, ISBN: 9781617291999