На сегодняшний день 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.
Если вы не хотите пропустить следующую часть, то рекомендую вам подписаться на мой Телеграм-канал, где я также публикую новости Java.
rrrad
Аналог Arrays.asList() для Set не нужен, всегда можно передать результат Arrays.asList в конструктор HashSet. Чаще всего такая инициализация востребована в статических константах, а значит лишняя аллокация не имеет значения.
Что касается Map-а, в java8 использую комбинацию Stream.of с SimpleImmutableEntry (вот она замена Map.entry) и последующий collect в Map.
Да, вариант более многословный, но стандартные, уменьшающие громоздкость объявления констант заменяются нестандартными методами, так что на killer-фичу, заставляющую обновляться даже близко не подходит.
Кучка функций для замены commons-lang и подобных пакетов — это круто, но пока они появляются точечно, от сторонних пакетов отказаться не получится, а это заметно осложняет переход (зачем учить новое апи, если всё равно требуется использовать стороннюю библиотеку, в которой уже есть куча функций, которыми уже привык пользоваться). Конечно, появление этих функций в стандартной джаве — это круто, но почему это происходит так избирательно?
Что касается Predicate.not, в java8 есть Predicate.negate(), правда, он требует использования вспомогательной функции, либо явного каста, иначе компилятор не понимает, что это предикат (это, вроде, должны были починить в одной из более поздних версий). Да, Predicate.not — удобная штука, но на killer-фичу не тянет даже близко.
orionll Автор
Ну то есть вы предлагаете писать так?
Очень удобно, ничего не скажешь.
Где я утверждал, что это киллер-фича? Вы взяли из списка наименее востребованные функции, и сделали вывод, что мигрировать не нужно. А остальные функции проигнорировали. Что скажете по поводу ProcessHandle, takeWhile, Cleaner? Почему вы делаете суждения исходя из востребованности каждой отдельной функции, а не их в совокупности? А почему вы проигнорировали тот факт, что это лишь первая часть? Будут ещё статьи и будет ещё много API.
rrrad
Что касается ProcessHandle и Cleaner, то первый нужен для специфических целей и его появление обязано расширению границ применения java вообще, а к последнему я отношусь как к обычной смене одного апи на другое (лично мне пришлось писать финализатор лишь единожды). Что касается гарантий закрытия у Closable, появление достаточно умных ide, которые теперь ругаются на отсутствие вызова close у Closable-объекта, делает данную защиту от забытого close менее острой чем раньше.
webkumo
В моей практике они были нужные почти никогда. Точнее ProcessHandle — вообще ни разу, takeWhile — потенциально пару раз мог пригодиться, а Cleaner — чистейший code smells.
orionll Автор
"Я это не использую, значит оно не нужно"
webkumo
Оно не нужно в вебе, оно мало кому нужно в десктопных приложениях. Я не говорю, что реализация этого API плохо, я отвечаю на ваш домысел, что это "востребованные функции". Но дело в том, что они востребованы в узком списке специфических потребностей, а большинству — не нужны. Т.е. они из категории "есть и хорошо", а не из категории "дайте срочно мне!". Вот стримы и nio отвечали потребностям более широкой аудитории, поэтому на java 7/8 переход был более массовый, чем переход на более современные. Ну вот поставил я себе 11-ую JDK и не пользуюсь её фичами (кошмар какой?!)...