Java 11 не представил никаких новаторских функций, но содержит несколько жемчужин, о которых вы могли ещё не слышать. Уже смотрели на новинки в String
, Optional
, Collection
и других рабочих лошадках? Если нет, то вы пришли по адресу: сегодня мы рассмотрим 11 скрытых жемчужин из Java 11!
Вывод типов для лямбда-параметров
При написании лямбда-выражения можно выбирать между явным указанием типов и их пропуском:
Function<String, String> append = string -> string + " ";
Function<String, String> append = (String s) -> s + " ";
Java 10 ввела var
, но его нельзя было использовать в лямбдах:
// ошибка компиляции в Java 10
Function<String, String> append = (var string) -> string + " ";
В Java 11 уже можно. Но почему? Не похоже чтобы var
давал больше чем просто пропуск типа. И хотя это так, использование var
имеет два незначительных преимущества:
- делает использование
var
более универсальным, убрав исключение из правил - позволяет добавлять аннотации на тип параметра, не прибегая к использованию его полного имени
Вот пример второго случая:
List<EnterpriseGradeType<With, Generics>> types = /*...*/;
types
.stream()
// нормально, но нам нужна аннотация @Nonnull на типе
.filter(type -> check(type))
// в Java 10 нужно сделать так ~> гадость
.filter((@Nonnull EnterpriseGradeType<With, Generics> type) -> check(type))
// в Java 11 можно уже так ~> гораздо лучше
.filter((@Nonnull var type) -> check(type))
Хотя смешивание выведеных, явных и неявных типов в лямбда-выражениях вида (var type, String option, index) -> ...
может быть реализовано, но (в рамках JEP-323) эта работа не проводилась. Следовательно, необходимо выбрать один из трех подходов и придерживаться его для всех параметров лямбда-выражения. Необходимость указывать var
для всех параметров, чтобы добавить аннотацию для одного из них, может слегка раздражать, но в целом терпимо.
Потоковая обработка строк с ‘String::lines’
Получили многострочную строку? Хотите что-нибудь сделать с каждой её строчкой? Тогда String::lines
это правильный выбор:
var multiline = "Это\r\nваша\r\nмногострочная\r\nстрока";
multiline
.lines() //Stream<String>
.map(line -> "// " + line)
.forEach(System.out::println);
// ВЫВОД:
// Это
// ваша
// многострочная
// строка
Заметьте, что исходная строка использует виндовые разделители \r\n
и, хотя я нахожусь на Linux, lines()
всё равно разбил её. Так происходит из-за того что, не смотря на текущую ОС, этот метод интерпретирует \r
, \n
, и \r\n
как разрыв строки – даже если они смешаны в одной строке.
Поток из строчек никогда не содержит сами разделители строки. Строчки могут быть пустыми ("как\n\nв этом\n\nслучае"
, который содержит 5 строчек), но последняя строчка исходной строки игнорируется если она получается пустой ("как\nтут\n"
; 2 строчки). (Замечание переводчика: им то удобно есть line
, а есть string
, а у нас и то и то строка
.)
В отличии от split("\R")
, lines()
ленив и, я цитирую, "обеспечивает лучшую производительность […] более быстрым поиском новых разрывов строки". (Если кто-то хочет запилить бенчмарк на JMH для проверки, дайте мне знать). А так же он лучше отражает алгоритм обработки и использует более удобную структуру данных (поток вместо массива).
Удаление пробельных символов с ‘String::strip’
и т.п.
Изначально, String
имел метод trim
для удаления пробельных символов, которыми считал всё с кодами вплоть до U+0020
. Да, BACKSPACE
(U+0008)
это пробельный символ как и BELL
(U+0007
), но LINE SEPARATOR
(U+2028
) уже не считается таковым.
Java 11 ввёл метод strip
, подход которого имет больше нюансов. Он использует метод Character::isWhitespace
из Java 5 для определения что же именно нужно удалять. Из его документации видно что это:
SPACE SEPARATOR
,LINE SEPARATOR
,PARAGRAPH SEPARATOR
, но не неразрывный пробелHORIZONTAL TABULATION
(U+0009
),LINE FEED
(U+000A
),VERTICAL TABULATION
(U+000B
),FORM FEED
(U+000C
),CARRIAGE RETURN
(U+000D
)FILE SEPARATOR
(U+001C
),GROUP SEPARATOR
(U+001D
),RECORD SEPARATOR
(U+001E
),UNIT SEPARATOR
(U+001F
)
С этой же логикой есть ещё два очищающих метода, stripLeading
и stripTailing
, которые делают именно то, что от них ожидается.
И на конец, если просто нужно узнать станет ли строка пустой после удаления пробельных символов, то нет необходимости реально их удалять – просто используйте isBlank
:
" ".isBlank(); // пробел ~> true
" ".isBlank(); // неразрывный пробел ~> false
Повторение строк с ‘String::repeat’
Ловите идею:
Шаг 1: Пристально следим за развитием JDK
Шаг 2: Разыскиваем на StackOverflow связанные вопросы
Шаг 3: Прилетаем с новым ответом, основанным на будущих изменениях
Шаг 4: ????
Шаг 4: Профит
Как вы поняли, у String
появился новый метод repeat(int)
. Он работает точно в соответствии с ожиданиями, и тут мало что можно обсудить.
Создание путей с ‘Path::of’
Мне очень нравится API Path
, но конвертация путей между разными представлениями (такими как Path
, File
, URL
, URI
и String
) всё же раздражает. Этот момент стал менее запутанным в Java 11 за счёт копирования двух методов Paths::get
в методы Path::of
:
Path tmp = Path.of("/home/nipa", "tmp");
Path codefx = Path.of(URI.create("http://codefx.org"));
Их можно считать каноничными, так как оба старых метода Paths::get
используют новые варианты.
Чтение и запись файлов с ‘Files::readString’
и ‘Files::writeString’
Если мне нужно читать из большого файла, я обычно использую Files::lines
для получения ленивого потока его строчек. Аналогично, для записи большого объёма данных, которые и в памяти могут не храниться целиком, я использую Files::write
передавая их как Iterable<String>
.
А как же простой случай когда я хочу обработать содержимое файла как одну строку? Это не очень удобно, так как Files::readAllBytes
и подходящие варианты Files::write
оперируют массивами байт.
И тут появляется Java 11, добавляя readString
и writeString
в Files
:
String haiku = Files.readString(Path.of("haiku.txt"));
String modified = modify(haiku);
Files.writeString(Path.of("haiku-mod.txt"), modified);
Понятно и просто в использовании. При необходимости можно передать Charset
в readString
, а во writeString
ещё и массив OpenOptions
.
Пустое I/O с ‘Reader::nullReader’
и т.п.
Нужен OutputStream
, который никуда не пишет? Или пустой InputStream
? А как насчёт Reader
и Writer
, которые ничего не делают? В Java 11 есть всё это:
InputStream input = InputStream.nullInputStream();
OutputStream output = OutputStream.nullOutputStream();
Reader reader = Reader.nullReader();
Writer writer = Writer.nullWriter();
(Примечание переводчика: в commons-io
от Apache эти классы существовали ещё примерно с 2014-го года.)
В прочем, я удивлён — является ли null
действительно лучшим префиксом? Мне не нравится как оно используется для обозначения "намеренного отсутствия"… Возможно было бы лучше использовать noOp
? (Примечание переводчика: скорее всего этот префикс был выбран из-за распространённого использования /dev/null
.)
{ } ~> [ ]
с ‘Collection::toArray’
Как вы конвертируете коллекции в массивы?
// до Java 11
List<String> list = /*...*/;
Object[] objects = list.toArray();
String[] strings_0 = list.toArray(new String[0]);
String[] strings_size = list.toArray(new String[list.size()]);
Первый вариант, objects
, теряет всю информацию о типах, так что он в пролёте. Что на счёт остальных? Оба громоздки, но первый короче. Последний создаёт массив требуемого размера, так что он выглядит производительнее (то есть "кажется более производительным", см. правдоподобность). Но реально ли он производительнее? Нет, наоборот, он медленнее (на текущий момент).
Но почему я должен заботиться об этом? Разве нет лучшего способа сделать это? В Java 11 есть:
String[] strings_fun = list.toArray(String[]::new);
Появился новый вариант Collection::toArray
, который принимает IntFunction<T[]>
, т.е. функцию, которая получает размер массива и возвращает массив требуемого размера. Её можно кратко выразить в виде ссылки на конструктор вида T[]::new
(для известного T
).
Занятный факт, дефолтная реализация Collection#toArray(IntFunction<T[]>)
всегда передаёт 0
в генератор массивов. Сперва я решил, что это решение было основано на лучшей производительности при массивах нулевой длины, но сейчас я думаю, что причиной может быть то, что для некоторых коллекций вычисление размера может быть очень дорогой операцией и не стоит такой подход использовать в дефолтной реализации Collection
. При этом конкретные реализации коллекций, такие как ArrayList
, могут изменить этот подход, но в Java 11 они не меняют. Не стоит того, наверное.
Проверка отсутствия с ‘Optional::isEmpty’
При обильном использовании Optional
, особенно в больших проектах, где часто сталкиваешься с не Optional
-подходом, часто приходится проверять у него наличие значения. Для этого есть метод Optional::isPresent
. Но так же часто нужно знать и обратное — то что Optional
пуст. Нет проблем просто используй !opt.isPresent()
, так ведь?
Конечно, можно и так, но практически всегда понять логику if
проще, если его условие не инвертируется. А иногда Optional
всплывает в конце длинной цепочки вызовов и если нужно проверить его на пустоту, то приходится ставить !
в самое начало:
public boolean needsToCompleteAddress(User user) {
return !getAddressRepository()
.findAddressFor(user)
.map(this::canonicalize)
.filter(Address::isComplete)
.isPresent();
}
В таком случае пропустить !
очень легко. Начиная с Java 11 есть вариант лучше:
public boolean needsToCompleteAddress(User user) {
return getAddressRepository()
.findAddressFor(user)
.map(this::canonicalize)
.filter(Address::isComplete)
.isEmpty();
}
Инвертирование предикатов с ‘Predicate::not’
Говоря об инвертировании… Интерфейс Predicate
имеет метод экземпляра negate
: он возвращяет новый предикат, который выполняет ту же проверку, но инвертирует её результат. К сожалению, мне редко удаётся его использовать…
// хочу распечатать не пустые строки
Stream
.of("a", "b", "", "c")
// тьфу, лямбда ~> хочу использовать ссылку на метод и инвертировать
.filter(s -> !s.isBlank())
// компилятор не знает во что превратить ссылку на метод ~> ошибка
.filter((String::isBlank).negate())
// тьфу, каст ~> так даже хуче чем с лямбдой
.filter(((Predicate<String>) String::isBlank).negate())
.forEach(System.out::println);
Проблема в том, что я редко имею доступ к инстансу Predicate
. Гораздо чаще я хочу получить такой инстанс через ссылку на метод (и инвертировать его), но, чтобы такое прокатило, компилятор должен знать к чему приводить ссылку на метод — без этого он ничего не может сделать. И именно это и происходит, если использовать конструкцию (String::isBlank).negate()
: компилятор больше не знает чем должен быть String::isBlank
на этом и сдаётся. Правильно указанный каст исправляет это, но какой ценой?
Хотя и есть и простое решение. Не использовать метод экземпляра negate
, а использовать новый статический метод Predicate.not(Predicate<T>)
из Java 11:
Stream
.of("a", "b", "", "c")
// статически импортированный `java.util.function.Predicate.not`
.filter(not(String::isBlank))
.forEach(System.out::println);
Уже лучше!
Регулярные выражения как предикат с ‘Pattern::asMatchPredicate’
Есть регулярное выражение? Нужно по нему отфильтровать данные? Как на счёт такого:
Pattern nonWordCharacter = Pattern.compile("\\W");
Stream
.of("Metallica", "Motorhead")
.filter(nonWordCharacter.asPredicate())
.forEach(System.out::println);
Я был очень рад найдя этот метод! Стоит добавить, что это метод из Java 8. Упс, упустил это тогда. Java 11 добавила ещё один похожий метод: Pattern::asMatchPredicate
. В чём же разница?
asPredicate
проверяет что строка или часть строки соответствует шаблону (работает какs -> this.matcher(s).find()
)asMatchPredicate
проверяет что вся строка соответствует шаблону (работает какs -> this.matcher(s).matches()
)
Например, у нас есть регулярное выражение, которое проверяет телефонные номера, но оно не содержит ^
и $
для слежения за началом и концом строки. Тогда следующий код будет работать не так как вы, возможно, ожидаете:
prospectivePhoneNumbers
.stream()
.filter(phoneNumberPatter.asPredicate())
.forEach(this::robocall);
Вы заметили ошибку? Строка вида "о ФЗ-152 слышал? +1-202-456-1414"
пройдёт фильтрацию, потому что содержит валидный телефонный номер. С другой стороны Pattern::asMatchPredicate
не позволит этого, потому что строка целиком уже не будет соответствовать шаблону.
Самопроверка
Вот обзор всех одиннадцати жемчужин — а вы ещё помните, что делает каждый метод? Если да — вы прошли проверку.
- в
String
:
Stream<String> lines()
String strip()
String stripLeading()
String stripTrailing()
boolean isBlank()
String repeat(int)
- в
Path
:
static Path of(String, String...)
static Path of(URI)
- в
Files
:
String readString(Path) throws IOException
Path writeString(Path, CharSequence, OpenOption...) throws IOException
Path writeString(Path, CharSequence, Charset, OpenOption...) throws IOException
- в
InputStream
:static InputStream nullInputStream()
- в
OutputStream
:static OutputStream nullOutputStream()
- в
Reader
:static Reader nullReader()
- в
Writer
:static Writer nullWriter()
- в
Collection
:T[] toArray(IntFunction<T[]>)
- в
Optional
:boolean isEmpty()
- в
Predicate
:static Predicate<T> not(Predicate<T>)
- в
Pattern
:Predicate<String> asMatchPredicate()
Веселитесь с Java 11!
kotbaun
а кто-то может объяснить, почему начал активно использоваться символ ::?
valery1707 Автор
В Java именно так создаётся ссылка на метод экземпляра объекта.
По примерам кода это должно быть видно.