Глава 2: Использование коллекций
Мы часто используем различные коллекции, числа, строки и объекты. Они повсюду, и даже если код, управляющий коллекцией, можно немного оптимизировать, это сделает код намного понятнее. В этой главе мы исследуем, как использовать лямбда-выражения для управления коллекциями. Мы используем его для перемещения по коллекциям, преобразования коллекций в новые, удаления элементов из коллекций и объединения коллекций.
Пройти по списку
Обход списка — это самая базовая операция над множеством, и с течением времени ее операции претерпели некоторые изменения. Мы используем небольшой пример обхода имен, представляя его от самой старой версии до самой элегантной на сегодняшний день.
Мы можем легко создать неизменяемый список имен с помощью следующего кода:
Скопируйте код кода следующим образом:
окончательный список<String> друзей =
Arrays.asList("Брайан", "Нейт", "Нил", "Раджу", "Сара", "Скотт");
System.out.println(friends.get(i));
}
Ниже приведен наиболее распространенный метод обхода списка и его печати, хотя он также является наиболее общим:
Скопируйте код кода следующим образом:
for(int i = 0; i < friends.size(); i++) {
System.out.println(friends.get(i));
}
Я называю этот способ письма мазохистским — он многословен и подвержен ошибкам. Нам нужно остановиться и подумать: «Это i< или i<=?» Это имеет смысл только тогда, когда нам нужно работать с конкретным элементом, но даже в этом случае мы все равно можем использовать функциональные выражения, соответствующие принципу неизменяемый стиль, о котором мы вскоре поговорим.
Java также обеспечивает относительно развитую структуру.
Скопируйте код кода следующим образом:
коллекции/fpij/Iteration.java
for(Имя строки: друзья) {
System.out.println(имя);
}
На самом деле такая итерация реализуется с использованием интерфейса Iterator, вызывая его методы hasNext и next. Оба метода являются внешними итераторами и сочетают в себе то, как это сделать, и то, что вы хотите сделать. Мы явно контролируем итерацию, сообщая ей, где начать и где закончить. Вторая версия делает это «под капотом» с помощью метода Iterator; При явной операции вы также можете использовать операторы Break и continue для управления итерацией. Во второй версии есть некоторые моменты, которых не хватает в первой. Этот подход лучше первого, если мы не собираемся изменять элемент коллекции. Однако оба эти метода являются обязательными, и в текущей версии Java от них следует отказаться. Причин перехода на функциональный стиль несколько:
1. Цикл for сам по себе является последовательным и его трудно распараллелить.
2. Такой цикл не является полиморфным; вы получаете то, что просите. Мы передаем коллекцию непосредственно в цикл for, а не вызываем метод (который поддерживает полиморфизм) для коллекции для выполнения определенной операции.
3. С точки зрения дизайна код, написанный таким образом, нарушает принцип «Говори, не спрашивай». Мы просим выполнить итерацию, а не оставлять ее базовой библиотеке.
Пришло время перейти от старого императивного программирования к более элегантному функциональному программированию внутренних итераторов. После использования внутренних итераторов мы оставляем выполнение многих конкретных операций базовой библиотеке методов, чтобы вы могли больше сосредоточиться на конкретных бизнес-требованиях. Базовая функция будет отвечать за итерацию. Сначала мы используем внутренний итератор для перечисления списка имен.
Интерфейс Iterable был улучшен в JDK8. Он имеет специальное имя forEach, которое получает параметр типа Comsumer. Как следует из названия, экземпляр Consumer потребляет переданный ему объект через метод принятия. Для использования метода forEach мы используем знакомый синтаксис анонимного внутреннего класса:
Скопируйте код кода следующим образом:
friends.forEach(new Consumer<String>() { public void Accept (окончательное имя строки) {
System.out.println(имя);
});
Мы вызвали метод forEach для коллекции друзей, передав ему анонимную реализацию Consumer. Этот метод forEach вызывает метод принятия переданного в Consumer для каждого элемента в коллекции, позволяя ему обработать этот элемент. В этом примере мы просто печатаем его значение, то есть имя. Давайте посмотрим на вывод этой версии, который такой же, как и предыдущие две:
Скопируйте код кода следующим образом:
Брайан
Нейт
Нил
Раджу
Сара
Скотт
Мы изменили только одно: отказались от устаревшего цикла for и использовали новый внутренний итератор. Преимущество состоит в том, что нам не нужно указывать, как перебирать коллекцию, и мы можем больше сосредоточиться на обработке каждого элемента. Недостаток в том, что код выглядит более многословным, что почти убивает радость от нового стиля кодирования. К счастью, это легко изменить, и именно здесь в игру вступают возможности лямбда-выражений и новых компиляторов. Давайте сделаем еще одну модификацию и заменим анонимный внутренний класс лямбда-выражением.
Скопируйте код кода следующим образом:
friends.forEach((окончательное имя строки) -> System.out.println(имя));
Так выглядит намного лучше. Кода меньше, но давайте сначала посмотрим, что это значит. Метод forEach — это функция высшего порядка, которая получает лямбда-выражение или блок кода для работы с элементами в списке. При каждом вызове элементы коллекции будут привязаны к переменной имени. В базовой библиотеке осуществляется вызов лямбда-выражения. Он может принять решение отложить выполнение выражений и, при необходимости, выполнить параллельные вычисления. Вывод этой версии также такой же, как и в предыдущей.
Скопируйте код кода следующим образом:
Брайан
Нейт
Нил
Раджу
Сара
Скотт
Версия внутреннего итератора более лаконична. Более того, используя его, мы можем больше сосредоточиться на обработке каждого элемента, а не на его обходе — это декларативно.
Однако эта версия имеет недостатки. Как только метод forEach начнет выполняться, в отличие от двух других версий, мы не сможем выйти из этой итерации. (Конечно, есть и другие способы сделать это). Поэтому этот способ записи чаще используется, когда необходимо обработать каждый элемент коллекции. Позже мы представим некоторые другие функции, которые позволят нам контролировать процесс цикла.
Стандартный синтаксис лямбда-выражений заключается в помещении параметров внутри (), предоставлении информации о типе и использовании запятых для разделения параметров. Чтобы освободить нас, компилятор Java также может автоматически выполнять вывод типов. Типы конечно удобнее не писать. Работы меньше и мир тише. Ниже представлена предыдущая версия после удаления типа параметра:
Скопируйте код кода следующим образом:
friends.forEach((имя) -> System.out.println(имя));
В этом примере компилятор Java знает, что тип имени — String, посредством контекстного анализа. Он просматривает сигнатуру вызываемого метода forEach, а затем анализирует функциональный интерфейс в параметрах. Затем он проанализирует абстрактный метод в этом интерфейсе и проверит количество и тип параметров. Даже если это лямбда-выражение получает несколько параметров, мы все равно можем выполнить вывод типа, но в этом случае все параметры не могут иметь типы параметров в лямбда-выражениях, типы параметров должны быть записаны вообще, а если они записаны, то их необходимо записать; полностью.
Компилятор Java специально обрабатывает лямбда-выражения с одним параметром: если вы хотите выполнить вывод типа, круглые скобки вокруг параметра можно опустить.
Скопируйте код кода следующим образом:
friends.forEach(имя -> System.out.println(имя));
Здесь есть небольшая оговорка: параметры, используемые для вывода типа, не относятся к конечному типу. В предыдущем примере явного объявления типа мы также пометили параметр как окончательный. Это не позволяет вам изменить значение параметра в лямбда-выражении. Вообще говоря, изменять значение параметра — это плохая привычка, которая легко может вызвать ОШИБКУ, поэтому хорошей привычкой будет отмечать его как окончательное. К сожалению, если мы хотим использовать вывод типа, нам придется самим следовать правилам и не изменять параметры, потому что компилятор нас больше не защищает.
Чтобы добраться до этой точки, потребовалось много усилий, но теперь объем кода действительно стал немного меньше. Но это еще не самое простое. Давайте попробуем эту последнюю минималистскую версию.
Скопируйте код кода следующим образом:
friends.forEach(System.out::println);
В приведенном выше коде мы используем ссылку на метод. Мы можем напрямую заменить весь код именем метода. Мы рассмотрим это более подробно в следующем разделе, а пока давайте вспомним знаменитую цитату Антуана де Сент-Экзюпери: «Совершенство — это не то, что можно добавить, а то, что уже нельзя отнять».
Лямбда-выражения позволяют нам кратко и ясно просматривать коллекции. В следующем разделе мы поговорим о том, как это позволяет нам писать такой лаконичный код при выполнении операций удаления и преобразования коллекций.