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, знакомого другим читателям вашего кода.