In weiteren Schritten zu Funktionaler Programmierung

Checkpoint Java: Funktionaler mit Java 9

Sven Ruppert

© Shutterstock / Suttha Burawonk

Im diesem Teil der Kolumne „Checkpoint Java“ machen wir die nächsten Schritte hin zu Funktionaler Programmierung. Dazu wirft der Autor einen Blick auf die Änderungen von Java 8 zu 9 und was man mit den neuen Möglichkeiten so alles anstellen kann.

In unserem letzten Teil dieser Serie zum Thema „Funktional und reaktiv mit Java 9“ haben wir die ersten Umformungen vorgenommen und uns angesehen, wie sich der Quelltext verändert, wenn wir ein wenig funktionale Elemente und Aspekte verwenden. Aber die Frage, ab wann wir von funktionaler Entwicklung sprechen, ist immer noch offen. Beginnen wir also mit ein paar Beispielen genau dazu.

Wir werden für diese Artikelreihe von Beginn an mit JDK 9 arbeiten, auch wenn es zum Zeitpunkt der Veröffentlichung noch nicht final verfügbar ist. Das OpenJDK kann hier gefunden werden. Zuzüglich zu den Quelltextbeispielen in diesem Artikel verwende ich auch die Sourcen des Open-Source-Projekts Functional-Reactive. Die Sourcen liegen auf GitHub.

FunctionalInterfaces und Lambdas

Mit Java 8 haben wir FunctionalInterfaces bekommen, die in Java 9 erweitert worden sind. Diese sind ein oft verwendetes technisches Mittel, um funktionale Elemente in Java zu realisieren. Zur Wiederholung ein paar Kleinigkeiten zu den Interfaces und den Änderungen in Java 9: Wir haben ein Interface mit zwei Methoden. Eine davon ist die statische Methode doMore()die andere doSomething() hat eine default-Implementierung.

  public interface DemoInterface {
    static void doMore() {
      System.out.println(DemoInterface.class.getSimpleName() + ">

Nun erstellen wir ein Interface mit dem Namen InterfaceA und erben von DemoInterface. Hier überschreiben wir die Methode doSomething().

  public interface InterfaceA extends DemoInterface {
    default void doSomething() {
      System.out.println(InterfaceA.class.getSimpleName() + " : " + "doSomething");
    }
  }

Als letztes erstellen wir ein Interface mit dem Namen InterfaceB, das nicht vom Interface DemoInterface erbt, jedoch auch eine Methode mit dem Namen doSomething und einer default-Implementierung hat. Wenn wir nun folgenden Quelltext schreiben und ausführen, was erhalten wir dann für Meldungen auf der Konsole?

  public static void main(String[] args) {
    DemoInterface.doMore();
    new ImplA().doSomething();
    ImplA.doMore(); 
    new ImplB().doSomething();
  }

Das erste was uns passiert, ist ein Abbruch der Übersetzung. Die Zeile ImplA.doMore() wird mit einem cannot find symbol „doMore()“ durch den Kompiler quittiert. Statische Methoden können also nur auf dem originalen Interface aufgerufen werden. Leider wird das immer Mal wieder gerne vergessen und erst später in der Implementierung bemerkt. Wenn wir die betreffende Zeile auskommentieren, können wir den Quelltext ausführen und erhalten die nachfolgende Ausgabe.

DemoInterface : doMore
InterfaceA : doSomething
ImplB : doSomething

Es wird also immer die letzte Implementierung aus der Vererbungskette verwendet. Im Fall von InterfaceB kann der Kompiler nicht selbst entscheiden, welche Implementierung die dominante von beiden ist. Hier muss der Entwickler mittels Implementierung selbst die Endscheidung treffen.

In Java 9 können wir nun zusätzlich Methoden mit der Sichtbarkeit private deklarieren und implementieren. Eine reine Deklaration ist nicht möglich, da diese auch in erbenden Interfaces nicht mit einer Implementierung versehen werden kann. Nun sind wir aber wieder in der Lage, größere default-Implementierungen sauber in mehrere Methoden innerhalb eines Interface aufzuteilen. In Java 8 musste dies entweder ausgelagert werden oder führte zu unschönen Quelltextfragmenten.

Kommen wir nun zu den FunctionalInterfaces. Die Definition der FunctionalInterfaces ist in Java 9 immer noch dieselbe wie zu Java-8-Zeiten. Es handelt sich um ein FunctionalInterface, wenn eine noch zu implementierende Methode vorhanden ist. Die Anzahl der Methoden mit default
Implementierung hat hier keinen Einfluss, auch nicht, ob die Annotation @FunctionalInterface vorhanden ist oder nicht.

Nun zu den Lambdas. Ich werden nicht auf alle möglichen Formen eingehen. Nur soweit, dass überall dort, wo ein FunctionalInterface als Attribut übergeben werde kann, auch ein Lambda zum Einsatz kommt. Die Verwendung von Lambdas wir öfter mit Funktionaler Programmierung gleichgesetzt. Aber ist es das wirklich? Sehen wir uns dazu folgendes Beispiel an:

public class Main {

  @FunctionalInterface
  public static interface Service {
    public Integer doWork(String value);
  }

  public static class ServiceImpl implements Service {
    @Override
    public Integer doWork(String value) {
      return Integer.valueOf(value);
    }
  }

  public static void main(String[] args) {
    Integer integer = new ServiceImpl().doWork("1");
    Service serviceA = (value)-> { return Integer.valueOf(value);};
    Service serviceB = Integer::valueOf;
    serviceA.doWork("1");
    serviceB.doWork("1"); 
  }
}

Hier haben wir ein Interface mit dem Namen Service, das  durch die Klasse ServiceImpl klassisch implementiert worden ist. Die Verwendung erfolgt wie in Java üblich, indem eine Instanz der Klasse erzeugt wird, um nachfolgend die Methode aufzurufen. Aber auch die beiden anderen Implementierungen, erst mittels Lambda und nachfolgend mittels Methodreference, können wie gewohnt verwendet werden. Nur haben wir hier einen etwas anderen Lebenszyklus bei der Initialisierung. Und es kann keine Klassenattribute (Zustand) wie bei der ersten klassischen Implementierung geben. Das kann man schon in den Bereich der funktionalen Aspekte aufnehmen. Also Wert rein, Wert raus und kein Zustand, der etwas verderben kann. Die Unterschiede zwischen Lambdas und Methodreference lasse ich hier bewusst außen vor.

Nun kann es ja sein, dass die Methode mit dem Eingangswert „HoppelPoppel“ aufgerufen wird. Was soll die Antwort der Methode sein? Entweder eine Exception oder ein null? Beides ist gegen die grundsätzlichen Überlegungen der Funktionalen Programmierung.

Checkpoint Java

In dieser Kolumne klopft der Autor Sven Ruppert (Vaadin) Java auf alltägliche Probleme ab. Er gibt hilfreiche Tipps und Tricks, wie Entwickler gängige Stolperfallen vermeiden und klareren Code schreiben können. Einen besonderen Blick wirft er auf die neuen Möglichkeiten von Funktionaler und Reaktiver Programmierung. Alle Teile der Kolumne Checkpoint Java finden sich hier.

Optional zur Rettung

Sehen wir uns hierzu die seit Java 8 verfügbare Klasse Optional an. Sie dient dazu, eine Referenz zurückzugeben, selbst wenn der Wert nicht verfügbar ist, um den es letztendlich geht. Anstelle von null oder dem Wert (die Instanz vom gewünschten Typ) bekommt man nun eine Instanz der Klasse Optional. Hierzu schreiben wir das Interface um. Die klassische Implementierung lasse ich im Listing weg. Sie ist im git Repository Package v003 zu finden.

  @FunctionalInterface
  public static interface Service {
    public Optional<Integer> doWork(String value);
  }

  public static class ServiceImpl implements Service {
    @Override
    public Optional<Integer> doWork(String value) {
      try {
        return Optional.of(Integer.valueOf(value));
      } catch (NumberFormatException e) {
        e.printStackTrace();
      }
      return Optional.empty();
    }
  }

  public static void main(String[] args) {
    Optional<Integer> integer = new ServiceImpl().doWork("1");

    Service serviceA = (value) -> {
      try {
        return Optional.of(Integer.valueOf(value));
      } catch (NumberFormatException e) {
        e.printStackTrace();
      }
      return Optional.empty();
    };
  
  //SNIPP usage of the implementations  
  }

Es werden immer Werte übergeben, bzw. es wird nie null als Rückgabewert verwendet. Außerdem ist kein Wert aus dem Wertevorrat der Zielmenge, hier Integer, dazu verwendet worden, einen Fehler zu signalisieren. Die RuntimeException muss auch nicht mehr bei der Verwendung gefangen werden.
Wir haben also ein wesentlich robusteres Stück Quelltext geschrieben. Wenn wir die Verwendung der Instanz Optional ansehen, kommen wir einen Schritt weiter in Richtung funktionaler Aspekte. Nun können wir hier wieder, wie im letzten Teil der Serie, die Methode in eine Function<String, Optional> umschreiben und als Rückgabewert einer statischen Methode implementieren.

  @FunctionalInterface
  public static interface Service {
    public Optional<Integer> doWork(String value);
  }

  
  static Function<String, Optional<Integer>> service() {
    return (value) -> {
      try {
        return Optional.of(Integer.valueOf(value));
      } catch (NumberFormatException e) {
        e.printStackTrace();
      }
      return Optional.empty();
    };
  }

  public static void main(String[] args) {
    Optional<Integer> apply = service().apply("1");
  }

Jetzt haben wir wieder eine Funktion. Aber den Bezug zu der fachlichen Ausprägung Service ist verloren gegangen. Bei Bedarf gibt es wieder die Möglichkeit, dies mittels Vererbung in den fachlichen Namensbereich Service zu heben.

public class Main {

  @FunctionalInterface
  public static interface Service extends Function<String, Optional<Integer>> {
    public default Optional<Integer> doWork(String value) {
      return apply(value);
    }
  }

  static Service service() {
    return (value) -> {
      try {
        return Optional.of(Integer.valueOf(value));
      } catch (NumberFormatException e) {
        e.printStackTrace();
      }
      return Optional.empty();
    };
  }

  public static void main(String[] args) {

    Optional<Integer> applyA = service().apply("1");
    
    Optional<Integer> applyB = service().doWork("1");
  }
}

Hier ist zu beachten, das die Methode mittels default-Implementierung auf die Methode apply delegiert und wir dadurch auch wieder ein FunctionalInterface erhalten. Der Rückgabewert kann jetzt unterschiedlich verarbeitet werden.

Wir haben also ein Optional, in dem ein Integer verpackt ist. Was nun? Um die Möglichkeiten zu erkunden, gehen wir einmal ein wenig die Varianten durch, die uns ein Optional bietet.

    Optional<Integer> apply = service().doWork("1");

    final Integer intA = apply.get();
  //if(apply.isPresent()){   
    if (intA != null) {
      //do something usefull
    }
    else {
      //do something ???
    }

Natürlich können wir den enthaltenen Wert einfach dem Optional entnehmen. Danach sind wir aber wieder genau an dem Punkt angekommen, an dem wir losgelaufen sind. Wir müssen entweder den Wert an sich auf null prüfen oder das Optional selbst mittels der Methode isPresent() befragen, ob ein Wert enthalten ist. Das Ziel ist jedoch, diese direkten Kontrollstrukturen durch funktionale Aspekte zu ersetzen.

    final Integer intB1 = apply.orElseGet(() -> - 1);
    final Integer intB2 = apply.orElse(- 1);
    final Integer intB3 = apply.orElseThrow(() -> new RuntimeException("panic ;-)"));

Ein Optional kann auch mit Alternativwerten versehen werden oder eine Exception werfen, wenn kein Wert vorhanden ist. Letzteres ist jedoch mit Bedacht zu verwenden, da wir damit erst zur Laufzeit wieder konfrontiert werden. Man kann eine Aktion auch nur mit dem Vorhandensein eines Wertes verknüpfen.

    apply.ifPresent(v -> {
      System.out.println("v = " + v);
    });

Bei der Verwendung von Java 8 ist schnell aufgefallen, dass es einige Unzulänglichkeiten gibt. Zum Beispiel fehlt die Möglichkeit zusätzlich zur Aktion beim Vorhandensein eines Wertes auch eine Aktion zu definieren, wenn eben kein Wert vorhanden ist. Das wurde in Java 9 erweitert.

    //since 9
    apply.ifPresentOrElse(v -> {
      System.out.println("v = " + v);
    } , () -> {
      System.out.println("value not present");
    });

Ebenfalls wurde die Kombination ifPresent() und get() oft verwendet. Deswegen gibt es nun in Java 9 ein stream(), das einen Stream von Optional liefert.

final Stream<Integer> stream = apply.stream();

Außerdem wurde eine Möglichkeit für eine Alternative eingebaut. Diesmal aber nicht für den Wert, sondern als Optional. Der Typ T kann dabei allerdings nicht geändert werden. Die Alternative wird geliefert, wenn die Methode ìsPresent() false liefert.

apply.or(() -> Optional.of(Integer.MAX_VALUE));

Fazit

Wenn man alles zusammenfasst, ergeben sich verschiedene Ansatzpunkte, die man direkt im täglichen Arbeitsalltag anwenden kann. Zum Beispiel kann man APIs mit einem Optional versehen, das man an der Stelle der NullPointerException entgegen arbeitet. Die Verwendung von Optional führt auch dazu, dass man Kontrollstrukturen anders formulieren kann. Es sind keine imperativen Fallunterscheidungen nötig. Und das geht definitiv in Richtung funktionaler Entwicklung. Wenn man noch ein wenig mehr darüber nachdenkt, kann man mit der map()-Methode bei einem Optional auch noch andere Dinge anstellen.

    String value = "";
    service()
        .doWork(value)
        .or(() -> Optional.of(0))  // per definition
        .map((Function<Integer, Function<Integer, String>>) integer 
                    -> (valueToAdd) -> integer + valueToAdd + " was calculated")
        .ifPresentOrElse(
            fkt -> System.out.println("f(10) ==> = " + fkt.apply(10)),
            () -> System.out.println("value not present (usless here)"));

Was man daraus machen kann, werden wir noch sehen, und auch, wie wir die Unzulänglichkeiten des Optional in Java 8 aber auch die in Java 9 noch umgehen können. Den Quelltext findet ihr auf GitHub. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare
  1. Sven Ruppert2017-08-19 06:08:08

    Es hat sich ein kleiner BUG eingeschlichen (danke https://github.com/ChristianCiach )

    -> https://github.com/Java-Publications/functional-reactive-with-core-java-002/commit/584880074d45ed4576f8d124c02f42597ed1e030#commitcomment-23723014

    Habe eine kleine Änderung vorgenommen.

    https://github.com/Java-Publications/functional-reactive-with-core-java-002/commit/0405cf40f0a35081a2739cc861ffa723711882f8

    Die Stelle sollte dann eher lauten:

    ##########
    Natürlich können wir den enthaltenen Wert einfach dem Optional entnehmen.
    Wenn dieser jedoch **null** ist, so wird eine ```NoSuchElementException``` geworfen.
    Wir müssen das Optional selbst mittels der Methode ```isPresent()``` befragen ob ein Wert enthalten ist.
    #########

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.