На сегодняшний день 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:
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(" \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.