Глава 3. Строки, компараторы и фильтры
Некоторые методы, представленные в JDK, очень полезны для написания кода функционального стиля. Мы уже хорошо знакомы с некоторыми классами и интерфейсами библиотеки JDK, такими как String. Чтобы избавиться от старого стиля, к которому мы привыкли, нам приходится активно искать возможности использования этих новых методов. Аналогично, когда нам нужно использовать анонимный внутренний класс только с одним методом, мы теперь можем заменить его лямбда-выражением без необходимости писать его так громоздко, как раньше.
В этой главе мы будем использовать лямбда-выражения и ссылки на методы для перемещения по строкам, реализации интерфейса Comparator, просмотра файлов в каталоге и отслеживания изменений в файлах и каталогах. Некоторые из методов, представленных в предыдущей главе, будут по-прежнему появляться здесь, чтобы помочь нам лучше выполнять эти задачи. Новые методы, которые вы изучите, помогут превратить длинный и утомительный код во что-то лаконичное, быстро реализуемое и простое в обслуживании.
Перебирать строку
Метод chars() — это новый метод класса String, который является частью интерфейса CharSequence. Это очень полезный инструмент, если вы хотите быстро просмотреть последовательность символов строки. С помощью этого внутреннего итератора мы можем удобно работать с каждым символом строки. Попробуйте сначала использовать его для обработки строки. Вот несколько способов использования ссылок на методы.
Скопируйте код кода следующим образом:
окончательная строка str = "w00t";
стр.chars()
.forEach(ch -> System.out.println(ch));
Метод chars() возвращает объект Stream, для обхода которого мы можем использовать его внутренний итератор forEach(). В итераторе мы можем напрямую обращаться к символам строки. Ниже приведен результат цикла по строке и печати каждого символа.
Скопируйте код кода следующим образом:
119
48
48
116
Это не тот результат, который нам нужен. Мы ожидаем увидеть буквы, но на выходе — цифры. Это связано с тем, что метод chars() возвращает целочисленный поток вместо символьного типа. Давайте сначала разберемся с этим API, а затем оптимизируем выходные результаты.
В предыдущем коде мы создали лямбда-выражение в качестве входного параметра метода forEach. Он просто передает параметры методу println(). Поскольку эта операция очень распространена, мы можем использовать компилятор Java для упрощения этого кода. Как и при использовании ссылок на методы на стр. 25, замените ее ссылкой на метод и позвольте компилятору выполнить маршрутизацию параметров за нас.
Мы увидели, как создать ссылку на метод экземпляра. Например, метод name.toUpperCase(), ссылка на метод — String::toUpperCase. В следующем примере мы вызываем метод экземпляра, который статически ссылается на System.out. Левая часть двух двоеточий, на которые ссылается метод, может быть именем класса или выражением. Благодаря такой гибкости мы можем легко создать ссылку на метод println(), как показано ниже.
Скопируйте код кода следующим образом:
стр.chars()
.forEach(System.out::println);
Как видите, компилятор Java может очень эффективно выполнять маршрутизацию параметров. Напомним, что лямбда-выражения и ссылки на методы могут появляться только там, где получен функциональный интерфейс, и компилятор Java сгенерирует там соответствующий метод (Аннотация: Компилятор сгенерирует реализацию функционального интерфейса, имеющую только один метод.). Метод, который мы использовали ранее, ссылается на String::toUpperCase, и параметры, переданные в сгенерированный метод, в конечном итоге станут целевым объектом вызова метода, например:parameter.toUpperCase(). Это связано с тем, что ссылка на метод основана на имени класса (String). Ссылка на метод в приведенном выше примере основана на выражении, которое является экземпляром PrintStream и на которое имеется ссылка через System.out. Поскольку объект вызова метода уже существует, компилятор Java решает использовать параметры сгенерированного метода в качестве параметров этого метода println: System.out.println(name).
(Аннотация: на самом деле существует в основном два сценария. Также передается ссылка на метод. Один из них — это пройденный объект, конечно, целевой объект вызова метода, например name.toUpperCase, а другой используется в качестве параметра вызов метода, например System.out.println(name).)
Код становится намного проще после использования ссылок на методы, но нам необходимо глубже понять, как он работает. Как только мы познакомимся со ссылками на методы, мы сможем самостоятельно разобраться с маршрутизацией параметров.
Хотя код в этом примере достаточно краток, результат все равно неудовлетворителен. Мы ожидали увидеть буквы, но вместо них появились цифры. Чтобы решить эту проблему, напишем метод вывода int в виде букв.
Скопируйте код кода следующим образом:
Private static void printChar(int aChar) {
System.out.println((char)(aChar));
}
Использование ссылок на методы позволяет легко оптимизировать выходные результаты.
Скопируйте код кода следующим образом:
стр.chars()
.forEach(IterateString::printChar);
Теперь, хотя результат, возвращаемый chars(), — int, это не имеет значения. Когда нам нужно напечатать, мы преобразуем его в символы. На этот раз на выходе наконец-то появятся буквы.
Скопируйте код кода следующим образом:
ш
0
0
т
Если мы хотим с самого начала обрабатывать символы вместо целых чисел, мы можем напрямую преобразовать целые числа в символы после вызова chars:
Скопируйте код кода следующим образом:
стр.chars()
.mapToObj(ch -> Character.valueOf((char)ch))
.forEach(System.out::println);
Здесь мы используем внутренний итератор потока, возвращаемый функцией chars(). Конечно, можно использовать не только этот метод. После получения объекта Stream мы можем использовать его методы, такие как Map(), Filter(), Reduc() и т.д. Мы можем использовать метод filter() для фильтрации символов, которые являются числами:
Скопируйте код кода следующим образом:
стр.chars()
.filter(ch -> Символ.isDigit(ch))
.forEach(ch -> printChar(ch));
При выводе таким способом мы можем видеть только числа:
Скопируйте код кода следующим образом:
0
0
Аналогично, помимо передачи лямбда-выражений в методы filter() и forEach(), мы также можем использовать ссылки на методы.
Скопируйте код кода следующим образом:
стр.chars()
.filter(Character::isDigit)
.forEach(IterateString::printChar);
Ссылка на метод здесь исключает избыточную маршрутизацию параметров. В этом примере мы также видим использование, отличное от двух предыдущих методов. В первый раз мы ссылались на метод экземпляра, во второй раз это был метод статической ссылки (System.out). На этот раз это ссылка на статический метод — ссылки на методы приносят прибыль молча.
Ссылки на методы экземпляра и статические методы выглядят одинаково: например, String::toUpperCase и Character::isDigit. Компилятор определяет, является ли метод методом экземпляра или статическим методом, чтобы определить, как маршрутизировать параметры. Если это метод экземпляра, он будет использовать входные параметры сгенерированного метода в качестве целевого объекта вызова метода, например параметра toUpperCase() (конечно, есть исключения, например целевой объект вызова метода); указано, например System::out.println ()). Кроме того, если это статический метод, входные параметры созданного метода будут использоваться в качестве параметров метода, на который ссылаются, например Charter.isDigit(parameter). Приложение 2 на стр. 152 содержит подробные инструкции по использованию ссылок на методы и их синтаксис.
Хотя ссылки на методы удобны в использовании, все же существует проблема — неоднозначность, вызванная конфликтами имен методов. Если соответствующий метод является одновременно методом экземпляра и статическим методом, компилятор сообщит об ошибке из-за неоднозначности метода. Например, если мы напишем Double::toString вот так, мы на самом деле хотим преобразовать тип double в строку, но компилятор не знает, вызывать ли метод экземпляра public String toString() или вызывать public static String. toString (double), поскольку оба метода относятся к классу Double. Если вы столкнулись с такой ситуацией, не расстраивайтесь, просто используйте лямбда-выражения для ее завершения.
Как только мы освоимся с функциональным программированием, мы сможем по своему желанию переключаться между лямбда-выражениями и ссылками на методы.
В этом разделе мы используем новый метод в Java 8 для перебора строк. Давайте посмотрим на улучшения интерфейса Компаратора.