Лямбда-выражения в Java 8

Java задняя часть

18 марта 2014 г. корпорация Oracle выпустилаJava SE 8. Прошло три года с момента выпуска Java 8. Недавно я просто нашел время, чтобы разобраться в особенностях Java 8 следующим образом:

  • метод интерфейса по умолчанию
  • Лямбда-выражения
  • функциональный интерфейс
  • Ссылки на методы и конструкторы
  • Лямбда-область
  • доступ к локальным переменным
  • Доступ к полям объекта и статическим переменным
  • Доступ к методу интерфейса по умолчанию
  • Date API
  • Аннотация

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

задний план

Java — это объектно-ориентированный язык программирования. И объектно-ориентированные языки программирования, и функциональные языки программирования имеют базовые элементы (Basic Values), которые могут динамически инкапсулировать поведение программы: объектно-ориентированные языки программирования используют объекты с методами для инкапсуляции поведения, а функциональные языки программирования используют функции для инкапсуляции поведения. Но это сходство неочевидно, потому что объекты Java имеют тенденцию быть «тяжеловесными»: создание экземпляра типа часто включает разные классы и требует инициализации полей и методов в классе.

Но некоторые объекты Java являются просто оболочками для одной функции. Например, следующий типичный вариант использования: интерфейс (обычно называемый интерфейсом обратного вызова) определяется в Java API, и пользователь передает заданное поведение, предоставляя экземпляр этого интерфейса.

public interface ActionListener {
  void actionPerformed(ActionEvent e);
}

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

button.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent e) {
    ui.dazzle(e.getModifiers());
  }
});

Многие библиотеки полагаются на приведенный выше шаблон. Это особенно верно для параллельных API, потому что нам нужно передать код для выполнения в параллельные API, а параллельное программирование — очень полезная область для изучения, потому что здесь возрождается закон Мура: хотя у нас нет более быстрых ядер ЦП ( ядер), но у нас больше процессорных ядер. Последовательный API может использовать только ограниченную вычислительную мощность.

анонимный внутренний класс

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

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

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

Несмотря на все ограничения и проблемы анонимных внутренних классов, у них есть приятная особенность, очень тесно интегрированная с системой типов Java: каждый объект функции соответствует типу интерфейса. Эта функция считается хорошей, потому что:

  • Интерфейсы являются частью системы типов Java.
  • Интерфейсы, естественно, имеют свое представление во время выполнения.
  • Интерфейсы могут выражать некоторый неформальный контракт через комментарии Javadoc, например, через комментарии о том, что операция должна быть коммутативной.

Интерфейс имеет только один метод, и большинство интерфейсов обратного вызова имеют эту функцию: например, интерфейс Runnable и интерфейс Comparator. Мы называем эти интерфейсы функциональными интерфейсами только с одним методом. (Ранее они назывались типами SAM, т.е. Single Abstract Method)

Нам не нужна дополнительная работа, чтобы объявить, что интерфейс является функциональным интерфейсом: компилятор будет судить о себе на основе структуры интерфейса (процесс оценки — это не просто подсчет методов интерфейса: интерфейс может избыточно определять методы, которые Объект уже предоставляет , например toString(), или определяет статические методы или методы по умолчанию, которые не относятся к категории методов функционального интерфейса). Однако авторы API могут явно указать, что интерфейс является функциональным интерфейсом с помощью аннотации @FunctionalInterface (чтобы избежать непреднамеренного объявления интерфейса, соответствующего функциональному стандарту), и после добавления этой аннотации компилятор проверит, что интерфейс удовлетворяет функции Требования к интерфейсу.

Другой способ реализации функциональных типов — введение совершенно нового типа структурированной функции, который мы также называем типом «стрелка». Например, тип функции, который принимает String и Object и возвращает целое число, может быть представлен как (String, Object) -> int. Мы внимательно рассмотрели этот подход, но в итоге отказались от него по следующим причинам:

  • Это вносит дополнительную сложность в систему типов Java и приносит смесь структурных типов и номинальных типов. (Java использует почти исключительно именованные типы)
  • Это приведет к расхождению в стилях библиотек — некоторые библиотеки продолжат использовать интерфейсы обратного вызова, в то время как другие будут использовать типы структурированных функций.
  • Его синтаксис может стать довольно громоздким, особенно после включения проверенных исключений.
  • Для каждого типа функции сложно иметь свое представление во время выполнения, а это означает, что разработчики страдают и ограничены стиранием типов. Например, мы не можем перегружать методы m(T->U) и m(X->Y)

Поэтому мы выбрали путь «использовать известный тип», потому что существующие библиотеки интенсивно используют функциональные интерфейсы, и, следуя этому шаблону, мы позволяем существующим библиотекам напрямую использовать лямбда-выражения. Например, вот функциональный интерфейс, который уже существует в Java SE 7:


java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener

Кроме того, в Java SE 8 был добавлен новый пакет: java.util.function, который содержит часто используемые функциональные интерфейсы, такие как:

Predicate<T>——接收 T 并返回 boolean
Consumer<T>——接收 T,不返回值
Function<T, R>——接收 T,返回 R
Supplier<T>——提供 T 对象(例如工厂),不接收值
UnaryOperator<T>——接收 T 对象,返回 T
BinaryOperator<T>——接收两个 T,返回 T

В дополнение к этим базовым функциональным интерфейсам, описанным выше, мы также предоставляем некоторые специализированные функциональные интерфейсы для типов-примитивов, таких как IntSupplier и LongBinaryOperator. (Мы предоставляем специализированные функциональные интерфейсы только для int, long и double, если вам нужно использовать другие примитивные типы, вам необходимо выполнить преобразование типов) Аналогично, мы также предоставляем некоторые функциональные интерфейсы для нескольких параметров, таких как BiFunction , который принимает объект T и объект U и возвращает объект R.

лямбда-выражение

Самая большая проблема с анонимными типами — их избыточный синтаксис. Некоторые люди шутят, что анонимные типы вызывают "большие проблемы". Лямбда-выражения — это анонимные методы, предоставляющие облегченный синтаксис, который решает «проблему высоты», создаваемую анонимными внутренними классами.

(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }

Первое лямбда-выражение принимает два целочисленных параметра x и y и возвращает их сумму; второе лямбда-выражение не принимает параметров и возвращает целое число «42»; третье лямбда-выражение принимает строку и суммирует ее. Выводит на консоль, не возвращает значения.

Синтаксис лямбда-выражения состоит из списка параметров, символа стрелки -> и тела функции. Тело функции может быть как выражением, так и блоком операторов:

  • Выражение: Выражение будет выполнено, и будет возвращен результат выполнения.
  • Блок операторов: операторы в блоке операторов будут выполняться последовательно, точно так же, как операторы в методе.
    • Оператор return передает управление вызывающей стороне анонимного метода.
    • break и continue можно использовать только в циклах
    • Если тело функции имеет возвращаемое значение, то каждый путь внутри тела функции должен возвращать значение.

Тело функции-выражения подходит для небольших лямбда-выражений, оно исключает ключевое слово return и делает синтаксис более лаконичным.

Лямбда-выражения также часто появляются во вложенных средах, таких как параметры методов. Чтобы в этих сценариях лямбда-выражения были максимально краткими, мы удалили ненужные разделители. Но в некоторых случаях мы также можем разделить его на несколько строк и заключить в круглые скобки, как и любое другое нормальное выражение.

Практическое применение

Function

Интерфейс Function принимает один параметр и возвращает результат с некоторыми методами по умолчанию, которые можно комбинировать с другими функциями:andThenиcompose.

@Test
public void testFun() {
    //Function 接口有一个参数并且返回一个结果
    Function<String, Integer> toInteger = (t) -> Integer.valueOf(t);
    System.out.println("compose: " + toInteger.andThen(a -> a + 10).compose(str -> str + "1").apply("123"));
    Function<String, String> backToString = toInteger.andThen(String::valueOf);
    Function<String, Integer> f = toInteger.compose(backToString);
    int str = f.apply("123");
    System.out.println(str);
}

composeиandThenФункции, определенные в compose, применяются в обратном порядке: методы в compose применяются первыми, а текущая функция — второй.

Supplier

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

@Test
public void testSupplier() {
    //Supplier 接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数
    Supplier sp = () -> "sp";
    System.out.println(sp.get());
}

Приведенный выше код вернет строку «sp»,getМетод получает возвращаемое значение.

Predicate

Интерфейс Predicate имеет только один параметр и возвращает логический тип. Интерфейс содержит несколько методов по умолчанию для объединения предикатов в другую сложную логику (например, И, ИЛИ, НЕ):

@Test
public void testPredicate() {
    Predicate<String> isEmpty = String::isEmpty;
    Predicate<String> isNotEmpty = isEmpty.negate();
    isEmpty.and(str -> str.equals("test"));
    System.out.println("tes: " + isEmpty.and(str -> str.equals("test")).test("tes"));
}

Приведенный выше код определяет, является ли строка пустой, и применяет операции И и НЕ.

Consumer

Интерфейс Consumer представляет собой операцию, выполняемую над одним параметром, основным методом являетсяandThenиaccept.

@Test
public void testConsumer() {
    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
    Consumer<String> greeter = (p) -> System.out.println("Hello, " + p);
    greeter.andThen((t) -> System.out.println("now is :" + df.format(new Date()))).accept("Skywalker");
}

acceptУказывает, что указанные параметры получены и операция выполнена.andThenУказывает на дополнительные операции после завершения текущей операции.

Comparator

Comparatorинтерфейс используется для сравнения, в Java 8 добавлены различные методы по умолчанию, такие какreversedиthenComparingЖдать.

@Test
public void testComparator() {
    Comparator<String> comparator = String::compareTo;
    String str1 = "eeeabc";
    String str2 = "bcd";
    System.out.println("str比较大小:" + comparator.compare(str1, str2));
    System.out.println("str比较大小反转:" + comparator.reversed().compare(str1, str2));
}

Optional

Вспомогательный тип для предотвращения исключений NullPointerException, теперь посмотрите, что может сделать этот интерфейс:

Optionalопределяется как простой контейнер, значение которого может быть или не быть нулевым. До Java 8 функция должна была возвращать объект, отличный от null, но иногда она может возвращать null.В Java 8 не рекомендуется возвращать null, а возвращать Optional.

@Test
public void testOptional() {
    //用来防止NullPointerException异常的辅助类型
    List<String> list = Arrays.asList("ab", "bc");
    System.out.println(list.stream().findFirst().orElse("null str"));
    Optional<String> optional = Optional.of("hello");
    optional.isPresent();           // true
    optional.get();                 // "hello"
    optional.orElse("hi");    // "hello"
    optional.ifPresent((s) -> System.out.println("字符串不为空:" + s));
}

optional.orElseОн используется для возврата предустановленного результата возврата в ненормальных условиях.

Stream

java.util.Stream представляет собой последовательность операций, которые можно применять к набору элементов, выполняемых одновременно. Потоковые операции делятся на промежуточные операции и конечные операции.Конечная операция возвращает определенный тип результата вычисления, а промежуточная операция возвращает сам поток, так что вы можете последовательно связать несколько операций. Для создания Stream необходимо указать источник данных, например подкласс java.util.Collection, List или Set, Map не поддерживает.

@Test
public void testSort() {
    List<String> list = Arrays.asList("abe", "abc");
    list = list.stream().filter(s -> s.startsWith("a")).sorted().collect(Collectors.toList());
    list.stream().forEach(System.out::println);
}

Map

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

@Test
public void testMap() {
    List<String> list = Arrays.asList("abe", "abc");
    //map返回的Stream类型是根据传递进去的函数的返回值决定
    list.stream().map(String::toCharArray).forEach(array -> System.out.println(array.length));
}

Match

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

@Test
public void testMatch() {
    List<String> list = Arrays.asList("ab", "abc");
    boolean anyMatch = list.stream().map(String::toCharArray).anyMatch(array -> array.length == 3);
    boolean allMatch = list.stream().map(String::toCharArray).allMatch(array -> array.length == 3);
    boolean noneMatch = list.stream().map(String::toCharArray).noneMatch(array -> array.length == 3);
    System.out.println("anyMatch:" + anyMatch);
    System.out.println("allMatch:" + allMatch);
    System.out.println("noneMatch:" + noneMatch);
}

Reduce

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

@Test
public void testReduce() {
    List<String> list = Arrays.asList("ab", "abc", "abcd");
    Optional<String> reduce = list.stream().reduce((s1, s2) -> s1 + ":" + s2);
    reduce.ifPresent(s -> System.out.println(s));
}

ParallelStream

Операции с последовательными потоками выполняются последовательно в одном потоке, а операции с параллельными потоками выполняются одновременно в нескольких потоках.

@Test
public void testParallelStream() {
    int max = 1000000;
    List<String> values = new ArrayList<>(max);
    for (int i = 0; i < max; i++) {
        UUID uuid = UUID.randomUUID();
        values.add(uuid.toString());
    }
    long t0 = System.nanoTime();
    long count = values.parallelStream().sorted().count();
    System.out.println(count);
    long t1 = System.nanoTime();
    long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
    System.out.println(String.format("sequential sort took: %d ms", millis));
}

Как показано выше для параллельной сортировки, сортировка этого потока занимает значительно меньше времени, чем последовательная.

Метод карты

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

@Test
public void testMapFun() {
    Map<Integer, String> map = new HashMap<>();
    for (int i = 0; i < 10; i++) {
        map.putIfAbsent(i, "val" + i);
    }
    map.forEach((id, val) -> System.out.println(val));
    map.computeIfPresent(3, (num, val) -> val + num);
    System.out.println(map.get(3));
    map.computeIfPresent(9, (num, val) -> null);
    System.out.println(map.containsKey(9));
    map.computeIfAbsent(23, num -> "val" + num);
    System.out.println(map.get(23));
    map.putIfAbsent(3, "bam");
    System.out.println(map.get(3));
    map.remove(3, "val3");
    System.out.println(map.get(3));
    //Merge时,如果键名不存在则插入,否则则对原键对应的值做合并操作并重新插入到map中
    map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
    System.out.println(map.get(9));
    map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
    System.out.println(map.get(9));
}

UnaryOperator

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

@Test
public void testUnaryOperator() {
    UnaryOperator<String> unaryOperator = str -> str + "-test";
    System.out.println(unaryOperator.apply("123"));
}

резюме

В этой статье в основном представлены лямбда-выражения в Java8 и выбраны часто используемые методы для простого объяснения приложения. Лямбда-выражения — важная новая функция в Java SE 8. Лямбда-выражения позволяют заменить функциональные интерфейсы выражениями. Лямбда-выражения, как и методы, предоставляют обычный список параметров и тело (тело, которое может быть выражением или блоком кода), которое использует эти параметры.
Лямбда-выражения также улучшают библиотеку коллекций, включая пакеты java.util.function и java.util.stream. Лямбда-выражения очень лаконичны, значительно упрощая количество строк кода, делая код в определенной степени лаконичным и чистым, но, опять же, это также может быть недостатком, поскольку слишком много вещей опущено, читабельность кода может быть снижена. ограничено в определенной степени.В меньшей степени все зависит от того, где вы используете лямбда-выражение для разработки API, знакомого другим читателям вашего кода.

Справочная документация

  1. Некоторые общие новые функциональные интерфейсы в Java 8
  2. Глубокое понимание Java 8 Lambda (глава о языке — лямбда-выражения, ссылки на методы, целевые типы и методы по умолчанию)