API, ради которых наконец-то стоит обновиться с Java 8. Часть 1

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


2020-02-09 19:42

разработка по

На сегодняшний день Java 8 является самой популярной версией Java и ещё довольно долго будет ей оставаться. Однако с тех пор уже выпущено пять новых версий Java (9, 10, 11, 12, 13), и совсем скоро выйдет ещё одна, Java 14. В этих новых версиях появилось гигантское количество новых возможностей. Например, если считать в JEP'ах, то в сумме их было реализовано 141:

  • 99 в JDK 9
  • 12 в JDK 10
  • 17 в JDK 11
  • 8 в JDK 12
  • 5 в JDK 13
  • (+14 в JDK 14)

Однако в этом цикле статей не будет никакого сухого перечисления JEP'ов. Вместо этого я хочу просто рассказать об интересных API, которые появились в новых версиях. Каждая статья будет содержать по 10 API. В выборе и порядке этих API не будет какой-то определённой логики и закономерности. Это будет просто 10 случайных API, не ТОП 10 и без сортировки от наиболее важного API к наименее важному. Давайте начнём.

1. Методы Objects.requireNonNullElse() и Objects.requireNonNullElseGet()

Появились в: Java 9

Начнём мы наш список с двух очень простеньких, но очень полезных методов в классе java.util.Objects: requireNonNullElse() и requireNonNullElseGet(). Эти методы позволяют вернуть передаваемый объект, если он не null, а если он null, то вернуть объект по умолчанию. Например:

class MyCoder {     private final Charset charset;      MyCoder(Charset charset) {         this.charset = Objects.requireNonNullElse(                 charset, StandardCharsets.UTF_8);     } }

requireNonNullElseGet() – это не что иное, как просто ленивая версия requireNonNullElse(). Она может пригодиться, если вычисление аргумента по умолчанию является затратным:

class MyCoder {     private final Charset charset;      MyCoder(Charset charset) {         this.charset = Objects.requireNonNullElseGet(                 charset, MyCoder::defaultCharset);     }      private static Charset defaultCharset() {         // long operation...     } }

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

2. Методы-фабрики, возвращающие неизменяемые коллекции

Появились в: Java 9

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

  • List.of(E... elements) (и перегрузки)
  • Set.of(E... elements) (и перегрузки)
  • Map.of(K k1, V v1, K k2, V v2, ...) (и перегрузки)
  • Map.ofEntries(Entry<? extends K, ? extends V>... entries)

К этому же списку можно добавить сопутствующий метод Map.entry(K k, V v), создающий Entry из ключа и значения, а также методы копирования коллекций, которые появились в Java 10:

  • List.copyOf(Collection<? extends E> coll)
  • Set.copyOf(Collection<? extends E> coll)
  • Map.copyOf(Map<? extends K,? extends V> map)

Статические методы-фабрики позволяют создать неизменяемую коллекцию и инициализировать её в одно действие:

List<String> imageExtensions = List.of("bmp", "jpg", "png", "gif");

Если не пользоваться сторонними библиотеками, то аналогичный код на Java 8 выглядит гораздо более громоздким:

List<String> imageExtensions = Collections.unmodifiableList(         Arrays.asList("bmp", "jpg", "png", "gif"));

А в случае с Set или Map всё ещё печальнее, потому что аналогов Arrays.asList() для Set и Map не существует.

Такая громоздкость провоцирует многих людей, пишуших на Java 8, вообще отказываться от неизменяемых коллекций и всегда использовать обычные ArrayList, HashSet и HashMap, причём даже там, где по смыслу нужны неизменяемые коллекции. В результате это ломает концепцию immutable-by-default и снижает безопасность кода.

Если же наконец обновиться с Java 8, то работать с неизменяемыми коллекциями становится намного проще и приятнее благодаря методам-фабрикам.

3. Files.readString() и Files.writeString()

Появились в: Java 11

Java всегда была известна своей неспешностью вводить готовые методы для частых операций. Например, для одной из самых востребованных операций в программировании, чтения файла, очень долго не было готового метода. Лишь спустя 15 лет после выхода Java 1.0 появилось NIO, где был введён метод Files.readAllBytes() для чтения файла в массив байтов.

Но этого всё ещё не хватало, потому что людям часто приходится работать с текстовыми файлами и для этого нужно читать из файла строки, а не байты. Поэтому в Java 8 добавили метод Files.readAllLines(), возвращающий List<String>.

Однако и этого было недостаточно, так как люди спрашивали, как просто прочитать весь файл в виде одной строки. В итоге, для полноты картины в Java 11 добавили долгожданный метод Files.readString(), тем самым окончательно закрыв этот вопрос. Удивительно, что если аналогичный метод присутствовал во многих других языках с самого начала, то Java для этого потребовалось больше 20 лет.

Вместе с readString() конечно же ввели и симметричный метод writeString(). Также у этих методов есть перегрузки, позволяющие указать Charset. В совокупности всё это делает работу с текстовыми файлами чрезвычайно удобной. Пример:

/** Перекодировать файл из одной кодировки в другую */ private void reencodeFile(Path path,                           Charset from,                           Charset to) throws IOException {     String content = Files.readString(path, from);     Files.writeString(path, content, to); }

4. Optional.ifPresentOrElse() и Optional.stream()

Появились в: Java 9

Когда Optional появился в Java 8, для него не сделали удобного способа выполнить два разных действия в зависимости от того, есть ли в нём значение или нет. В итоге людям приходится прибегать к обычной цепочке isPresent() и get():

Optional<String> opt = ... if (opt.isPresent()) {     log.info("Value = " + opt.get()); } else {     log.error("Empty"); }

Либо можно извернуться ещё таким образом:

Optional<String> opt = ... opt.ifPresent(str ->     log.info("Value = " + str)); if (opt.isEmpty()) {     log.error("Empty"); }

Оба варианта не идеальны. Но, начиная с Java 9, такое можно сделать элегантно с помощью метода Optional.ifPresentOrElse():

Optional<String> opt = ... opt.ifPresentOrElse(     str -> log.info("Value = " + str),     () -> log.error("Empty"));

Ещё одним новым интересным методом в Java 9 стал Optional.stream(), который возвращает Stream из одного элемента, если значение присутствует, и пустой Stream, если отсутствует. Такой метод может быть очень полезен в цепочках с flatMap(). Например, в этом примере очень просто получить список всех телефонных номеров компании:

class Employee {     Optional<String> getPhoneNumber() { ... } }  class Department {     List<Employee> getEmployees() { ... } }  class Company {     List<Department> getDepartments() { ... }      Set<String> getAllPhoneNumbers() {         return getDepartments()             .stream()             .flatMap(d -> d.getEmployees().stream())             .flatMap(e -> e.getPhoneNumber().stream())             .collect(Collectors.toSet());     } }

В Java 8 пришлось бы писать что-нибудь вроде:

e -> e.getPhoneNumber().map(Stream::of).orElse(Stream.empty())

Это выглядит громоздко и не очень читабельно.

5. Process.pid(), Process.info() и ProcessHandle

Появились в: Java 9

Если без предыдущих API обойтись худо-бедно ещё можно, то вот замену метода Process.pid() в Java 8 найти будет довольно проблематично, особенно кроссплатформенную. Этот метод возвращает нативный ID процесса:

Process process = Runtime.getRuntime().exec("java -version"); System.out.println(process.pid());

Также с помощью метода Process.info() можно узнать дополнительную полезную информацию о процессе. Он возвращает объект типа ProcessHandle.Info. Давайте посмотрим, что он вернёт нам для процесса выше:

Process process = Runtime.getRuntime().exec("java -version"); ProcessHandle.Info info = process.info(); System.out.println("PID = " + process.pid()); System.out.println("User = " + info.user()); System.out.println("Command = " + info.command()); System.out.println("Args = " + info.arguments().map(Arrays::toString)); System.out.println("Command Line = " + info.commandLine()); System.out.println("Start Time = " + info.startInstant()); System.out.println("Total Time = " + info.totalCpuDuration());

Вывод:

PID = 174 User = Optional[orionll] Command = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java] Args = Optional[[-version]] Command Line = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java -version] Start Time = Optional[2020-01-24T05:54:25.680Z] Total Time = Optional[PT0.01S]

Что делать, если процесс был запущен не из текущего Java-процесса? Для этого на помощь приходит ProcessHandle. Например, давайте достанем всю ту же самую информацию для текущего процесса с помощью метода ProcessHandle.current():

ProcessHandle handle = ProcessHandle.current(); ProcessHandle.Info info = handle.info(); System.out.println("PID = " + handle.pid()); System.out.println("User = " + info.user()); System.out.println("Command = " + info.command()); System.out.println("Args = " + info.arguments().map(Arrays::toString)); System.out.println("Command Line = " + info.commandLine()); System.out.println("Start Time = " + info.startInstant()); System.out.println("Total Time = " + info.totalCpuDuration());

Вывод:

PID = 191 User = Optional[orionll] Command = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java] Args = Optional[[Main.java]] Command Line = Optional[/usr/lib/jvm/java-13-openjdk-amd64/bin/java Main.java] Start Time = Optional[2020-01-24T05:59:17.060Z] Total Time = Optional[PT1.56S]

Чтобы получить ProcessHandle для любого процесса по его PID, можно использовать метод ProcessHandle.of() (он вернёт Optional.empty, если процесса не существует).

Также в ProcessHandle есть много других интересных методов, например, ProcessHandle.allProcesses().

6. Методы String: isBlank(), strip(), stripLeading(), stripTrailing(), repeat() и lines()

Появились в: Java 11

Целая гора полезных методов для строк появилась в Java 11.

Метод String.isBlank() позволяет узнать, является ли строка состоящей исключительно из whitespace:

System.out.println("   	".isBlank()); // true

Методы String.stripLeading(), String.stripTrailing() и String.strip() удаляют символы whitespace в начале строки, в конце строки или с обоих концов:

String str = " 	Hello, world!	 ";  String str1 = str.stripLeading(); // "Hello, world!	 " String str2 = str.stripTrailing(); // " 	Hello, world!" String str3 = str.strip(); // "Hello, world!"

Заметьте, что String.strip() не то же самое, что String.trim(): второй удаляет только символы, чей код меньше или равен U+0020, а первый удаляет также пробелы из Юникода:

System.out.println("stru2000".strip()); // "str" System.out.println("stru2000".trim()); // "stru2000"

Метод String.repeat() конкатенирует строку саму с собой n раз:

System.out.print("Hello, world! ".repeat(3));

Вывод:

Hello, world! Hello, world! Hello, world!

Наконец, метод String.lines() разбивает строку на линии. До свидания String.split(), с которым люди постоянно путают, какой аргумент для него использовать, то ли " ", то ли " " то ли " " (на самом деле, лучше всего использовать регулярное выражение "R", которое покрывает все комбинации). Кроме того, String.lines() зачастую может быть более эффективен, поскольку он возвращает линии лениво.

System.out.println("line1 line2 line3 "     .lines()     .map(String::toUpperCase)     .collect(Collectors.joining(" ")));

Вывод:

LINE1 LINE2 LINE3

7. String.indent()

Появился в: Java 12

Давайте разбавим наш рассказ чем-нибудь свежим, что появилось совсем недавно. Встречайте: метод String.indent(), который увеличивает (или уменьшает) отступ каждой линии в данной строке на указанную величину. Например:

String body = "<h1>Title</h1> " +               "<p>Hello, world!</p>";  System.out.println("<html> " +                    "  <body> " +                    body.indent(4) +                    "  </body> " +                    "</html>");

Вывод:

<html>   <body>     <h1>Title</h1>     <p>Hello, world!</p>   </body> </html>

Заметьте, что для последней линии String.indent() сам вставил перевод строки, поэтому нам не пришлось добавлять ' ' после body.indent(4).

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

8. Методы Stream: takeWhile(), dropWhile(), iterate() с предикатом и ofNullable()

Появились в: Java 9

Stream.takeWhile() похож на Stream.limit(), но ограничивает Stream не по количеству, а по предикату. Такая необходимость в программировании возникает очень часто. Например, если нам надо получить все записи в дневнике за текущий год:

[   { "date" : "2020-01-27", "text" : "..." },   { "date" : "2020-01-25", "text" : "..." },   { "date" : "2020-01-22", "text" : "..." },   { "date" : "2020-01-17", "text" : "..." },   { "date" : "2020-01-11", "text" : "..." },   { "date" : "2020-01-02", "text" : "..." },   { "date" : "2019-12-30", "text" : "..." },   { "date" : "2019-12-27", "text" : "..." },   ... ]

Stream записей является почти бесконечным, поэтому filter() использовать не получится. Тогда на помощь приходит takeWhile():

getNotesStream()     .takeWhile(note -> note.getDate().getYear() == 2020);

А если мы хотим получить записи за 2019 год, то можно использовать dropWhile():

getNotesStream()     .dropWhile(note -> note.getDate().getYear() == 2020)     .takeWhile(note -> note.getDate().getYear() == 2019);

В Java 8 Stream.iterate() мог генерировать только бесконечный Stream. Но в Java 9 у этого метода появилась перегрузка, которая принимает предикат. Благодаря этому многие циклы for теперь можно заменить на Stream:

// Java 8 for (int i = 1; i < 100; i *= 2) {     System.out.println(i); }
// Java 9+ IntStream     .iterate(1, i -> i < 100, i -> i * 2)     .forEach(System.out::println);

Обе этих версии печатают все степени двойки, которые не превышают 100:

1 2 4 8 16 32 64

Кстати, последний код можно было бы переписать с использованием takeWhile():

IntStream     .iterate(1, i -> i * 2)     .takeWhile(i -> i < 100)     .forEach(System.out::println);

Однако вариант с трёхаргументным iterate() всё-таки чище (и IntelliJ IDEA предлагает его исправить обратно).

Наконец, Stream.ofNullable() возвращает Stream с одним элементом, если он не null, и пустой Stream, если он null. Этот метод отлично подойдёт в примере выше с телефонами компании, если getPhoneNumber() будет возвращать nullable String вместо Optional<String>:

class Employee {     String getPhoneNumber() { ... } }  class Department {     List<Employee> getEmployees() { ... } }  class Company {     List<Department> getDepartments() { ... }      Set<String> getAllPhoneNumbers() {         return getDepartments()             .stream()             .flatMap(d -> d.getEmployees().stream())             .flatMap(e -> Stream.ofNullable(e.getPhoneNumber()))             .collect(Collectors.toSet());     } }

9. Predicate.not()

Появился в: Java 11

Этот метод не вносит ничего принципиально нового и носит скорее косметический, нежели фундаментальный характер. И всё же возможность немного подсократить код всегда очень приятна. С помощью Predicate.not() лямбды, в которых есть отрицание, можно заменить на ссылки на методы:

Files.lines(path)      .filter(str -> !str.isEmpty())      .forEach(System.out::println);

А теперь используя not():

Files.lines(path)      .filter(not(String::isEmpty))      .forEach(System.out::println);

Да, экономия не такая уж и огромная, а если использовать s -> !s.isEmpty(), то количество символов, наоборот, становится больше. Но даже в этом случае я всё равно предпочту второй вариант, так как он более декларативен и в нём не используется переменная, а значит не захламляется пространство имён.

10. Cleaner

Появился в: Java 9

Сегодняшний рассказ я хочу завершить новым интересным API, появившимся в Java 9 и служащим для очистки ресурсов перед их утилизацией сборщиком мусора. Cleaner является безопасной заменой метода Object.finalize(), который сам стал deprecated в Java 9.

С помощью Cleaner можно зарегистрировать очистку ресурса, которая произойдёт, если её забыли сделать явно (например, забыли вызвать метод close() или не использовали try-with-resources). Вот пример абстрактного ресурса, для которого в конструкторе регистрируется очищающее действие:

public class Resource implements Closeable {     private static final Cleaner CLEANER = Cleaner.create();     private final Cleaner.Cleanable cleanable;     public Resource() {         cleanable = CLEANER.register(this, () -> {             // Очищающее действие             // (например, закрытие соединения)         });     }      @Override     public void close() {         cleanable.clean();     } }

По-хорошему, такой ресурс пользователи должны создавать в блоке try:

try (var resource = new Resource()) {     // Используем ресурс }

Однако могут найтись пользователи, которые забудут это делать и будут писать просто var resource = new Resource(). В таких случаях очистка выполнится не сразу, а позовётся позже в одном из следующих циклов сборки мусора. Это всё же лучше, чем ничего.

Если вы хотите изучить Cleaner получше и узнать, почему никогда не стоит использовать finalize(), то рекомендую вам послушать мой доклад на эту тему.

Заключение

Java не стоит на месте и постепенно развивается. Пока вы сидите на Java 8, с каждым релизом появляется всё больше и больше новых интересных API. Сегодня мы рассмотрели 10 таких API. И вы сможете использовать их все, если наконец решитесь мигрировать с Java 8.

В следующий раз мы рассмотрим ещё 10 новых API.


Источник: habr.com

Комментарии: