Статьи

Java 8 против Scala, Часть II: API Streams

Это часть 2 статьи. Если вы хотите начать с 1, вы можете нажать  здесь .

Поток против Коллекции?

Давайте начнем с краткого и неполного описания мной :), коллекция — это конечный набор данных, а поток — это последовательный набор значений, который может быть как конечным, так и бесконечным. Да, вот и все.

Streams API — это новый API, который поставляется с Java 8 для управления сбором и потоковой передачей данных. API Streams не изменяет состояние, в то время как API Коллекций. Например, когда вы вызываете  Collections.sort (list) , метод сортирует экземпляр коллекции, который вы передаете через аргумент, в то время как вызов  list.stream (). Sorted ()  дает вам новую копию коллекции и оставляет исходную копию без изменений. Вы можете прочитать больше об API потоков  здесь .

Ниже приведены интересные сравнения коллекций и потоков, которые я скопировал из документации по Java 8. Я настоятельно рекомендую вам прочитать полную версию  здесь .

Потоки отличаются от коллекций несколькими способами:

1. Нет хранения. Поток — это не структура данных, в которой хранятся элементы; вместо этого он передает элементы из источника через конвейер вычислительных операций.
2. Функциональный характер. Операция над потоком дает результат, но не изменяет его источник.
3. Поиск лени. Многие потоковые операции, такие как фильтрация, отображение или удаление дубликатов, могут быть реализованы лениво, открывая возможности для оптимизации.
4. Возможно, неограничен. Хотя коллекции имеют конечный размер, потоки не нужны.
5. Расходные материалы. Элементы потока посещаются только один раз в течение жизни потока.

Java и Scala имеют довольно простой способ одновременного вычисления значений в коллекции. В Java вы просто должны вызывать  ParallelStream () *  или stream () .rallel ()  вместо просто  stream ()  из коллекции. В Scala вы должны вызвать  par ()  перед вызовом других функций. Довольно заманчиво добавить параллелизм и ожидать, что программа будет работать быстрее. К сожалению, большую часть времени он будет работать медленнее. На самом деле, параллелизм — это особенность, которую очень легко реализовать неправильно. Проверьте эту интересную статью:  параллельные потоки Java вредны для вашего здоровья!

* Из Javadoc функция  parallelStream ()  возвращает возможно параллельный поток с коллекцией в качестве источника. Таким образом, он может возвращать последовательный поток. Какие??? ( кто-то исследовал, почему существует этот API )

Название изображения

Java Streams API вычисляет элементы лениво. Это означает, что промежуточные операции не выполняют никакой обработки, пока не будет выполнена терминальная операция. Обработка ленивых потоков может быть оптимизирована для повышения производительности. Например, у нас есть фильтрация, отображение и суммирование в конвейере. Эти операции могут быть объединены в один проход данных, чтобы уменьшить количество промежуточных состояний. Лень также позволяет обрабатывать только те данные, которые необходимы. Напротив, коллекции Scala строго подразумевают, что элемент будет обработан с нетерпением. … Хм … это будет означать, что в нашем тесте Java Streams API будет иметь преимущество перед Scala? Если мы сравним API потоков Java с API коллекций Scala, ответ будет положительным. Но у вас есть так много вариантов в Scala. Вы можете легко преобразовать коллекцию в поток, просто позвонив toStream () . У Scala также есть другая концепция, которая называется View, которая также является нестрогой коллекцией, такой как Stream. Что такое нестрогая коллекция? Это коллекция, которая будет вычисляться лениво, ака. ленивая коллекция.

Давайте кратко рассмотрим функции Stream и View в Scala.

Скала Стрим

Поток Scala немного отличается от потока Java. В потоке Scala вам не нужно вызывать операцию терминала, чтобы получить результат, поскольку потоки являются результатом. Stream — это абстрактный класс, реализующий черты AbstractSeq , LinearSeq и  GenericTraversableTemplate . Таким образом, вы можете относиться к Stream как к Seq .    

Если вы знакомы с Java, но не Scala, вы можете думать о  Seq  как о  интерфейсе List  в Java. ( Список  Scala  не является интерфейсом, но это тема для другой статьи :)).

Здесь мы должны знать, что элементы в Streams вычисляются лениво, и поэтому Stream может быть реализован для бесконечных данных. Ожидается, что поток будет иметь ту же производительность, что и список, если все элементы в коллекции вычисляются. После вычисления значения кэшируются. Поток имеет функцию, называемую  силой . Вызывает оценку всего потока, а затем возвращает результат. Будьте осторожны, чтобы не вызывать эту функцию в бесконечном потоке, а также другие операции, которые заставляют API обрабатывать весь поток, такие как  size () , toList ()foreach () и т. Д. Эти операции являются неявными терминальными операциями в Scala. Поток.

Давайте реализуем последовательность Фибоначчи в потоке Scala.

def fibFrom(a: Int, b: Int): Stream[Int] = a #:: fibFrom(b, a + b)
val fib1 = fibFrom(0, 1) //0 1 1 2 3 5 8 …
val fib5 = fibFrom(0, 5) //0 5 5 10 15 …
//fib1.force //Don’t do this cause it will call the function infinitely and soon you will get the OutOfMemoryError
//fib1.size //Don’t do this too with the same reason as above.
fib1.take(10) //Do this. It will take the first 10 from the inifite Stream.
fib1.take(20).foreach(println(_)) //Prints 20 first numbers

::  понятие обычно используется как имя функции для объединения данных в коллекции. Итак,  # ::  также означает конкатенацию, но она будет лениво объединять правое значение (у вас больше свободы в названии функции в Scala, чем в Java).

Scala ‘View

Еще раз, коллекция Scala является строгой коллекцией, в то время как представление является нестрогой коллекцией. Представление — это коллекция, представляющая базовую коллекцию, но все преобразователи реализованы лениво. Вы можете преобразовать строгую коллекцию в представление, вызвав   функцию view, и вы можете преобразовать ее обратно, вызвав  функцию force  . Представление не кэширует результат и применяет преобразование каждый раз, когда вы выбираете. Это похоже на представление базы данных, где представление представляет собой виртуальную коллекцию.

Давайте создадим набор данных, над которым мы будем работать.

public class Pet {
    public static enum Type {
        CAT, DOG
    }
    public static enum Color {
        BLACK, WHITE, BROWN, GREEN
    }
    private String name;
    private Type type;
    private LocalDate birthdate;
    private Color color;
    private int weight;
    ...
}

Предположим, у нас есть коллекция домашних животных, и мы собираемся поиграть с этой коллекцией в этой статье.

Фильтр

Требование:  мы хотим отфильтровать только пухлых домашних животных из коллекции. Любое животное, которое весит более 50 фунтов, считается пухлым. Мы также хотим получить список домашних животных, которые родились до 1 января 2013 года. В следующем фрагменте кода показано, как выполнить эту работу фильтра различными способами.

Java Подход 1:  Традиционный стиль

//Before Java 8
List<Pet> tmpList = new ArrayList<>();
for(Pet pet: pets){
    if(pet.getBirthdate().isBefore(LocalDate.of(2013, Month.JANUARY, 1))
            && pet.getWeight() > 50){
        tmpList.add(pet);
    }
}

Это то, что мы обычно делаем на императивных языках. Вы должны создать временную коллекцию, затем выполнить итерацию по каждому элементу и сохранить каждый, который удовлетворяет предикату (ам) во временной коллекции. Это довольно многословно, но всегда выполняет свою работу, и ее производительность тоже удивительна. Я могу побаловать вас здесь тем, что традиционный подход быстрее, чем подход Streams API. Не беспокойтесь о производительности, потому что создание более элегантного кода перевешивает небольшой выигрыш в производительности.

Подход Java 2:  API потоков

//Java 8 - Stream
pets.stream()
    .filter(pet -> pet.getBirthdate().isBefore(LocalDate.of(2013, Month.JANUARY, 1)))
    .filter(pet -> pet.getWeight() > 50)
    .collect(toList())

Из приведенного выше кода мы использовали API Streams для фильтрации элементов в коллекции. Я намеренно дважды вызывал  метод filter,  чтобы показать, что Streams API разработан как  шаблон Builder . В паттерне Builder вы можете объединить различные методы в цепочку, прежде чем вызывать метод build, который создает результирующий объект. В Streams API метод построения называется терминальной операцией, а метод, который не является терминальной операцией, является промежуточной операцией. Операции терминала могут отличаться от функции построения в шаблоне Builder, поскольку вы не можете вызывать операцию терминала более одного раза в Streams API. Существует множество терминальных операций, которые вы можете использовать -  собиратьсчитатьминМакситераторtoArray . Эти операции производят значения, тогда как некоторые терминальные операции, такие как  forEach , используют значения. Какой подход вы считаете более читабельным? Традиционный или потоковый API подход.

Подход Java 3:  API коллекций

//Java 8 - Collection
pets.removeIf(pet -> !(pet.getBirthdate().isBefore(LocalDate.of(2013, Month.JANUARY, 1))
                    && pet.getWeight() > 50));
//Applying De-Morgan's law.
pets.removeIf(pet -> pets.get(0).getBirthdate().toEpochDay() >= LocalDate.of(2013, Month.JANUARY, 1).toEpochDay()
                || pet.getWeight() <= 50);

Этот подход самый короткий. Тем не менее, он изменяет исходную коллекцию, а предыдущие нет. Функция  removeIf  принимает  Predicate <T>  (функциональный интерфейс) в качестве аргумента. Предикат  — это поведенческий параметр, и у него есть только один абстрактный метод с именем  test,  который принимает объект и возвращает логическое значение. Обратите внимание, что мы должны изменить логику, поставив! перед выражением или вы можете применить  закон де Моргана,  и код будет выглядеть как второе утверждение.

Подход Scala : сбор, просмотр и потоковая передача

//Scala - strict collection
pets.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //List[Pet]
//Scala - non-strict collection
pets.views.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //SeqView[Pet]
//Scala - stream
pets.toStream.filter { pet => pet.getBirthdate.isBefore(LocalDate.of(2013, Month.JANUARY, 1))}
.filter { pet => pet.getWeight > 50 } //Stream[Pet]

Решения в Scala очень похожи на Java Streams API. Посмотрите на каждом из них, вы просто должны вызов  вид  функции , чтобы превратить строгую коллекцию нестрогого одного и вызова  toStream  функции превратить строгую коллекцию потока.

Я думаю, что вы уже поняли идею. Итак, я покажу вам только код и держу рот на замке: D.

Группировка

Группирует элементы в коллекции по одному из атрибутов в элементе. Результатом будет Map <T, List <T >>, где T — универсальный тип.

Требование:  Группировать домашних животных по типу, т.е. Собака, Кошка и др.

//Java approach
Map<Pet.Type, List<Pet>> result = pets.stream().collect(groupingBy(Pet::getType));
//Scala approach
val result = pets.groupBy(_.getType)

Примечание:  groupingBy  — это статический вспомогательный метод в  java.util.stream.Collectors .

Сортировка

Сортирует элементы в коллекции по любым атрибутам в элементе. Результатом будет любой вид коллекций, в зависимости от конфигурации, которые сохраняют порядок элементов.

Требование:  мы хотим отсортировать домашних животных по типу, имени и цвету по порядку.

//Java approach
pets.stream().sorted(comparing(Pet::getType)
    .thenComparing(Pet::getName)  
    .thenComparing(Pet::getColor))
    .collect(toList());
//Scala approach
pets.sortBy{ p => (p.getType, p.getName, p.getColor) }

картографирование

Применяет данную функцию к каждому элементу в коллекции. Результат может быть любого типа в зависимости от данной функции.

Требование:  мы хотим преобразовать Pet в строку в этом формате «% s — имя:% s, цвет:% s»

//Java approach
pets.stream().map( p-> 
        String.format(“%s — name: %s, color: %s”, 
            p.getType(), p.getName(), p.getColor())
    ).collect(toList());
//Scala approach
pets.map{ p => s"${p.getType} - name: ${p.getName}, color: ${p.getColor}"}

В первую очередь

Находит первый элемент, который соответствует данному предикату.

Требование:  Мы хотим найти питомца с именем «Красавчик». Нам все равно, сколько красивых питомцев в коллекции. Мы просто хотим первый.

//Java approach
pets.stream()
    .filter( p-> p.getName().equals(“Handsome”))
    .findFirst();
//Scala approach
pets.find{ p=> p.getName == “Handsome” }

Это хитрый. Вы заметили, что в подходе Scala я использовал  функцию поиска вместо  фильтра ? Если вы используете  фильтр  вместо  find , он вычислит все элементы в коллекции, потому что коллекция Scala строгая. Однако вам не нужно беспокоиться об использовании  фильтра  в Java Streams API, API выяснит, что вы просто хотите получить первый, и не будет вычислять всю коллекцию, когда найдет ее. Это когда ленивая коллекция светит!

Давайте посмотрим больше примеров отложенной коллекции в коде Scala ниже. Мы помещаем предикат, который всегда возвращает true, в функцию фильтра и получаем второй результат операции. Что мы увидим в качестве выхода?

pets.filter { x => println(x.getName); true }.get(1) --- (1)
pets.toStream.filter { x => println(x.getName); true }.get(1) -- (2)

Из приведенного выше кода первое утверждение напечатает имена всех домашних животных в коллекции, а второе утверждение напечатает только имена первых двух домашних животных. В этом преимущество ленивой коллекции. Это вычисляет лениво.

pets.view.filter { x => println(x.getName); true }.get(1) --- (3)

Из приведенного выше кода мы увидим тот же результат, что и (2)? Ответ — Нет. Результат будет таким же, как (1). Не могли бы вы сказать мне, почему?

После сравнения Java и Scala подход по нескольким распространенным операциям — фильтрация, группировка, сопоставление и поиск; Очевидно, что подход Scala короче, чем Java. Но какой подход тебе нравится? Какой из них вы считаете более читабельным?

В следующей части этой статьи мы увидим, какая из них быстрее. Надеюсь, в следующий раз это будет менее спорным. Будьте на связи!