На сегодняшний день 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(" nrt".isBlank()); // true
Методы String.stripLeading()
, String.stripTrailing()
и String.strip()
удаляют символы whitespace в начале строки, в конце строки или с обоих концов:
String str = " tHello, world!tn";
String str1 = str.stripLeading(); // "Hello, world!tn"
String str2 = str.stripTrailing(); // " tHello, 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!n".repeat(3));
Вывод:
Hello, world!
Hello, world!
Hello, world!
Наконец, метод String.lines()
разбивает строку на линии. До свидания String.split()
, с которым люди постоянно путают, какой аргумент для него использовать, то ли "n"
, то ли "r"
то ли "nr"
(на самом деле, лучше всего использовать регулярное выражение "R"
, которое покрывает все комбинации). Кроме того, String.lines()
зачастую может быть более эффективен, поскольку он возвращает линии лениво.
System.out.println("line1nline2nline3n"
.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.
Если вы не хотите пропустить следующую часть, то рекомендую вам подписаться на мой Телеграм-канал, где я также публикую новости Java.
Автор: Zheka Kozlov