Java 8 Lambdas und Methoden Referenzen

Als Fortsetzung zum Artikel Java 8 Default Methods, werden in diesem Artikel Lambdas erläutert. Generell sind Lambdas anonyme Methoden bzw. Closures, das heißt Methoden ohne einen Namen, weshalb diese auch nicht in einer Klasse deklariert werden müssen. Dabei sind Lambdas eine Möglichkeit auf einfache Art und Weise Code zu exekutieren.

Ein Weg zu Lambdas…

Um nun einen Einblick in die Funktionsweise von Lambdas zu geben bzw. was hinter diesen steckt, wird das Vehicle Beispiel des vorherigen Artikels Java 8 Default Methoden im folgenden, um eine Implementierung eines Filters erweitert. Man stelle sich dazu folgendes Interface Checkable vor.

public interface Checkable {
    boolean check(final Vehicle vehicle);
}

Dieses Interface soll nun ein Lambda repräsentieren, mit dessen Hilfe Vehicle Objekte in einer entsprechenden Utility Methode gefiltert werden können. Diese Methode könnte zum Beispiel wie folgt implementiert werden.

public final class VehicleUtils {
    // private constructor here
    public static List<Vehicle> filter(Checkable lambda, Collection<Vehicle> c) {
        List<Vehicle> filteredVehicles = new LinkedList<Vehicle>();
        for (Vehicle vehicle : c) {
            if (lambda.check(vehicle)) {
                filteredVehicles.add(vehicle);
            } 
        }
    }
}

Das übergebene Checkable Lambda repräsentiert dabei die Operation nach welcher die Vehicle Objekte gefiltert werden. Diese Implementierung ist nun abhängig vom Anwendungsfall, allerdings wird im folgenden ein Beispiel solch einer Implementierung demonstriert.

final Checkable checker = new Checkable() {
    @Override
    public boolean check(final Vehicle vehicle) {
        return vehicle.gas() >= 4;
    }
}

Mithilfe dieses deklarierten Checkable Lambdas werden nun alle Vehicle Objekte in der filter Methode returniert, deren gas Methode größer-gleich vier returniert. Der Aufruf der filter Methode wäre nun wie folgt.

List<Vehicle> vehicles = new ArrayList<Vehicle>();
// ... populate list ...
vehicles = VehicleUtils.filter(checker, vehicles); // return vehicle.gas() > 4

In Java 8 wäre das Checkable Interface bereits als Lambda verwendbar, welches mittels einer kleinen Anpassung als ein solches geschrieben werden kann. Diese Anpassung beinhaltet das annotieren des Interfaces mit der @FunctionalInterface Annotation, wie im folgenden sichtbar ist.

@FunctionalInterface
public interface Checkable {
    boolean check(final Vehicle vehicle);
}

Somit kann der Aufruf der filter Utility Methode sehr vereinfacht werden. Dafür siehe folgendes Beispiel.

final Checkable checker = vehicle -> vehicle.gas() > 4; // Java 8 Lambda
vehicles = VehicleUtils.filter(checker, vehicles);

Lambdas

Wie bereits weiter oben erwähnt, repräsentieren Lambdas eine anonyme Funktion. In Java 8 ist die Schreibweise eines Lambda Ausdruckes simpel, denn sie folgt folgender Notation.

Arguments -> Expression

Allerdings gibt es hierbei einigen Regeln. In der Argumenten Liste

  • kann der Typ des Argumentes des Lambdas angegeben werden; falls jedoch der Compiler nicht in der Lage ist den Typ aus dem Kontext eindeutig zu bestimmen, ist die Bekanntgabe desselben erforderlich
  • können die Klammern bei einem einzigen Argument weggelassen werden
  • müssen die Klammern bei keinem Argument angegeben werden
  • müssen die Klammern bei mehreren Argumenten angegeben werden

Hingegen sind die Regeln des Ausdruckes oder Blockes selbst, wie folgt. Der Ausdruck kann aus

  • einem einzigen Funktionsaufruf bestehen
  • mehreren Funktionsaufrufen bestehen, jedoch ist in diesem Fall ein Block bestehend aus einer „{“ und einer „}“ (Scope) nötig
  • einer Methoden Referenz bestehen

Beispiele für gültige Lambdas sind im Folgenden sichtbar.

// Body with one expression
x -> x + 1;

// Body with one expression
(x, y) -> x + y;

// Body with scope block
(String[] args) -> { 
    if (args != null) {
        return args.length;
    } else {
        return 0;
    }
};

// Method Reference
System.out::println;

@FunctionalInterface

Doch stellt sich nun die Frage, woher weiß der Java Cmpiler, ob der Lambda Ausdruck wirklich ein Lambda Ausdruck ist? Einmal von der Notation des Lambdas abgesehen, ist ein Lambda ein @FunctionalInterface wie bereits beim Checkable interface weiter oben, sichtbar wurde. Das heißt, dass ein Lambda Ausdruck ist nur möglich ist, wenn hinter dem Lambda ein Interface, welches mit der @FunctionalInterface Annotation annotiert wurde, vorhanden ist.

Dies bringt jedoch folgende Regeln mit sich: Das Interface darf maximal eine abstrakte Methode beinhalten. Würden mehre abstrakte Methoden vorhanden sein, so wüsste der Java Compiler nicht, welchen von diesen nun ausgeführt werden müsste, da ein Lambda ja nur eine einzige anonyme Funktion darstellt. Diese Methode darf nun jede Charakteristika einer klassischen Methode aufweisen, so zum Beispiel eine Argumentenliste bzw. die möglichen Exceptions, welche diese werfen könnte.

Lambdas in JDK 8

Die JDK 8 stellt dabei schon einige Lambda Typen zur Verfügung, allerdings nur solche welche von der JDK 8 benötigt werden. Sollen andere Lambdas verwendet werden, so muss jenes leider selbst implementiert werden.

Der Consumer repräsentiert dabei ein Lambda, welches ein Argument „konsumiert“, da es schlichtweg void returniert. Dieses Lambda eignet sich nunmehr hervorragend für das Verwerten von irgendwelchen Parametern, sowie für das Debugging.

Im Gegensatz dazu steht der Supplier, welcher anders als beim Consumer kein Argument konsumiert, sondern einen Wert returniert und dabei kein Argument bekommt. Dieses eignet sich besonders, um Werte anzubieten bzw. bereitzustellen.

Hingegen ist ein Predicate ein Lambda, welches ein Argument bekommt und boolean returniert. So eignet sich dieses Lambda perfekt für das Abbilden von Konditionen (vgl. if-Statement). Ebenfalls können damit logische Operationen wunderbar repräsentiert werden.

JDK 8 inkludiert ebenfalls Operatoren. Diese Lambdas zeichnen sich dadurch aus, dass das Argument, sowie der Rückgabewert dem gleichen Typ entsprechen. Besonders wichtig sind diese Lambdas, wenn für das Argument eines Typs verändert und returniert werden muss.

Schließlich ist das Function Interface eine generelle Abstraktion von sonstigen Funktionskombinationen. Dabei gibt es zum Beispiel Funktionen, welche ein Argument eines Typs bekommt und einen anderen Typ returniert. Es gibt viele weitere Kombinationen, die hier entnommen werden können.

Bestehende Interfaces als Lambdas

Die JDK 8 ermöglicht zusätzlich ältere Interfaces als Lambdas zu benutzen. So zum Beispiel das Runnable Interface, welches das Implementieren eines Threads deutlich erleichter und auch das Comparator Interface, durch welche Vergleichsoperationen auf zwei Argumenten auch ohne Boilerplate Code möglich sind.

Methoden und Konstruktor Referenzen

Lambdas an sich repräsentieren nun schon eine sehr kurze Schreibweise für unterschiedliche Implementierungen. Doch manchmal ist auch diese Schreibweise schlichtweg zu lang bzw. produziert ein wenig Boilerplate Code. So wurden Methoden Referenzen zusätzlich eingeführt. Allerdings gilt: Regeln für Lambdas gelten auch für Methoden Referenzen. Dabei setzen Methoden Referenzen auf den neuen „::“ Operator, welcher bereits oben in einem Beispiel kurz sichtbar war.

Zum Beispiel vereinfacht folgende Methoden Referenz, das folgende Lambda.

// Lambda
str -> System.out.println(str);

// Method Reference
System.out::println;

Wie sichtbar ist, ist dies schon um einiges kürzer. Allerdings muss der Typ des Argumentes mit dem Typ der Methoden Referenz übereinstimmen. Ansonsten sind Methoden Referenzen nicht schreibbar.

Eine Spezialisierung einer Methden Referenz ist die Konstruktor Referenz, welche in folgendem Beispiel zu sehen ist. Diese ist nur bei einem Supplier anwendbar, da der Konstruktor immernoch eine Instanz des jeweiligen Objektes returniert.

// Constructor Reference (Supplier)
String::new;

Nachteile von Lambdas

Auch wenn in diesem Artikel bisher nur Lambdas demonstriert und deren Vorteile erwähnt wurden, so sollten die Nachteile nicht außer Acht gelassen werden. Ein großer Nachteil von Lambdas ist, dass diese schlichtweg ein Interface sind. Sollten eigene spezifische Lambdas implementiert werden, so muss für diese ein @FunctionalInterface angelegt werden. Der Grund hierfür ist, dass Java als objektorientierte Sprache nur Objekte kennt.

Des Weiteren, wurde in Java das stark umstrittene Autoboxing in einer früheren Version eingeführt. Da die meisten Lambdas generisch sind, also Generics benutzen, wird Autoboxing bei primitiven Typen vermehrt durchgeführt. Dies liegt daran, dass zum Beispiel der primitive Typ int im Generic nicht eingesetzt werden kann, sondern das Object Integer genutzt werden muss. Um diesen Umstand zu umgehen, wurden spezifische Lambdas für primitive Typen eingeführt, wie zum beispiel IntFunction, LongFunction und DoubleFunction.  Wir erinnern uns, dass in der JDK 8 lediglich die Lambdas implementiert sind, welche von dieser benötigt werden. Dies resultiert darin, dass nur Lambdas für die primitiven Typen int, long und double implementiert wurden. Was ist jedoch mit boolean, byte, char, float und short?

Schließlich soll das Thema Checked Exceptions angesprochen werden. Viele Entwickler empfinden Checked Exceptions als eines der unnötigsten Erweiterungen von Java. Die Verwendung von Lambdas mit Checked Exceptions ist in Java 8 scheinbar nicht vorgesehen. So ist der große Vorteil von Lambdas, nämlich die Reduktion von Boilerplate Code, nicht mehr gegeben. Man stelle sich folgendes Beispiel:

className -> {
    try {
        return Class.forName(className);
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
};

Dennoch sollte folgender Ausdruck möglich sein:

className -> Class::forName; // may throw ClassNotFoundException
Tagged with:     , , , ,

About the author /


Schon früh hat sich meine technische Begabung gezeigt, weshalb ich mich vor ein paar Jahren entschloss ein Informatik Studium zu beginnen. Ich beschäftige mich intensiv mit Programmieren, besonders im Bereich Java und Mobile Applications. Ich bin immer bemüht mich in verschiedensten technischen Bereichen weiterzubilden und neue Erfahrungen zu sammeln. Außerhalb der technischen Welt spiele ich gerne Squash, hin und wieder ein interessantes Computerspiel und ich versuche mich im Kochen und Backen mit mehr oder weniger gutem Erfolg.

Related Articles

Post your comments

Your email address will not be published. Required fields are marked *

*

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden .

Unterstütz uns!

Folgt uns!

Diese Seite

wurde erstellt mit Ehrgeiz, Liebe und viel Koffein. Bei der Erstellung kamen keine jar-Dateien zu Schaden. Das Logo wurde erstellt von Star-seven.at.