Одинадцать скрытых жемчужин Java 11

Автор оригинала: Nicolai Parlog
  • Перевод
  • Tutorial

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

Пристально следим за развитием JDK


Шаг 2: Разыскиваем на StackOverflow связанные вопросы

Разыскиваем на StackOverflow связанные вопросы


Шаг 3: Прилетаем с новым ответом, основанным на будущих изменениях

Swoop in with new answer based on upcoming changes


Шаг 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", "Motörhead")
    .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!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Какую версию Java вы используете в проде?

  • 1,3%Java 12 — на острие атаки3
  • 19,2%Java 11 — в ногу со временем45
  • 76,9%Java 8 — на веки в наших сердцах180
  • 6,0%Java 7 — нам и так хорошо14
  • 2,6%Java 6 — я ретроград6
  • 1,3%Java 5 — стабильность наше всё3
  • 3,0%Java 4 — разве выходило что-то новее?7
  • +18
  • 7,9k
  • 2
Поддержать автора
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 2

    0
    а кто-то может объяснить, почему начал активно использоваться символ ::?
      0

      В Java именно так создаётся ссылка на метод.
      По примерам кода это должно быть видно.


      В документации по Java есть описание всех видов ссылок на методы:


      • Ссылка на статический метод: ContainingClass::staticMethodName
      • Ссылка на метод конкретного экземпляра: containingObject::instanceMethodName
      • Ссылка на метод экземпляра указанного типа: ContainingType::methodName
      • Ссылка на конструктор: ClassName::new

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое