JavaRush /Java блог /Архив info.javarush /Java 8. Руководство. 1 часть.
ramhead
13 уровень

Java 8. Руководство. 1 часть.

Статья из группы Архив info.javarush

"Java еще жива - и люди начинают понимать это."

Добро пожаловать в мое введение по Java 8. Это руководство проведет вас шаг за шагом, по всем новым возможностям языка. Опираясь на короткие и простые примеры кода, вы узнаете как использовать default-методы интерфейсов, лямбда выражения, ссылочные методы и повторяемые аннотации. К концу статьи вы будете знакомы с последними изменениями в API, таких как потоки, функциональные интерфейсы, расширения ассоциаций и нового Date API. Никаких стен из скучного текста - только куча прокомментированных фрагментов кода. Наслаждайтесь!

Default-методы для интерфейсов

Java 8 позволяет нам добавлять не-абстрактные методы реализованные в интерфейсе благодаря использованию ключевого слова default. Эта возможность также известна как методы расширения. Вот наш первый пример: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } Помимо абстрактного метода calculate, интерфейс Formula также определяет default-метод sqrt. Классы реализующие интерфейс Formula, реализуют только абстрактный метод calculate. Default-метод sqrt может быть использован прямо из "коробки". Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 Объект formula реализован как анонимный объект. Код достаточно внушительный: 6 строк кода для простого вычисления sqrt(a * 100). Как мы увидим далее в следующем разделе, есть более привлекательный путь реализации объектов с единственным методом в Java 8.

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

Начнем с простого примера, как отсортировать массив строк в ранних версиях Java: List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator() { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Статистический вспомогательный метод Collections.sort принимает список и компаратор(Comparator), чтобы отсортировать элементы данного списка. Часто случается, что вы создаете анонимные компараторы и передаете их в методы сортировки. Вместо создания анонимных объектов на протяжении всего времени, Java 8 дарит возможность использовать гораздо меньший объем синтаксиса, лямбда выражения: Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Как вы можете заметить, код гораздо короче и проще для чтения. Но вот он становится еще короче: Collections.sort(names, (String a, String b) -> b.compareTo(a)); Для метода-в-одну-линию вы можете избавиться от фигурных скобок {} и ключевого слова return. Но вот код становится еще более короче: Collections.sort(names, (a, b) -> b.compareTo(a)); Java компилятор осведомлен о типах параметров, так что вы можете также не указывать и их. Теперь давайте погрузимся глубже в то, как лямбда выражения могут быть использованы в реалии.

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

Как лямбда выражения вписываются в систему типов Java? Каждая лямбда соответствует заданному типу, определенному с помощью интерфейса. А так называемый функциональный интерфейс должен содержать ровно один объявленный абстрактный метод. Каждое лямбда выражение данного типа, будет соответствовать этому абстрактному методу Поскольку default-метод не являются абстрактными методами, вы вольны добавлять default-методы к своему функциональному интерфейсу. Мы можем использовать произвольный интерфейс как лямбда выражение, при условии что этот интерфейс содержит только один абстрактный метод. Дабы гарантировать, что ваш интерфейс удовлетворяет таким условиям, вы должны добавить аннотацию @FunctionalInterface. Компилятор будет осведомлен данной аннотацией, что интерфейс должен содержать только один метод и в случае обнаружения второго абстрактного метода в данном интерфейсе, выдаст ошибку. Пример: @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123 Имейте ввиду, что данный код также допустим, даже если бы аннотация @FunctionalInterface не была объявлена.

Ссылки на методы и конструкторы

Пример выше в дальнейшем может быть упрощен с помощью ссылки на статистический метод: Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 Java 8 позволяет передавать ссылки на методы и конструкторы с помощью ключевых символов ::. Приведенный выше пример показывает как можно обращаться к статистическим методам. Но мы также можем ссылаться на методы объектов: class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" Давайте взглянем, как использование :: работает для конструкторов. Для начала определим пример с различными конструкторами: class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } Далее мы определяем интерфейс фабрики PersonFactory, для создания новых объектов person: interface PersonFactory

{ P create(String firstName, String lastName); } Вместо реализации фабрики вручную, мы объединяем все вместе с помощью ссылки на конструктор: PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker"); Мы создаем ссылку на конструктор класса Person через Person::new. Компилятор Java автоматически вызовет подходящий конструктор, сравнивая сигнатуру конструкторов с сигнатурой метода PersonFactory.create.

Лямбда области

Организация доступа к переменным внешней области видимости из лямбда выражений, подобна доступу из анонимного объекта. Вы можете получить доступ к переменным с модификатором final из локальной области видимости, также как к полям экземпляров и статистическим переменным.
Доступ к локальным переменным
Мы можем прочитать локальную переменную с модификатором final из области видимости лямбда выражения: final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 Но в отличии от анонимных объектов, для обеспечения доступа из лямбда выражения к переменным, они не обязательно должны быть продекларированы с помощью final. Этот код также является верным: int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 Однако переменная num должна оставаться неизменяемой, т.е. быть неявной final, для компиляции кода. Следующий код не будет компилироваться: int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3; Изменения num внутри лямбда выражения также недопустимы.
Доступ к полям экземпляров и статистическим переменным
В отличии от локальных переменных, поля экземпляров и статистические переменные мы можем считать и изменять внутри лямбда выражений. Такое поведение известно нам от анонимных объектов. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
Доступ к default-методам интерфейсов
Помните пример с экземпляром formula из первого раздела? Интерфейс Formula определяет default-метод sqrt, который может быть доступен из каждого экземпляра formula включая анонимные объекты. Это не работает с лямбда выражениями. Default-метод не могут быть доступны внутри лямбда выражений. Следующий код, не компилируется: Formula formula = (a) -> sqrt( a * 100);

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

API JDK 1.8 содержит множество встроенных функциональных интерфейсов. Некоторые из них хорошо известны из предыдущих версий Java. Например Comparator или Runnable. Эти интерфейсы расширены, дабы включить поддержку "лямбды" с помощью аннотации @FunctionalInterface. Но API Java 8 также полон и новыми функциональными интерфейсами, что cделают вашу жизнь легче. Некоторые из этих интерфейсов хорошо известны из библиотеки Google Guava. Даже если вы знакомы с этой библиотекой, вы должны внимательно приглядеться как эти интерфейсы расширены, с помощью некоторых полезных методов расширения.
Predicates
Predicates это булевые функции с одним аргументом. Интерфейс содержит различные default-методы, для создание с помощью предикатов, сложных логических выражений(and, or, negate) Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
Functions
Functions принимают один аргумент и выдают результат. Default-методы могут использоваться для объединения нескольких функции вместе, в одну цепочку(compose, andThen). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Suppliers
Suppliers возвращают результат(экземпляр) того или иного типа. В отличии от функций, поставщики не принимают аргументов. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Consumers
Consumers олицетворяют методы интерфейса с единственным аргументом. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Comparators
Comparators известны нам из предыдущих версий Java. Java 8 позволяет добавлять различные дефолтные методы в интерфейсы. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Optionals
Интерфейс Optionals не является функциональным, но это отличная утилита для предотвращения NullPointerException. Это важный момент для следующего раздела, так что давайте быстро взглянем как работает данный интерфейс. Интерфейс Optional это простой контейнер для значений который могут быть null или не-null. Представьте что метод, может возвращать значение или ничего. В Java 8, вместо возврата null, вы возвращаете экземпляр Optional. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

Stream

java.util.Stream представляет из себя последовательность элементов над которыми выполняется одна или множество операций. Каждая операция Stream является либо промежуточной, либо терминальной. Терминальные операции возвращают результат определенного типа, в то время как промежуточные операции возвращают сам объект stream, что позволяет создавать цепочку вызовов методов. Stream представляет собой интерфейс, подобно java.util.Collection для lists и sets(maps не поддерживаются).Каждая операция Stream может выполнятся либо последовательно, либо параллельно. Давайте взглянем как работает stream. Первое, мы создадим пример кода в форме списка strings: List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1"); Коллекции в Java 8 расширены так, что вы можете достаточно просто создать streams вызовом Collection.stream() или Collection.parallelStream(). Следующий раздел разъяснит самые важные, простые stream операции.
Filter
Filter принимает предикаты для фильтрации всех элементов stream. Эта операция является промежуточной, что позволяет нам вызывать другие stream операции(например forEach) для полученного результата(отфильтрованного). ForEach принимает операцию, которая будет выполнена для каждого элемента уже отфильтрованного stream. ForEach является терминальной операцией. Далее, вызов других оперций невозможен. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Sorted
Sorted является промежуточной операцией, которая возвращает сортированное представление stream. Элементы сортируются в правильном порядке, если вы не укажите свой Comparator. stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Имейте в виду, что sorted создает сортированное представление stream не влияя на саму коллекцию. Порядок элементов stringCollection остается нетронутым: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map
Промежуточная операция map, конвертирует каждый элемент в другой объект с помощью полученной функции. Следующий пример конвертирует каждую строку в строку в верхнем регистре. Но вы также можете использовать map для преобразования каждого объекта в другой тип. Тип объектов резльутирующего stream зависит от типа функции, которую вы передаете в map. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match
Различные операции соответствия могут быть использованы для проверки истинности определенного предиката в отношении stream. Все match операции являются терминальными и возвращают булевый результат. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Count
Count является терминальной операции, возвращающая количество элементов stream как long. long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reduce
Это терминальная операция, выполняющая сокращение элементов stream с помощью переданной функцией. Результатом будет Optional содержащий сокращенное значение. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Parallel Streams

Как уже упоминалось выше, stream бывают последовательными и параллельныеми. Операции последовательного stream выполняются в последовательном потоке, в то время как операции параллельного stream выполняются на множестве параллельных потоках. Следующий пример демонстрирует как легко увеличить производительность испоьльзуя параллельный stream. Для начала создадим большой список уникальных элементов: int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); } Сейчас мы определим время затраченное на сортировку stream данной коллекции.
Последовательный stream
long t0 = System.nanoTime(); long count = values.stream().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)); // sequential sort took: 899 ms
Параллельный stream
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("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms Как вы можете заметить, оба фрагмента когда почти идентичны, но параллельная сортировка выполняется на 50% быстрее. Все что вам нужно, это изменить stream() на parallelStream().

Map

Как уже было упомянуто, map'ы не поддерживают stream'ы. Вместо этого map стал поддерживать новые и полезные методы для решения обычных задач. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); Код выше должен быть интуитивно-понятен: putIfAbsent предостерегает нас от написания дополнительных проверок на null. forEach принимает функцию на выполнение для каждого из значений map. Этот пример показывает, как выполняются операции на значениях map, используя функции: map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 Далее мы узнаем как удалить запись для данного ключа, только если он сопоставляется заданному значению: map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null Другой хороший метод: map.getOrDefault(42, "not found"); // not found Вполне легко производится слияние записей map: map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat Слияние либо вставит ключ/значение в map, если для данного ключа отсутствует запись, либо будет вызвана функция слияния, которая изменит значение существующей записи.
Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Damir Juldashev Уровень 8
10 июня 2022

Вместо реализации фабрики вручную, мы объединяем все вместе с помощью ссылки на конструктор:
PersonFactory personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
Может я что то не так делал, Но про ссылку на конструктор у меня не сработало, пока тип возращаемого метода create "P" не заменил на "Person"

interface PersonFactory
 {
    P create(String firstName, String lastName);
}
Артём Уровень 26
24 октября 2019
"Интерфейс Optionals не является функциональным..." Optionals это же вроде как абстрактный класс? Или я что-то неправильно понял?
Riccio Уровень 35 Master
2 октября 2019
А где вторая часть?