Введение в Вавр

1. Обзор

В этой статье мы собираемся исследовать, что именно представляет собой Vavr , зачем он нам нужен и как его использовать в наших проектах.

Vavr - это функциональная библиотека для Java 8, предоставляющая неизменяемые типы данных и функциональные структуры управления.

1.1. Maven Dependency

Чтобы использовать Vavr, вам нужно добавить зависимость:

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

Рекомендуется всегда использовать последнюю версию. Вы можете получить его, перейдя по this link .

2. Опция

Основной целью Option является устранение нулевых проверок в нашем коде за счет использования системы типов Java.

Option - это контейнер объектов в Vavr с аналогичной конечной целью, такой как ссылка:/java-option[Optional]в Java 8. Option в Vavr реализует Serializable, Iterable, и имеет более богатый API _. _

Поскольку любая ссылка на объект в Java может иметь значение null , мы обычно должны проверять ее на недействительность с помощью операторов if перед ее использованием. Эти проверки делают код надежным и стабильным:

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

Без проверок приложение может аварийно завершить работу из-за простого NPE:

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

Тем не менее, проверки делают код многословным и не очень читабельным , особенно когда операторы if оказываются вложенными несколько раз.

Option решает эту проблему, полностью удаляя nulls и заменяя их действительной ссылкой на объект для каждого возможного сценария.

С Option значение null будет оцениваться для экземпляра None , в то время как ненулевое значение будет оцениваться для экземпляра Some :

@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());
}

Следовательно, вместо непосредственного использования значений объектов желательно обернуть их внутри экземпляра Option , как показано выше.

Обратите внимание, что нам не нужно было делать проверку перед вызовом toString , но нам не приходилось иметь дело с NullPointerException , как мы это делали раньше. Опция toString возвращает нам значимые значения в каждом вызове.

Во втором фрагменте этого раздела нам понадобилась проверка null , в которой мы назначали бы значение по умолчанию для переменной, прежде чем пытаться ее использовать. Option может справиться с этим в одной строке, даже если есть ноль:

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

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

Или ненулевой

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

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

Обратите внимание, как без null проверок мы можем получить значение или вернуть значение по умолчанию в одной строке.

3. Кортеж

В Java нет прямого эквивалента структуры данных кортежа. Кортеж является распространенным понятием в функциональных языках программирования. Кортежи являются неизменяемыми и могут содержать несколько объектов разных типов безопасным для типов образом.

Vavr переносит кортежи в Java 8. Кортежи относятся к типу Tuple1, от Tuple2 до Tuple8 в зависимости от количества элементов, которые они должны взять.

В настоящее время существует верхний предел восьми элементов. Мы получаем доступ к элементам кортежа, таким как tuple. n , где n__ аналогично понятию индекса в массивах:

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);
}

Обратите внимание, что первый элемент извлекается с помощью n == 1 . Таким образом, кортеж не использует нулевую базу как массив. Типы элементов, которые будут храниться в кортеже, должны быть объявлены в его объявлении типа, как показано выше и ниже:

@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);
}

Место кортежа заключается в хранении фиксированной группы объектов любого типа, которые лучше обрабатываются как единое целое и могут передаваться. Более очевидный вариант использования - это возвращение более одного объекта из функции или метода в Java.

4. Пытаться

В Vavr Try - это контейнер для вычисления , который может привести к исключению.

Поскольку Option оборачивает обнуляемый объект, так что нам не нужно явно заботиться о nulls с проверками if , Try упаковывает вычисления, чтобы нам не пришлось явно заботиться об исключениях с блоками try-catch .

Возьмите следующий код для примера:

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

Без блоков try-catch приложение будет аварийно завершено. Чтобы избежать этого, вам нужно заключить оператор в блок try-catch .

С помощью Vavr мы можем обернуть тот же код в экземпляр Try и получить результат:

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

    assertTrue(result.isFailure());
}

Было ли вычисление успешным или нет, можно проверить по выбору в любой точке кода.

В приведенном выше фрагменте мы решили просто проверить на успех или неудачу. Мы также можем выбрать возврат значения по умолчанию:

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

    assertEquals(-1, errorSentinel);
}

Или даже явно выбросить исключение по нашему выбору:

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

Во всех вышеперечисленных случаях у нас есть контроль над тем, что происходит после вычислений, благодаря Try от Vavr.

5. Функциональные интерфейсы

С появлением Java 8 функциональные интерфейсы стали встроенными и простыми в использовании, особенно в сочетании с лямбдами.

Однако Java 8 предоставляет только две основные функции. Один принимает только один параметр и дает результат:

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

    assertEquals(4, result);
}

Второй принимает только два параметра и дает результат:

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

    assertEquals(12, result);
}

С другой стороны, Vavr расширяет идею функциональных интерфейсов в Java, поддерживая до восьми параметров и добавляя API-интерфейс с помощью методов для запоминания, композиции и каррирования.

Как и в случае кортежей, эти функциональные интерфейсы именуются в соответствии с числом параметров, которые они принимают: Function0 , Function1 , Function2 и т. Д. С помощью Vavr мы бы написали две вышеупомянутые функции, например:

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

    assertEquals(4, result);
}

и это:

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

    assertEquals(12, result);
}

Когда параметра нет, но нам все еще нужен вывод, в Java 8 нам нужно будет использовать тип Consumer , в Vavr Function0 , чтобы помочь:

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

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

Как насчет функции с пятью параметрами, это просто вопрос использования 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);
}

Мы также можем объединить статический фабричный метод FunctionN.of для любой из функций, чтобы создать функцию Vavr из ссылки на метод. Например, если у нас есть следующий метод sum :

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

Мы можем создать функцию из этого следующим образом:

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

    assertEquals(11, summed);
}

6. Коллекции

Команда Vavr приложила немало усилий для разработки нового API коллекций, отвечающего требованиям функционального программирования, то есть постоянства, неизменности.

  • Java-коллекции изменчивы, что делает их отличным источником программных сбоев ** , особенно при наличии параллелизма. Интерфейс Collection предоставляет такие методы, как этот:

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

Этот метод удаляет все элементы в коллекции (производя побочный эффект) и ничего не возвращает. Такие классы, как ConcurrentHashMap , были созданы для решения уже созданных проблем.

Такой класс не только добавляет нулевые предельные выгоды, но также снижает производительность класса, лазейки в котором он пытается заполнить.

  • С неизменяемостью мы получаем потоковую безопасность бесплатно ** : нет необходимости писать новые классы для решения проблемы, которой не должно быть в первую очередь.

Другие существующие тактики добавления неизменности к коллекциям в Java по-прежнему создают дополнительные проблемы, а именно: исключения:

@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");
}

Все вышеперечисленные проблемы отсутствуют в коллекциях Vavr.

Чтобы создать список в 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));
}

API также доступны для выполнения вычислений в списке на месте:

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

    assertEquals(6, sum);
}

Коллекции Vavr предлагают большинство общих классов, имеющихся в Java Collections Framework, и фактически все функции реализованы.

Вывод - это неизменность , удаление пустых типов возврата и API, создающие побочные эффекты , более богатый набор функций для работы с базовыми элементами , очень короткий, надежный и компактный код по сравнению с коллекцией Java операции.

Полное освещение коллекций Vavr выходит за рамки этой статьи.

7. Проверка

Vavr привносит концепцию Applicative Functor в Java из мира функционального программирования. Проще говоря, Applicative Functor позволяет нам выполнять последовательность действий при накоплении результатов .

Класс vavr.control.Validation облегчает накопление ошибок. Помните, что обычно программа завершается, как только возникает ошибка.

Однако Validation продолжает обрабатывать и накапливать ошибки, чтобы программа работала с ними как с пакетом.

Предположим, что мы регистрируем пользователей по name и age и хотим сначала взять все данные и решить, создавать ли экземпляр Person или возвращать список ошибок. Вот наш класс Person :

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

   //standard constructors, setters and getters, toString
}

Далее мы создаем класс с именем PersonValidator . Каждое поле будет проверено одним методом, а другой метод может быть использован для объединения всех результатов в один экземпляр Validation :

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);
    }
}

Правило для age состоит в том, что оно должно быть целым числом больше 0, а правило для name - в нем не должно быть специальных символов:

@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());
}
  • Допустимое значение содержится в экземпляре Validation.Valid , список ошибок проверки содержится в экземпляре Validation.Invalid ** . Таким образом, любой метод проверки должен возвращать один из двух.

Внутри Validation.Valid находится экземпляр Person , а внутри Validation.Invalid - список ошибок.

8. Ленивый

Lazy - это контейнер, который представляет значение, вычисленное лениво, т.е.

вычисления откладываются до тех пор, пока не потребуется результат. Кроме того, оцененное значение кэшируется или запоминается и возвращается снова и снова каждый раз, когда это необходимо, без повторения вычисления:

@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);
}

В приведенном выше примере мы оцениваем функцию Math.random .

Обратите внимание, что во второй строке мы проверяем значение и понимаем, что функция еще не была выполнена. Это потому, что мы до сих пор не проявили интереса к возвращаемому значению.

В третьей строке кода мы проявляем интерес к значению вычисления, вызывая Lazy.get . На этом этапе функция выполняется и Lazy.evaluated возвращает true.

Мы также продолжаем и подтверждаем бит памятки Lazy , пытаясь get значение снова. Если предоставленная нами функция будет выполнена снова, мы определенно получим другое случайное число.

Однако Lazy снова лениво возвращает первоначально вычисленное значение, поскольку окончательное утверждение подтверждает.

9. Сопоставление с образцом

Сопоставление с образцом является родной концепцией почти во всех функциональных языках программирования. На данный момент в Java такого нет.

Вместо этого, всякий раз, когда мы хотим выполнить вычисление или вернуть значение на основе полученного нами ввода, мы используем несколько операторов if , чтобы определить правильный код для выполнения:

@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);
}

Мы можем внезапно увидеть код, занимающий несколько строк, при этом проверяя только три случая. Каждая проверка занимает три строки кода. Что если бы нам нужно было проверить до сотни случаев, то это было бы около 300 строк, а не приятно!

Другой альтернативой является использование оператора switch :

@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);
}

Не лучше Мы по-прежнему в среднем 3 строки на проверку. Много путаницы и потенциальных ошибок. Забывание предложения break не является проблемой во время компиляции, но может привести к трудным для обнаружения ошибкам позже.

В Vavr мы заменяем весь блок switch методом Match .

Каждый оператор case или if заменяется вызовом метода Case .

Наконец, атомарные шаблоны, такие как $ () , заменяют условие, которое затем оценивает выражение или значение. Мы также предоставляем это как второй параметр Case :

@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);
}

Обратите внимание, насколько компактен код, усредняя только одну строку на проверку. API сопоставления с образцом намного мощнее, чем этот, и может делать более сложные вещи.

Например, мы можем заменить атомарные выражения предикатом.

Представьте, что мы разбираем консольную команду для флагов help и version :

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);
    }))
);

Некоторые пользователи могут быть более знакомы с сокращенной версией (-v), а другие - с полной версией (-version). Хороший дизайнер должен рассмотреть все эти случаи.

Без необходимости нескольких if операторов, мы позаботились о нескольких условиях. Мы узнаем больше о предикатах, множественных условиях и побочных эффектах при сопоставлении с образцом в отдельной статье.

10. Заключение

В этой статье мы представили Vavr, популярную библиотеку функционального программирования для Java 8. Мы рассмотрели основные функции, которые мы можем быстро адаптировать для улучшения нашего кода.

Полный исходный код этой статьи доступен в проекте Github .