Рубрики
Без рубрики

Введение в различные

Краткое и практическое руководство по библиотеке Вавр.

Автор оригинала: baeldung.

1. Обзор

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

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

1.1. Зависимость Maven

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


    io.vavr
    vavr
    0.9.0

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

2. Опция

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

Option – это контейнер объектов в Var с аналогичной конечной целью, такой как Optional в Java 8. Опция Vavr реализует Сериализуемый, итеративный и имеет более богатый API .

Поскольку любая ссылка на объект в Java может иметь значение null , мы обычно должны проверять наличие 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 в конечном итоге вложены несколько раз.

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

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

@Test
public void givenValue_whenCreatesOption_thenCorrect() {
    Option noneOption = Option.of(null);
    Option someOption = Option.of("val");

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

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

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

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

@Test
public void givenNull_whenCreatesOption_thenCorrect() {
    String name = null;
    Option nameOption = Option.of(name);
   
    assertEquals("baeldung", nameOption.getOrElse("baeldung"));
}

Или ненулевое значение:

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

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

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

3. Кортеж

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

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

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

public void whenCreatesTuple_thenCorrect1() {
    Tuple2 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 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. Попробуйте

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

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

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

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

Без блоков try-catch приложение выйдет из строя. Чтобы избежать этого, вам нужно будет обернуть оператор в блок try-catch . С помощью Var мы можем обернуть тот же код в экземпляр Try и получить результат:

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

    assertTrue(result.isFailure());
}

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

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

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

    assertEquals(-1, errorSentinel);
}

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

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

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

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

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

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

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

    assertEquals(4, result);
}

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

@Test
public void givenJava8BiFunction_whenWorks_thenCorrect() {
    BiFunction 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 square = (num) -> num * num;
    int result = square.apply(2);

    assertEquals(4, result);
}

и это:

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

    assertEquals(12, result);
}

Когда нет параметра, но нам все еще нужен вывод, в Java 8 нам нужно будет использовать тип Supplier , в Var Функция 0 есть ли помощь:

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

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

Как насчет функции с пятью параметрами, это просто вопрос использования Функции 5 :

@Test
public void whenCreatesFunction_thenCorrect5() {
    Function5 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);
}

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

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

Мы можем создать из него функцию, подобную этой:

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

    assertEquals(11, summed);
}

6. Коллекции

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

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

interface Collection {
    void clear();
}

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

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

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

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

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

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

Чтобы создать список в Var:

@Test
public void whenCreatesVavrList_thenCorrect() {
    List 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);
}

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

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

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

7. Валидация

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

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

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

Учтите, что мы регистрируем пользователей по имени и возрасту , и мы хотим сначала принять все входные данные и решить, следует ли создавать экземпляр 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, Person> validatePerson(
      String name, int age) {
        return Validation.combine(
          validateName(name), validateAge(age)).ap(Person::new);
    }

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

    private Validation 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, Person> valid = 
      personValidator.validatePerson("John Doe", 30);

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

Допустимое значение содержится в проверке .Допустимый экземпляр, список ошибок проверки содержится в Проверке.Недопустимый экземпляр . Таким образом, любой метод проверки должен возвращать один из двух.

Внутри Проверка.Допустимый – это экземпляр Person , находящийся внутри Проверки.Недопустимый – это список ошибок.

8. Ленивый

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

@Test
public void givenFunction_whenEvaluatesWithLazy_thenCorrect() {
    Lazy 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 , пытаясь получить значение снова. Если бы предоставленная нами функция была выполнена снова, мы определенно получили бы другое случайное число.

Однако 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 не является проблемой во время компиляции, но может привести к труднодоступным ошибкам позже.

В Var мы заменяем весь блок 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 сопоставления шаблонов намного мощнее этого и может выполнять более сложные задачи.

Например, мы можем заменить атомарные выражения предикатом. Представьте, что мы анализируем консольную команду для справки и версии флагов:

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. Заключение

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

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