Мы долго ждали, когда лямбда внесет концепцию замыканий в Java, но если мы не будем использовать ее в коллекциях, мы потеряем большую ценность. Проблема миграции существующих интерфейсов в стиль лямбда решена с помощью методов по умолчанию. В этой статье мы глубоко проанализируем работу с массовыми данными (массовые операции) в коллекциях Java и разгадаем тайну самой мощной роли лямбды.
1. О JSR335
JSR — это аббревиатура от «Запросы спецификации Java», что означает «запрос спецификации Java». Основным улучшением версии Java 8 является проект Lambda (JSR 335), целью которого является упрощение написания кода на Java для многоядерных процессоров. JSR 335 = лямбда-выражение + улучшение интерфейса (метод по умолчанию) + пакетная обработка данных. Вместе с предыдущими двумя статьями мы полностью изучили соответствующее содержание JSR335.
2. Внешняя и внутренняя итерация
Раньше коллекции Java не могли выражать внутреннюю итерацию, а предоставляли только один способ внешней итерации, то есть цикл for или while.
Скопируйте код кода следующим образом:
Список людей = asList(new Person("Джо"), новый Person("Джим"), новый Person("Джон"));
for (Человек p: человек) {
p.setLastName("Доу");
}
Приведенный выше пример представляет собой наш предыдущий подход, который представляет собой так называемую внешнюю итерацию. Цикл представляет собой цикл с фиксированной последовательностью. В современную эпоху многоядерности, если мы хотим выполнять параллельный цикл, нам придется изменить приведенный выше код. Насколько можно повысить эффективность, пока неясно, и это принесет определенные риски (проблемы с потокобезопасностью и т. д.).
Чтобы описать внутреннюю итерацию, нам нужно использовать библиотеку классов, например Lambda. Давайте перепишем приведенный выше цикл, используя лямбда и Collection.forEach.
Скопируйте код следующим образом: person.forEach(p->p.setLastName("Doe"));
Теперь библиотека jdk управляет циклом. Нам не нужно заботиться о том, как установить фамилию для каждого объекта-человека. Библиотека может решить, как это сделать в зависимости от рабочей среды: параллельно, не по порядку или лениво. загрузка. Это внутренняя итерация, и клиент передает поведение p.setLastName в качестве данных в API.
На самом деле внутренняя итерация не имеет тесного отношения к пакетной обработке коллекций. С ее помощью мы можем почувствовать изменения в грамматическом выражении. Действительно интересная вещь, связанная с пакетными операциями, — это новый потоковый API. В JDK 8 был добавлен новый пакет java.util.stream.
3.Потоковый API
Поток представляет собой только поток данных и не имеет структуры данных, поэтому его уже невозможно пройти после того, как он был пройден один раз (на это нужно обращать внимание при программировании, в отличие от Коллекции, в нем все равно есть данные независимо от того, сколько раз) он пройден). Источником может быть коллекция, массив, io и т. д.
3.1 Промежуточные и конечные методы
Потоковая передача предоставляет интерфейс для работы с большими данными, делая операции с данными проще и быстрее. В нем есть такие методы, как фильтрация, сопоставление и сокращение количества обходов. Эти методы делятся на два типа: промежуточные методы и методы терминала. Абстракция «потока» должна быть непрерывной по своей природе. Промежуточные методы всегда возвращают поток. мы хотим получить окончательный результат. В этом случае необходимо использовать операции конечной точки для сбора окончательных результатов, полученных потоком. Разница между этими двумя методами заключается в том, чтобы посмотреть на возвращаемое значение. Если это Stream, то это промежуточный метод, в противном случае — конечный метод. Подробную информацию см. в API Stream.
Кратко представим несколько промежуточных методов (фильтр, карта) и методов конечной точки (сбор, сумма).
3.1.1Фильтр
Реализация функций фильтрации в потоках данных — самая естественная операция, которую мы можем придумать. Интерфейс Stream предоставляет метод фильтра, который принимает реализацию Predicate, представляющую операцию по использованию лямбда-выражения, определяющего условия фильтра.
Скопируйте код кода следующим образом:
Перечислить людей = …
Stream personOver18 = person.stream().filter(p -> p.getAge() > 18);//Фильтровать людей старше 18 лет
3.1.2Карта
Предположим, мы сейчас фильтруем некоторые данные, например, при преобразовании объектов. Операция Map позволяет нам выполнить реализацию функции (общие T и R функции Function<T, R> представляют входные данные и результаты выполнения соответственно), которая принимает входные параметры и возвращает их. Во-первых, давайте посмотрим, как описать его как анонимный внутренний класс:
Скопируйте код кода следующим образом:
Поток для взрослых = человек
.транслировать()
.filter(p -> p.getAge() > 18)
.map(новая функция() {
@Override
общественный взрослый подать заявку (человек) {
return new Adult(person);//Преобразовать человека старше 18 лет во взрослого
}
});
Теперь преобразуйте приведенный выше пример в лямбда-выражение:
Скопируйте код кода следующим образом:
Карта потока = person.stream()
.filter(p -> p.getAge() > 18)
.map(person -> новый взрослый(человек));
3.1.3 Подсчет
Метод count — это метод конечной точки потока, который может формировать окончательную статистику результатов потока и возвращать целое число. Например, давайте посчитаем общее количество людей в возрасте 18 лет и старше:
Скопируйте код кода следующим образом:
int countOfAdult=persons.stream()
.filter(p -> p.getAge() > 18)
.map(person -> новый Взрослый(человек))
.считать();
3.1.4 Собрать
Метод сбора также является методом конечной точки потока, который может собирать окончательные результаты.
Скопируйте код кода следующим образом:
Список AdultList=persons.stream()
.filter(p -> p.getAge() > 18)
.map(person -> новый Взрослый(человек))
.collect(Коллекторы.toList());
Или, если мы хотим использовать определенный класс реализации для сбора результатов:
Скопируйте код кода следующим образом:
Список AdultList = люди
.транслировать()
.filter(p -> p.getAge() > 18)
.map(person -> новый Взрослый(человек))
.collect(Collectors.toCollection(ArrayList::new));
Из-за ограниченности места другие промежуточные методы и методы конечной точки не будут представлены один за другим. После прочтения приведенных выше примеров вам нужно только понять разницу между этими двумя методами, и вы сможете решить использовать их в соответствии со своими потребностями. позже.
3.2 Последовательный и параллельный поток
Каждый поток имеет два режима: последовательное выполнение и параллельное выполнение.
Последовательность действий:
Скопируйте код кода следующим образом:
List <Person> люди = list.getStream.collect(Collectors.toList());
Параллельные потоки:
Скопируйте код кода следующим образом:
List <Person> люди = list.getStream.parallel().collect(Collectors.toList());
Как следует из названия, при использовании последовательного метода обхода каждый элемент считывается до того, как будет прочитан следующий элемент. При использовании параллельного обхода массив будет разделен на несколько сегментов, каждый из которых обрабатывается в отдельном потоке, а затем результаты выводятся вместе.
3.2.1 Принцип параллельного потока:
Скопируйте код кода следующим образом:
Список исходного списка = некоторые данные;
Split1 = originalList(0, Mid);//Разделяем данные на небольшие части
Split2 = originalList (середина, конец);
new Runnable(split1.process());//Выполнение операций небольшими частями
новый Runnable(split2.process());
Список пересмотренных списков = Split1 + Split2;//Объединяем результаты
3.2.2 Сравнение последовательных и параллельных тестов производительности
Если это многоядерная машина, теоретически параллельный поток будет в два раза быстрее последовательного потока. Ниже приведен тестовый код.
Скопируйте код кода следующим образом:
длинный t0 = System.nanoTime();
//Инициализируем целочисленный поток с диапазоном в 1 миллион и находим число, которое делится на 2. toArray() — метод конечной точки
int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
длинный t1 = System.nanoTime();
//Та же функция, что и выше, здесь мы используем параллельный поток для вычисления
int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
длинный t2 = System.nanoTime();
//Результаты моей локальной машины последовательные: 0,06 с, параллельные 0,02 с, что доказывает, что параллельный поток действительно быстрее последовательного.
System.out.printf("Последовательный: %.2fs, параллельный %.2fs%n", (t1 - t0) * 1e-9, (t2 - t1) * 1e-9);
3.3 О платформе Folk/Join
Аппаратный параллелизм приложений доступен в Java 7. Одной из новых функций пакета java.util.concurrent является структура параллельной декомпозиции в стиле fork-join. Она также очень мощная и эффективная. Я не буду ее изучать. подробнее здесь. По сравнению с Stream.parallel() я предпочитаю последний.
4. Резюме
Без лямбды Stream довольно неудобно использовать. Он будет генерировать большое количество анонимных внутренних классов, как в приведенном выше примере 3.1.2map. Если нет метода по умолчанию, изменения в структуре сбора неизбежно вызовут множество изменений. Таким образом, метод лямбда+по умолчанию делает библиотеку jdk более мощной и гибкой, улучшения Stream и среды сбора данных являются лучшим доказательством.