Einführung in Vavr

1. Überblick

In diesem Artikel untersuchen wir genau, was Vavr ist, warum wir es brauchen und wie wir es in unseren Projekten einsetzen können.

Vavr ist eine Funktionsbibliothek für Java 8, die unveränderliche Datentypen und funktionale Kontrollstrukturen bereitstellt.

1.1. Maven-Abhängigkeit

Um Vavr verwenden zu können, müssen Sie die Abhängigkeit hinzufügen:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

Es wird empfohlen, immer die neueste Version zu verwenden. Sie können es erhalten, indem Sie this link folgen.

2. Möglichkeit

__Das Hauptziel von Option besteht darin, Nullprüfungen in unserem Code durch Nutzung des Java-Typsystems zu beseitigen.

Option ist ein Objektcontainer in Vavr mit einem ähnlichen Endziel wie optional in Java 8. Vavrs Option implementiert Serializable, Iterable, und verfügt über eine umfangreichere API _. _

Da jeder Objektverweis in Java einen null -Wert haben kann, müssen wir ihn in der Regel vor der Verwendung mit if -Anweisungen auf Nichtigkeit prüfen. Diese Prüfungen machen den Code robust und stabil:

@Test
public void givenValue__whenNullCheckNeeded__thenCorrect() {
    Object possibleNullObj = null;
    if (possibleNullObj == null) {
        possibleNullObj = "someDefaultValue";
    }
    assertNotNull(possibleNullObj);
}

Ohne Prüfung kann die Anwendung aufgrund eines einfachen NPE abstürzen:

@Test(expected = NullPointerException.class)
public void givenValue__whenNullCheckNeeded__thenCorrect2() {
    Object possibleNullObj = null;
    assertEquals("somevalue", possibleNullObj.toString());
}

Die Prüfungen machen den Code jedoch verbose und nicht so lesbar , insbesondere wenn die if -Anweisungen mehrfach verschachtelt werden.

Option löst dieses Problem, indem nulls vollständig entfernt und durch eine gültige Objektreferenz für jedes mögliche Szenario ersetzt wird.

Mit Option wird ein null -Wert zu einer Instanz von None ausgewertet, während ein Nicht-NULL-Wert zu einer Instanz von Some ausgewertet wird:

@Test
public void givenValue__whenCreatesOption__thenCorrect() {
    Option<Object> noneOption = Option.of(null);
    Option<Object> someOption = Option.of("val");

    assertEquals("None", noneOption.toString());
    assertEquals("Some(val)", someOption.toString());
}

Anstatt Objektwerte direkt zu verwenden, empfiehlt es sich daher, sie wie oben gezeigt in eine Option -Instanz zu packen.

Beachten Sie, dass wir vor dem Aufruf von toString keine Prüfung durchführen mussten, jedoch nicht wie zuvor mit einer NullPointerException behandelt wurden. ToString der Option gibt bei jedem Aufruf sinnvolle Werte zurück.

Im zweiten Abschnitt dieses Abschnitts brauchten wir einen null check, bei dem wir der Variablen einen Standardwert zuweisen, bevor sie verwendet werden. Option kann dies in einer einzigen Zeile behandeln, auch wenn eine Null vorhanden ist:

@Test
public void givenNull__whenCreatesOption__thenCorrect() {
    String name = null;
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}

Oder eine Nicht-Null:

@Test
public void givenNonNull__whenCreatesOption__thenCorrect() {
    String name = "baeldung";
    Option<String> nameOption = Option.of(name);

    assertEquals("baeldung", nameOption.getOrElse("notbaeldung"));
}

Beachten Sie, wie wir ohne null -Prüfungen einen Wert erhalten oder einen Standardwert in einer einzelnen Zeile zurückgeben können.

3. Tuple

Es gibt keine direkte Entsprechung einer Tupeldatenstruktur in Java. Ein Tupel ist ein allgemeines Konzept in funktionalen Programmiersprachen. ** Tupel sind unveränderlich und können typensicher mehrere Objekte unterschiedlichen Typs aufnehmen.

Vavr bringt Tupel nach Java 8. Tupel sind vom Typ Tuple1, Tuple2 bis Tuple8 , abhängig von der Anzahl der Elemente, die sie übernehmen sollen.

Derzeit gibt es eine Obergrenze von acht Elementen. Wir greifen auf Elemente eines Tupels wie tuple. n zu, wobei n__ dem Begriff eines Index in Arrays ähnelt:

public void whenCreatesTuple__thenCorrect1() {
    Tuple2<String, Integer> java8 = Tuple.of("Java", 8);
    String element1 = java8.__1;
    int element2 = java8.__2();

    assertEquals("Java", element1);
    assertEquals(8, element2);
}

Beachten Sie, dass das erste Element mit n == 1 abgerufen wird. Ein Tupel verwendet also keine Nullbasis wie ein Array. Die Typen der Elemente, die im Tupel gespeichert werden, müssen in der Typdeklaration wie oben und unten angegeben deklariert werden:

@Test
public void whenCreatesTuple__thenCorrect2() {
    Tuple3<String, Integer, Double> java8 = Tuple.of("Java", 8, 1.8);
    String element1 = java8.__1;
    int element2 = java8.__2();
    double element3 = java8.__3();

    assertEquals("Java", element1);
    assertEquals(8, element2);
    assertEquals(1.8, element3, 0.1);
}

Die Stelle eines Tupels besteht darin, eine feste Gruppe von Objekten eines beliebigen Typs zu speichern, die besser als Einheit verarbeitet werden und herumgereicht werden können. Ein offensichtlicherer Anwendungsfall ist die Rückgabe von mehr als einem Objekt aus einer Funktion oder einer Methode in Java.

4. Versuchen

In Vavr ist Try ein Container für eine Berechnung , die zu einer Ausnahme führen kann.

Da Option ein nullfähiges Objekt umhüllt, sodass wir nulls nicht explizit mit if -Überprüfungen erledigen müssen, führt Try eine Berechnung aus, sodass Ausnahmen mit try-catch -Blöcken nicht explizit berücksichtigt werden müssen.

Nehmen Sie zum Beispiel den folgenden Code:

@Test(expected = ArithmeticException.class)
public void givenBadCode__whenThrowsException__thenCorrect() {
    int i = 1/0;
}

Ohne try-catch -Blöcke würde die Anwendung abstürzen. Um dies zu vermeiden, müssen Sie die Anweisung in einen try-catch -Block einschließen.

Mit Vavr können wir den gleichen Code in eine Try -Instanz einfassen und erhalten ein Ergebnis:

@Test
public void givenBadCode__whenTryHandles__thenCorrect() {
    Try<Integer> result = Try.of(() -> 1/0);

    assertTrue(result.isFailure());
}

Ob die Berechnung erfolgreich war oder nicht, kann an jeder Stelle des Codes durch Auswahl geprüft werden.

Im obigen Abschnitt haben wir uns entschieden, einfach nach Erfolg oder Misserfolg zu suchen. Wir können auch einen Standardwert zurückgeben:

@Test
public void givenBadCode__whenTryHandles__thenCorrect2() {
    Try<Integer> computation = Try.of(() -> 1/0);
    int errorSentinel = result.getOrElse(-1);

    assertEquals(-1, errorSentinel);
}

Oder sogar eine Ausnahme unserer Wahl explizit zu werfen:

@Test(expected = ArithmeticException.class)
public void givenBadCode__whenTryHandles__thenCorrect3() {
    Try<Integer> result = Try.of(() -> 1/0);
    result.getOrElseThrow(ArithmeticException::new);
}

In allen oben genannten Fällen haben wir dank Vavrs Try die Kontrolle darüber, was nach der Berechnung geschieht.

5. Funktionale Schnittstellen

Mit der Einführung von Java 8 sind Link:/java-8-Funktionsschnittstellen[Funktionsschnittstellen]eingebaut und einfacher zu verwenden, insbesondere in Verbindung mit Lambdas.

Java 8 bietet jedoch nur zwei grundlegende Funktionen. Man nimmt nur einen einzigen Parameter und erzeugt ein Ergebnis:

@Test
public void givenJava8Function__whenWorks__thenCorrect() {
    Function<Integer, Integer> square = (num) -> num **  num;
    int result = square.apply(2);

    assertEquals(4, result);
}

Der zweite nimmt nur zwei Parameter und erzeugt ein Ergebnis:

@Test
public void givenJava8BiFunction__whenWorks__thenCorrect() {
    BiFunction<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

Auf der anderen Seite erweitert Vavr die Idee funktionaler Schnittstellen in Java, indem er bis zu acht Parameter unterstützt und die API mit Methoden für Memoisierung, Komposition und Currying aufpeppt.

Genau wie Tupel werden diese funktionalen Schnittstellen nach der Anzahl der Parameter benannt, die sie verwenden: Function0 , Function1 , Function2 usw. Mit Vavr hätten wir die beiden obigen Funktionen wie folgt geschrieben:

@Test
public void givenVavrFunction__whenWorks__thenCorrect() {
    Function1<Integer, Integer> square = (num) -> num **  num;
    int result = square.apply(2);

    assertEquals(4, result);
}

und das:

@Test
public void givenVavrBiFunction__whenWorks__thenCorrect() {
    Function2<Integer, Integer, Integer> sum =
      (num1, num2) -> num1 + num2;
    int result = sum.apply(5, 7);

    assertEquals(12, result);
}

Wenn es keinen Parameter gibt, aber dennoch eine Ausgabe erforderlich ist, müssten Sie in Java 8 einen Consumer -Typ verwenden. In Vavr ist Function0 zur Unterstützung:

@Test
public void whenCreatesFunction__thenCorrect0() {
    Function0<String> getClazzName = () -> this.getClass().getName();
    String clazzName = getClazzName.apply();

    assertEquals("com.baeldung.vavr.VavrTest", clazzName);
}

Wie wäre es mit einer Funktion mit fünf Parametern? Es ist nur eine Frage der Verwendung von Function5 :

@Test
public void whenCreatesFunction__thenCorrect5() {
    Function5<String, String, String, String, String, String> concat =
      (a, b, c, d, e) -> a + b + c + d + e;
    String finalString = concat.apply(
      "Hello ", "world", "! ", "Learn ", "Vavr");

    assertEquals("Hello world! Learn Vavr", finalString);
}

Wir können auch die statische Factory-Methode FunctionN.of für jede der Funktionen kombinieren, um eine Vavr-Funktion aus einer Methodenreferenz zu erstellen. Wie wenn wir die folgende sum Methode haben:

public int sum(int a, int b) {
    return a + b;
}

Wir können daraus eine Funktion erstellen:

@Test
public void whenCreatesFunctionFromMethodRef__thenCorrect() {
    Function2<Integer, Integer, Integer> sum = Function2.of(this::sum);
    int summed = sum.apply(5, 6);

    assertEquals(11, summed);
}

6. Sammlungen

Das Vavr-Team hat große Anstrengungen unternommen, um eine neue Collection-API zu entwickeln, die die Anforderungen der funktionalen Programmierung erfüllt, d.

  • Java-Sammlungen sind veränderbar, was sie zu einer großen Quelle für Programmfehler macht ** , insbesondere bei gleichzeitiger Anwesenheit. Die Collection -Schnittstelle bietet folgende Methoden:

interface Collection<E> {
    void clear();
}

Diese Methode entfernt alle Elemente in einer Sammlung (erzeugt einen Nebeneffekt) und gibt nichts zurück. Klassen wie ConcurrentHashMap wurden erstellt, um die bereits erstellten Probleme zu beheben.

Eine solche Klasse bietet nicht nur einen unbedeutenden Nutzen, sondern verringert auch die Leistung der Klasse, deren Lücken gefüllt werden sollen.

  • Mit Unveränderlichkeit erhalten wir kostenlos Thread-Sicherheit ** : Es ist nicht notwendig, neue Klassen zu schreiben, um ein Problem zu lösen, das überhaupt nicht vorhanden sein sollte.

Andere bestehende Taktiken, um Sammlungen in Java nicht veränderbar zu machen, verursachen immer noch weitere Probleme, nämlich Ausnahmen:

@Test(expected = UnsupportedOperationException.class)
public void whenImmutableCollectionThrows__thenCorrect() {
    java.util.List<String> wordList = Arrays.asList("abracadabra");
    java.util.List<String> list = Collections.unmodifiableList(wordList);
    list.add("boom");
}

Alle oben genannten Probleme sind in Vavr-Sammlungen nicht vorhanden.

So erstellen Sie eine Liste in Vavr:

@Test
public void whenCreatesVavrList__thenCorrect() {
    List<Integer> intList = List.of(1, 2, 3);

    assertEquals(3, intList.length());
    assertEquals(new Integer(1), intList.get(0));
    assertEquals(new Integer(2), intList.get(1));
    assertEquals(new Integer(3), intList.get(2));
}

APIs sind auch verfügbar, um Berechnungen in der vorhandenen Liste durchzuführen:

@Test
public void whenSumsVavrList__thenCorrect() {
    int sum = List.of(1, 2, 3).sum().intValue();

    assertEquals(6, sum);
}

Vavr-Sammlungen bieten die meisten gebräuchlichen Klassen im Java Collections Framework, und tatsächlich sind alle Funktionen implementiert.

Imbiss ist Unveränderlichkeit , Entfernung von ungültigen Rückgabetypen und Nebeneffekt-APIs , eine reichhaltigere Gruppe von Funktionen für die zugrunde liegenden Elemente , sehr kurzer, robuster und ** kompakter Code im Vergleich zu Javas Sammlung Operationen.

Eine vollständige Abdeckung der Vavr-Sammlungen liegt nicht im Rahmen dieses Artikels.

7. Validierung

Vavr bringt das Konzept des Applicative Functor aus der funktionalen Programmierwelt in Java. Im einfachsten Sinne ermöglicht es ein Applicative Functor , eine Folge von Aktionen durchzuführen, während die Ergebnisse gesammelt werden .

Die Klasse vavr.control.Validation erleichtert das Sammeln von Fehlern. Denken Sie daran, dass ein Programm normalerweise beendet wird, sobald ein Fehler auftritt.

Validation verarbeitet jedoch weiterhin die Fehler und sammelt die Fehler, damit das Programm sie als Stapel verarbeiten kann.

Beachten Sie, dass wir Benutzer mit name und age registrieren. Wir möchten zunächst alle Eingaben übernehmen und entscheiden, ob Sie eine Person -Instanz erstellen oder eine Fehlerliste zurückgeben. Hier ist unsere Person Klasse:

public class Person {
    private String name;
    private int age;

   //standard constructors, setters and getters, toString
}

Als Nächstes erstellen wir eine Klasse mit dem Namen PersonValidator . Jedes Feld wird von einer Methode validiert. Mit einer anderen Methode können Sie alle Ergebnisse in einer Validation -Instanz kombinieren:

class PersonValidator {
    String NAME__ERR = "Invalid characters in name: ";
    String AGE__ERR = "Age must be at least 0";

    public Validation<Seq<String>, Person> validatePerson(
      String name, int age) {
        return Validation.combine(
          validateName(name), validateAge(age)).ap(Person::new);
    }

    private Validation<String, String> validateName(String name) {
        String invalidChars = name.replaceAll("[a-zA-Z]", "");
        return invalidChars.isEmpty() ?
          Validation.valid(name)
            : Validation.invalid(NAME__ERR + invalidChars);
    }

    private Validation<String, Integer> validateAge(int age) {
        return age < 0 ? Validation.invalid(AGE__ERR)
          : Validation.valid(age);
    }
}

Die Regel für age ist, dass es eine ganze Zahl größer als 0 sein soll, und die Regel für name ist, dass es keine Sonderzeichen enthalten sollte:

@Test
public void whenValidationWorks__thenCorrect() {
    PersonValidator personValidator = new PersonValidator();

    Validation<List<String>, Person> valid =
      personValidator.validatePerson("John Doe", 30);

    Validation<List<String>, Person> invalid =
      personValidator.validatePerson("John? Doe!4", -1);

    assertEquals(
      "Valid(Person[name=John Doe, age=30])",
        valid.toString());

    assertEquals(
      "Invalid(List(Invalid characters in name: ?!4,
        Age must be at least 0))",
          invalid.toString());
}
  • Ein gültiger Wert ist in einer Validation.Valid -Instanz enthalten, eine Liste von Validierungsfehlern befindet sich in einer Validation.Invalid -Instanz ** . Daher muss jede Validierungsmethode eine der beiden Methoden zurückgeben.

Inside Validation.Valid ist eine Instanz von Person , während in Validation.Valid eine Liste von Fehlern enthalten ist.

8. Faul

Lazy ist ein Container, der einen Wert darstellt, der träge berechnet wird, d. H.

Die Berechnung wird aufgeschoben, bis das Ergebnis erforderlich ist. Außerdem wird der ausgewertete Wert zwischengespeichert oder gespeichert und jedes Mal, wenn er benötigt wird, immer wieder zurückgegeben, ohne dass die Berechnung wiederholt wird:

@Test
public void givenFunction__whenEvaluatesWithLazy__thenCorrect() {
    Lazy<Double> lazy = Lazy.of(Math::random);
    assertFalse(lazy.isEvaluated());

    double val1 = lazy.get();
    assertTrue(lazy.isEvaluated());

    double val2 = lazy.get();
    assertEquals(val1, val2, 0.1);
}

In dem obigen Beispiel ist die Funktion, die wir auswerten, Math.random .

Beachten Sie, dass wir in der zweiten Zeile den Wert überprüfen und feststellen, dass die Funktion noch nicht ausgeführt wurde. Dies liegt daran, dass wir immer noch kein Interesse am Ertragswert gezeigt haben.

In der dritten Codezeile zeigen wir Interesse an dem Berechnungswert, indem wir Lazy.get aufrufen. An diesem Punkt wird die Funktion ausgeführt und Lazy.evaluated gibt true zurück.

Wir machen auch weiter und bestätigen das Memo-Bit von Lazy , indem wir versuchen, den Wert erneut zu get . Wenn die von uns bereitgestellte Funktion erneut ausgeführt wird, erhalten wir definitiv eine andere Zufallszahl.

Lazy gibt jedoch den anfänglich berechneten Wert wieder träge zurück, wenn die abschließende Aussage bestätigt wird.

9. Musterabgleich

Pattern Matching ist in fast allen funktionalen Programmiersprachen ein natives Konzept. In Java gibt es vorerst keine solche Sache.

Wann immer wir eine Berechnung durchführen oder einen Wert basierend auf der erhaltenen Eingabe zurückgeben möchten, verwenden wir mehrere if -Anweisungen, um den richtigen Code für die Ausführung aufzulösen:

@Test
public void whenIfWorksAsMatcher__thenCorrect() {
    int input = 3;
    String output;
    if (input == 0) {
        output = "zero";
    }
    if (input == 1) {
        output = "one";
    }
    if (input == 2) {
        output = "two";
    }
    if (input == 3) {
        output = "three";
    }
    else {
        output = "unknown";
    }

    assertEquals("three", output);
}

Wir können plötzlich feststellen, dass der Code mehrere Zeilen umfasst, während wir nur drei Fälle prüfen. Jede Prüfung nimmt drei Codezeilen in Anspruch. Was wäre, wenn wir bis zu hundert Fälle prüfen müssten, wären das etwa 300 Zeilen, nicht schön!

Eine andere Alternative ist die Verwendung einer switch -Anweisung:

@Test
public void whenSwitchWorksAsMatcher__thenCorrect() {
    int input = 2;
    String output;
    switch (input) {
    case 0:
        output = "zero";
        break;
    case 1:
        output = "one";
        break;
    case 2:
        output = "two";
        break;
    case 3:
        output = "three";
        break;
    default:
        output = "unknown";
        break;
    }

    assertEquals("two", output);
}

Nicht besser Wir haben immer noch durchschnittlich 3 Zeilen pro Scheck. Viel Verwirrung und Fehlerpotenzial. Das Vergessen einer break -Klausel ist zum Zeitpunkt der Kompilierung kein Problem, kann jedoch später zu schwer erkennbaren Fehlern führen.

In Vavr ersetzen wir den gesamten switch -Block durch eine Match -Methode.

Jede case - oder if -Anweisung wird durch einen Aufruf der Case -Methode ersetzt.

Schließlich ersetzen atomare Muster wie $ () die Bedingung, die dann einen Ausdruck oder einen Wert bewertet. Wir stellen dies auch als zweiten Parameter für Case zur Verfügung:

@Test
public void whenMatchworks__thenCorrect() {
    int input = 2;
    String output = Match(input).of(
      Case($(1), "one"),
      Case($(2), "two"),
      Case($(3), "three"),
      Case($(), "?"));

    assertEquals("two", output);
}

Beachten Sie, wie kompakt der Code ist, indem Sie pro Prüfung durchschnittlich nur eine Zeile berechnen. Die Pattern-Matching-API ist wesentlich leistungsfähiger und kann komplexere Aufgaben erledigen.

Beispielsweise können wir die atomaren Ausdrücke durch ein Prädikat ersetzen.

Stellen Sie sich vor, wir analysieren einen Konsolenbefehl für help - und version -Flags:

Match(arg).of(
    Case($(isIn("-h", "--help")), o -> run(this::displayHelp)),
    Case($(isIn("-v", "--version")), o -> run(this::displayVersion)),
    Case($(), o -> run(() -> {
        throw new IllegalArgumentException(arg);
    }))
);

Einige Benutzer sind möglicherweise mit der Kurzversion (-v) vertraut, andere mit der Vollversion (–version). Ein guter Designer muss alle diese Fälle berücksichtigen.

Ohne die Notwendigkeit mehrerer if -Anweisungen haben wir uns um mehrere Bedingungen gekümmert. In einem separaten Artikel erfahren Sie mehr über Prädikate, multiple Bedingungen und Nebenwirkungen beim Pattern-Matching.

10. Fazit

In diesem Artikel haben wir Vavr, die beliebte Bibliothek für die Funktionsprogrammierung für Java 8, vorgestellt. Wir haben die wichtigsten Funktionen in Angriff genommen, die wir schnell anpassen können, um unseren Code zu verbessern.

Den vollständigen Quellcode für diesen Artikel finden Sie im https://github.com/eugenp/tutorials/tree/master/vavr [Github-Projekt.