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

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

Однако в этом цикле статей не будет никакого сухого перечисления 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:

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

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

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(" \n\r\t".isBlank()); // true

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

String str = " \tHello, world!\t\n";

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

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

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

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

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

Вывод:

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

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

System.out.println("line1\nline2\nline3\n"
    .lines()
    .map(String::toUpperCase)
    .collect(Collectors.joining("\n")));

Вывод:

LINE1
LINE2
LINE3

7. String.indent()

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

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

String body = "<h1>Title</h1>\n" +
              "<p>Hello, world!</p>";

System.out.println("<html>\n" +
                   "  <body>\n" +
                   body.indent(4) +
                   "  </body>\n" +
                   "</html>");

Вывод:

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

Заметьте, что для последней линии String.indent() сам вставил перевод строки, поэтому нам не пришлось добавлять '\n' после 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.

Подписывайтесь на канал в Telegram, чтобы не пропускать новости.

Все материалы на этом сайте выложены под лицензией CC BY-SA 4.0
© Евгений Козлов, 2017-2020