JavaRush /Java блог /Random UA /Java 8. Посібник. 1 частина.
ramhead
13 рівень

Java 8. Посібник. 1 частина.

Стаття з групи Random UA

"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: Статистичний допоміжний метод Collections.sort приймає список та компаратор ( Comparator ), щоб відсортувати елементи даного списку. Часто трапляється, що ви створюєте анонімні компаратори та передаєте їх у методи сортування. Замість створення анонімних об'єктів протягом усього часу, Java 8 дарує можливість використовувати набагато менший обсяг синтаксису, лямбда виразу : Як ви можете помітити, код набагато коротший і простий для читання. Але ось він стає ще коротшим: Для методу в одну лінію ви можете позбутися фігурних дужок {} і ключового слова return 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(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); . Але код стає ще коротшим: Collections.sort(names, (a, b) -> b.compareTo(a)); Java компілятор обізнаний про типи параметрів, так що ви можете також не вказувати і їх. Тепер давайте поринемо глибше в те, як лямбда вирази можуть бути використані в реалії.

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

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

Посилання на методи та конструктори

Приклад вище може бути спрощений за допомогою посилання на статистичний метод: Java 8 дозволяє передавати посилання на методи і конструктори за допомогою ключових символів :: . Наведений вище приклад показує, як можна звертатися до статистичних методів. Але ми можемо також посилатися на методи об'єктів: Давайте поглянемо, як використання :: працює для конструкторів. Для початку визначимо приклад з різними конструкторами: Далі ми визначаємо інтерфейс фабрики PersonFactory для створення нових об'єктів person : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 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; } } interface PersonFactory

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

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

Організація доступу до змінних зовнішньої області видимості з лямбда виразів, подібна до доступу з анонімного об'єкта. Ви можете отримати доступ до змінних з модифікатором final з локальної області видимості, як до полів екземплярів і статистичним змінним.
Доступ до локальних змінних
Ми можемо прочитати локальну змінну з модифікатором final з області видимості лямбда виразу: Але на відміну від анонімних об'єктів, для забезпечення доступу з лямбда виразу до змінних вони не обов'язково повинні бути продекларовані за допомогою final . Цей код також є вірним: Проте змінна num має залишатися незмінною, тобто. бути неявною final для компіляції коду. Наступний код не компілюватиметься: Зміни num всередині лямбда вирази також неприпустимі. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
Доступ до полів екземплярів та статистичним змінним
На відміну від локальних змінних, поля екземплярів та статистичні змінні ми можемо вважати та змінювати всередині лямбду виразів. Така поведінка відома нам від анонімних об'єктів. 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 також сповнений новими функціональними інтерфейсами, що зроблять ваше життя легшим. Деякі з цих інтерфейсів добре відомі з 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
Комп'ютери відомі нам з попередніх версій 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: Колекції в Java 8 розширені так, що ви можете просто створити streams викликом Collection.stream() або 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"); 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. Всі матчі операції є термінальними і повертають булевий результат. 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. Для початку створимо великий список унікальних елементів: Зараз ми визначимо час, витрачений на сортування 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
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 став підтримувати нові та корисні методи для вирішення звичайних завдань. Код вище має бути інтуїтивно-зрозумілим: putIfAbsent застерігає нас від написання додаткових перевірок на null. forEach приймає функцію виконання для кожного з значень map. Цей приклад показує, як виконуються операції на значеннях map, використовуючи функції: Далі ми дізнаємося, як видалити запис для даного ключа, тільки якщо він зіставляється заданому значенню: Інший хороший метод: Цілком легко здійснюється злиття записів 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)); 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.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, якщо для цього ключа відсутній запис, або буде викликана функція злиття, яка змінить значення існуючого запису.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ