VAVR: измените свой опыт работы с Java

Java
VAVR: измените свой опыт работы с Java

Где свято?

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

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

VAVRНеизвестно, его предшественником был Javaslang, выпущенный в 2014 году, и в настоящее время он имеет почти 4 тысячи звезд на github.

Увидев это, многие люди сказали, что я был хедлайнером вечеринки.Библиотека Java также собирается ниспровергнуть Java?

это не я играюударное тело, откройте VAVRОфициальный сайт, его первая страница гласит: «vavr - turns javaс ног на голову

Разве это не означает подрывную деятельность Java?

Съедобные рекомендации

Для чтения этой статьи читатели должны иметь определенное представление о синтаксисе лямбда-выражений Java8 и общих API.

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

  • Не исчерпаны все функции и API
  • не буду вдаваться в детали источника

Что касается примера кода, он в основном будет представлен в виде модульных тестов и гарантированно пройдет

Примечание. В этой статье используется версия VAVR 0.10.3, а версия JDK — 11.

Давайте сделаем обзор

Коллекция, новый старт

Должен сказать, что библиотека коллекций Java8 действительно полезна после введения Stream, но именно из-за использования Stream мне приходится писать много шаблонного кода, что снижает массу опыта.

// of 方法是 Java9 开始提供的静态工厂
java.util.List.of(1, 2, 3, 4, 5)
                .stream()
                .filter(i -> i > 3)
                .map(i -> i * 2)
                .collect(Collectors.toList());

Более того, сама библиотека-коллекция Java является изменяемой, что явно нарушает основную черту функционального программирования - неизменяемость.По этой причине VAVR разработал новый набор библиотек-коллекций, а опыт использования бесконечно близок к Scala.

Более чистый API

io.vavr.collection.List.of(1, 2, 3, 4, 5)
    .filter(i -> i > 3)
    .map(i -> i * 2);

Добавление данных в коллекцию приводит к созданию новой коллекции, которая гарантированно будет неизменной.

var list = io.vavr.collection.List.of(1, 2)
var list2 = list
  .append(List.of(3, 4))
  .append(List.of(5, 6))
  .append(7);
// list = [1, 2]
// list2 = [1, 2, 3, 4, 5, 6]

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

var javaList = java.util.List.of(1, 2, 3);
java.util.List<Integer> javaList2 = io.vavr.collection.List.ofAll(javaList)
    .filter(i -> i > 1)
    .map(i -> i * 2)
    .toJavaList();

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

/**
* 用户信息
*/
@Data
class User {
  private Long id;
  private String name;
  private Integer age;
}

Сначала используйте стандартную библиотеку коллекций Java для выполнения этого требования, вы можете увидетьcollect(...)Этот длинный список вложений действительно неудобен

public Map<Integer, List<String>> userStatistic(List<User> users) {
    return users.stream()
      .filter(u -> u.getAge() >= 18)
      .collect(Collectors.groupingBy(User::getAge, Collectors.mapping(User::getName, Collectors.toList())));
}

Давайте посмотрим на реализацию VAVR, она проще и понятнее?

public Map<Integer, List<String>> userStatistic(List<User> users) {
    return users.filter(u -> u.getAge() >= 18)
      .groupBy(User::getAge)
      .mapValues(usersGroup -> usersGroup.map(User::getName));
}

Библиотека коллекций VAVR предоставляет более функциональные API, такие как

  • take(Integer) принимает первые n значений
  • tail() берет коллекцию, кроме головного узла
  • zipWithIndex() упрощает получение индекса (без fori)
  • find(Predicate) основан на условном значении запроса, что может быть достигнуто только с помощью filter + findFirst в стандартной библиотеке Java.
  • .....

Хотя во всех примерах кода используется список, перечисленные выше функции можно использовать в Queue, Set и Map, и все они поддерживают преобразование со стандартной библиотекой Java.

Кортежи, недостающая конструкция Java

Студенты, знакомые с Haskell и Scala, должны быть знакомы со структурой данных «кортеж».

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

// scala 的元组,用括号构建
val tup = (1, "ok", true)

// 按索引取值,执行对应类型的操作
val sum = tup._1 + 2  // int 加法
val world = "hello "+tup._2 // 字符串拼接
val res = !tup._3 // 布尔取反

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

Однако VAVR реализует кортежи через дженерики, черезTupleСтатическая фабрика, мы можем очень легко создавать кортежи (с синтаксисом var Java10 не слишком красиво)

import io.vavr.Tuple;

public TupleTest {
  
    @Test
    public void testTuple() {
        // 一元组
        var oneTuple = Tuple.of("string");
        String oneTuple_1 = oneTuple._1;

        // 二元组
        var twoTuple = Tuple.of("string", 1);
        String twoTuple_1 = twoTuple._1;
        Integer twoTuple_2 = twoTuple._2;

      	// 五元组
        var threeTuple = Tuple.of("string", 2, 1.2F, 2.4D, 'c');
        String threeTuple_1 = threeTuple._1;
        Integer threeTuple_2 = threeTuple._2;
        Float threeTuple_3 = threeTuple._3;
        Double threeTuple_4 = threeTuple._4;
        Character threeTuple_5 = threeTuple._5;
    }
}

если нетvar, вы должны написать следующее подробное определение переменной

Tuple5<String, Integer, Float, Double, Character> tuple5 = Tuple.of("string", 2, 1.2F, 2.4D, 'c');

В настоящее время VAVR поддерживает создание октуплетов максимум, то есть до 8 типов, а не до 8 значений.

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

PS: хоть и рано говорить о сопоставлении с образцом (мы еще встретимся позже), мы все же можем почувствовать его заранее.

var tup = Tuple.of("hello", 1);

// 模式匹配
Match(tup).of(
   Case($Tuple2($(is("hello")), $(is(1))), (t1, t2) -> run(() -> {})),
   Case($Tuple2($(), $()),(t1, t2) ->run(() -> {}))
);

Приведенный выше код фактически эквивалентен if...else

// 等同于 if...else
if (tup._1.equalas("hello")  && tup._2 == 1) {
  // ...  do something
} else {
  // ... do something
}

В дополнение к Option есть Try, Choose, Future...

Java8 представила необязательный вариант для решения печально известного исключения NullPointerException, и у VAVR есть аналогичный инструмент —Option, но у него другой дизайн.

В дополнение к Option, VAVR также реализует функциональные структуры, такие как Try, Someone, Future и т. д., которые являются очень мощными инструментами, недоступными в стандартной библиотеке Java.

Option

Optionсо стандартной библиотекой JavaOptionalОчень похожи, оба представляют собой необязательное значение, но их дизайн очень отличается. (Дизайн VAVR Option ближе к Scala)

В ВАВР,Optionэто интерфейс, конкретными реализациями которого являются Some и None

  • Some: представляет значение
  • Нет: означает отсутствие значения

Вы можете проверить с помощью следующего модульного теста

@Test
public void testOption() {
  	// 通过 of 工厂方法构造
    Assert.assertTrue(Option.of(null) instanceof Option.None);
    Assert.assertTrue(Option.of(1) instanceof Option.Some);
    
  	// 通过 none 或 some 构造
    Assert.assertTrue(Option.none() instanceof Option.Some);
    Assert.assertTrue(Option.some(1) instanceof Option.Some);
}

И дляjava.util.OptionalДругими словами, независимо от того, как он построен, это один и тот же тип.

@Test
public void testOptional() {
    Assert.assertTrue(Optional.ofNullable(null) instanceof Optional);
    Assert.assertTrue(Optional.of(1) instanceof Optional);
    Assert.assertTrue(Optional.empty() instanceof Optional);
    Assert.assertTrue(Optional.ofNullable(1) instanceof Optional);
}

Почему между ними такая разница в дизайне?

По сути, это другой ответ на вопрос «Является ли роль Option безопасным вычисление нуля?».

Следующие два метода тестирования с одинаковой логикой используют Option и Optional, но дают разные результаты.

@Test
public void testWithJavaOptional() {
    // Java Optional
    var result = Optional.of("hello")
      .map(str -> (String) null)
      .orElseGet(() -> "world");

  	// result = "world"
    Assert.assertEquals("word", result);
}

@Test
public void testWithVavrOption() {
  	// Vavr Option
    var result = Option.of("hello")
      .map(str -> (String) null)
      .getOrElse(() -> "world");
  
  	// result = null
    Assert.assertNull(result);
}

В тестовом коде VAVR передайтеOptional.of("hello")на самом деле получилSome("hello")объект.

тогда позвониmap(str -> (String)null)вернулся по-прежнемуSomeобъект (Some представляет значение), поэтому окончательный результат = null, а неgetOrElse(() -> "world")Возвращаемая мировая строка.

В тестовом коде на Java вызовитеmap(str -> null), Необязательный был переключен на Необязательный.пустой, поэтому он, наконец, возвращаетorElseGet(() -> "world")результат.

Это также тот момент, когда функциональные разработчики критикуют дизайн java.util.Optional.

Помимо разницы в дизайне, io.vavr.control.Option имеет более дружественные API, чем java.util.Optional.


@Test
public void testVavrOption() {
  	// option 直接转为 List
    List<String> result = Option.of("vavr hello world")
      .map(String::toUpperCase)
      .toJavaList();
    Assert.assertNotNull(result);
    Assert.assertEquals(1, result.size());
    Assert.assertEquals("vavr hello world", result.iterator().next());
  
  	// exists(Function)
    boolean exists = Option.of("ok").exists(str -> str.equals("ok"));
    Assert.assertTrue(exists);

  	// contains
    boolean contains = Option.of("ok").contains("ok");
    Assert.assertTrue(contains);
}

Учитывая совместимость со стандартной библиотекой, Option можно легко конвертировать в и обратно.

Option.of("toJava").toJavaOptional();

Option.ofOptional(Optional.empty());

Try

Try похож на Option, а также похож на «контейнер», за исключением того, что он содержитвозможно неправильное поведение, вы сразу подумали о конструкции try..catch?

try {
	//..
} catch (Throwable t) {
	//...
} finally {
  //....
}

С помощью Try VAVR также можно реализовать еще один более функциональный try...catch.

/**
* 输出
*	 failure: / by zero
*  finally
*/
Try.of(() -> 1 / 0)
    .andThen(r -> System.out.println("and then " + r))
    .onFailure(error -> System.out.println("failure" + error.getMessage()))
    .andFinally(() -> {
      System.out.println("finally");
    });

Try — это тоже интерфейс, конкретная реализация — это Success или Failure.

  • Успех: представляет выполнение без исключения
  • Ошибка: представляет собой исключение из выполнения

Как и Optoins, он также может быть построен с помощью фабричного метода.

@Test
public void testTryInstance() {
  	// 除以 0 ,构建出 Failure
    var error = Try.of(() -> 0 / 0);
    Assert.assertTrue(error instanceof Try.Failure);

  	// 合法的加法,构建出 Success
    var normal = Try.of(() -> 1 + 1);
    Assert.assertTrue(normal instanceof Try.Success);
}

С помощью метода recoveryWith метода Try мы можем элегантно реализовать стратегию перехода на более раннюю версию.

@Test
public void testTryWithRecover() {
    Assert.assertEquals("NPE", testTryWithRecover(new NullPointerException()));
    Assert.assertEquals("IllegalState", testTryWithRecover(new IllegalStateException()));
    Assert.assertEquals("Unknown", testTryWithRecover(new RuntimeException()));
}

private String testTryWithRecover(Exception e) {
    return (String) Try.of(() -> {
      throw e;
    })
      .recoverWith(NullPointerException.class, Try.of(() -> "NPE"))
      .recoverWith(IllegalStateException.class, Try.of(() -> "IllegalState"))
      .recoverWith(RuntimeException.class, Try.of(() -> "Unknown"))
      .get();
}

Для результата расчета Try его можно преобразовать по карте или легко преобразовать с помощью Option.

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

@Test
public void testTryMap() {
    String res = Try.of(() -> "hello world")
      .map(String::toUpperCase)
      .toOption()
      .getOrElse(() -> "default");
    Assert.assertEquals("HELLO WORLD", res);
}

Future

Это будущее неjava.util.concurrent.Future, но оба являются абстракцией над результатом асинхронного вычисления.

ваврFutureЛучше чемjava.util.concurrent.FutureБолее удобный механизм обратного вызова

  • onFailure Неудачный обратный вызов
  • Обратный вызов успеха onSuccess
@Test
public void testFutureFailure() {
    final var word = "hello world";
    io.vavr.concurrent.Future
      .of(Executors.newFixedThreadPool(1), () -> word)
      .onFailure(throwable -> Assert.fail("不应该走到 failure 分支"))
      .onSuccess(result -> Assert.assertEquals(word, result));
}

@Test
public void testFutureSuccess() {
    io.vavr.concurrent.Future
      .of(Executors.newFixedThreadPool(1), () -> {
          throw new RuntimeException();
      })
      .onFailure(throwable -> Assert.assertTrue(throwable instanceof RuntimeException))
      .onSuccess(result -> Assert.fail("不应该走到 success 分支"));
}

Его также можно конвертировать с Java CompleableFuture.

Future.of(Executors.newFixedThreadPool(1), () -> "toJava").toCompletableFuture();

Future.fromCompletableFuture(CompletableFuture.runAsync(() -> {}));

разное

Наконец, давайте кратко поговорим об ином, и ленивом.

  • Либо Указывает, что значение может быть одного из двух типов, например:compute()Либо возвращаемое значение функции представляет собой структуру, которая может быть Exception или String.

    Обычно право используется для представления правильного значения (право в английском языке имеет правильное значение)

    public Either<Exception, String> compute() {
      //...
    }
    
    public void test() {
    	Either<Exception, String> either = compute();
      
      // 异常值
      if (either.isLeft()) {
        Exception exception = compute().getLeft();
        throw new RuntimeException(exception);
      }
    
      // 正确值
      if (either.isRight()) {
        String result = compute().get();
        // ...
      }
    }
    
  • Ленивый также является контейнером, он может задерживать вычисление до тех пор, пока вычисление не будет вызвано в первый раз, после первого вызова результат будет кэширован, а последующие вызовы могут получить результат напрямую.

    Lazy<Double> lazy = Lazy.of(Math::random);
      lazy.isEvaluated(); // = false
      lazy.get();         // = 0.123 (random generated)
      lazy.isEvaluated(); // = true
      lazy.get();         // = 0.123 (memoized)
    

существуетio.vavr.APIВ Scala предусмотрено множество статических методов для имитации синтаксических конструкций Scala Option, Try и этих структур, но их следует использовать в сочетании со статическим импортом Java.

import static io.vavr.API.*;

@Test
public void testAPI() {
  // 构造 Option
  var some = Some(1);
  var none = None();

  // 构造 Future
  var future = Future(() -> "ok");

  // 构造 Try
  var tryInit = Try(() -> "ok");
}

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

Для получения более подробной информации желающие могут перейти наОфициальная документация сайтаучиться

Сопоставление с образцом: возмездие if..else

Образец здесь относится кСхема композиции структуры данных, в Scala вы можете использовать сопоставление с образцом напрямую через ключевое слово match

def testPatternMatch(nameOpt: Option[String], nums: List[Int]) = {
	/**
	* 匹配 Option 的结构
	*/
  nameOpt match {
    case Some(name) => println(s"你好,$name")
    case None => println("无名之辈")
  }

  /**
  * 匹配 List 的结构
  */
  nums match {
    case Nil => println("空列表")
    case List(v) => println(s"size=1 $v")
    case List(v, v2) => println(s"size=2 $v、 $v2")
    case _ => println("size > 2")
  }
}

В Java нет понятия сопоставления с образцом и, естественно, нет связанного с ним синтаксиса (переключатель не в счет).

Однако VAVR использует ООП для реализации сопоставления с образцом, хотя это и не сравнимо с собственным опытом Scala, но весьма близко.

Java вJEP 375: Pattern Matching for instanceofВ предложении реализована функция сопоставления с образцом для instanceof (ожидается, что она будет выпущена в Java15), но я думаю, что эта функция все еще далека от сопоставления с образцом в Scala.

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

public String bmiFormat(double height, double weight) {
  double bmi = weight / (height * height);
  String desc;
  if (bmi < 18.5) {
    desc = "有些许晃荡!";
  } else if (bmi < 25) {
    desc = "继续加油哦!";
  } else if (bmi < 30) {
    desc = "你是真的稳!";
  } else {
    desc = "难受!";
  }
  return desc;
}

Давайте проведем рефакторинг с сопоставлением шаблонов VAVR и удалим их if..else.

Чтобы сделать грамматику более удобной, лучше пройтиstatic importИмпорт API.

import static io.vavr.API.*;

Ниже приведен фрагмент кода с рефакторингом

public String bmiFormat(double height, double weight) {
  double bmi = weight / (height * height);
  return Match(bmi).of(
    // else if (bmi < 18.5)
    Case($(v -> v < 18.5), () -> "有些许晃荡!"),
    // else if (bmi < 25)
    Case($(v -> v < 25), () -> "继续加油哦!"),
    // else if (bmi < 30)
    Case($(v -> v < 30), () -> "你是真的稳!"),
    // else
    Case($(), () -> "难受!")
  );

}
  • Match(...), Case(...), $(...) всеio.vavr.APIСтатический метод для имитации синтаксиса «сопоставления с образцом».

  • Последний $() означает соответствие всем случаям, кроме приведенного выше.

Чтобы облегчить понимание читателей, я кратко перечислил сигнатуры каждого метода (методы Case и $ имеют много перегрузок, поэтому я не буду перечислять их все)

public static <T> Match<T> Match(T value) {...}

public static <T, R> Case<T, R> Case(Pattern0<T> pattern, Function<? super T, ? extends R> f) {...}

public static <T> Pattern0<T> $(Predicate<? super T> predicate) {...}

ofэто метод объекта Match

public final <R> R of(Case<? extends T, ? extends R>... cases) {...}

Давай, покажи мне свою собственную грамматическую память

匹配一下(这个东西)的结构,是不是下面的情况之一
// Match(XXX).Of(

  - 结构和 A 一样,做点什么事情
  //Case(  $(A),   () -> doSomethingA()   ),
  
  - 结构和 B 一样,做点什么事情
  //Case(  $(B),   () -> doSomethingB()   ),
  - .....
  
  - 和上面的结构都不一样,也做点事情
  //Case(  $(),    () -> doSomethingOthers())
  //);

Когда сопоставление с образцом сочетается с вышеупомянутым Option, Try, Someone, Tuple, это комбинация 1 + 1 > 3.

В следующем коде показано, как «сопоставление с образцом» делает Option еще более мощным.

import static io.vavr.API.*;
import static io.vavr.Patterns.$None;
import static io.vavr.Patterns.$Some;

public class PatternMatchTest {
  
    @Test
    public void testMatchNone() {
      // 匹配 None
        var noneOpt = Option.none();
        Match(noneOpt).of(
                Case($None(), r -> {
                    Assert.assertEquals(Option.none(), r);
                    return true;
                }),
                Case($(), this::failed)
        );
    }

    @Test
    public void testMatchValue() {
      // 匹配某一个值为 Nice 的 Some
        var opt2 = Option.of("Nice");
        Match(opt2).of(
                Case($Some($("Nice")), r -> {
                    Assert.assertEquals("Nice", r);
                    return true;
                }),
                Case($(), this::failed)
        );
    }

    @Test
    public void testMatchAnySome() {
      // 匹配 Some,值任意
        var opt = Option.of("hello world");
        Match(opt).of(
                Case($None(), this::failed),
                Case($Some($()), r -> {
                    Assert.assertEquals("hello world", r);
                    return true;
                })
        );
    }

    private boolean failed() {
        Assert.fail("不应该执行该分支");
        return false;
    }
}    

Есть еще Try, кстати, иногда, когда Case не имеет возвращаемого значения, второй параметр можно заменить на API.run()

import static io.vavr.API.*;
import static io.vavr.Patterns.*;
import static io.vavr.Predicates.instanceOf;

public class PatternMatchTest {

    @Test
    public void testMatchFailure() {
        var res = Try.of(() -> {
            throw new RuntimeException();
        });
        Match(res).of(
          // 匹配成功情况
                Case($Success($()), r -> run(Assert::fail)),
          // 匹配异常为 RuntimeException
                Case($Failure($(instanceOf(RuntimeException.class))), r -> true),
          // 匹配异常为 IllegalStateException
                Case($Failure($(instanceOf(IllegalStateException.class))), r -> run(Assert::fail)),
          // 匹配异常为 NullPointerException
                Case($Failure($(instanceOf(NullPointerException.class))), r -> run(Assert::fail)),
          // 匹配其余失败的情况
                Case($Failure($()), r -> run(Assert::fail))
        );
    }

    @Test
    public void testMatchSuccess() {
        var res = Try.of(() -> "Nice");
        Match(res).of(
          // 匹配任意成功的情况
                Case($Success($()), r -> run(() -> Assert.assertEquals("Nice", r))),
          // 匹配任意失败的情况
                Case($Failure($()), r -> run(Assert::fail))
        );
    }
}

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

Наконец

В этой статье представлены только некоторые часто используемые функции. Кроме того, VAVR также поддерживает расширенные функции, такие как Curring, Memoization и Partial application.Если вы хотите узнать больше, вы можете перейти на официальный сайт, чтобы узнать больше.

Наконец, этот кирпич был выброшен, может ли он привести к вашему нефриту?

рекламировать:

Если вы ищете проект на основе Java9+ для изучения новых функций, я рекомендую его.PrettyZoo,

Это настольный клиент zookeeper, разработанный на основе Java11, с использованием модуляризации, var и многих других новых функций, добро пожаловать, чтобы пометить, разветвить, выпустить.