Статьи

Изучение Java 8 Lambdas. Часть 1

Лямбда-функции и замыкания появятся в Java 8. Это был проект, который продолжался некоторое время, и недавно я воспользовался возможностью взять его на тест-драйв.

Информация :

Некоторые полезные документы для проекта можно найти по адресу:

  • Домашняя страница проекта Lambda .
  • Состояние лямбды : старая статья (декабрь 2011 г.), описывающая тогдашний статус внедрения лямбды. Несмотря на то, что документ по-прежнему весьма актуален, он может не описывать многое с точки зрения особенностей использования функций.
  • Лямбда FAQ   : Это необходимо прочитать FAQ. Во многих отношениях он охватывает многие из тех же самых тем, уже рассмотренных в лямбда-выражении , но делает то же самое в формате вопросов и ответов, который позволяет легко использовать информацию по одному небольшому ответу за раз. Я часто упоминаю об этом в этом посте.
  • список рассылки lambda-dev

Мотивация:

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

Страница Почему лямбда-выражения добавляются в Java? описывает первичную мотивацию

Лямбда-выражения (и замыкания) являются популярной функцией многих современных языков программирования. Среди различных причин для этого наиболее неотложной для платформы Java является то, что они облегчают распределение обработки коллекций по нескольким потокам. В настоящее время списки и наборы обычно обрабатываются клиентским кодом, получая итератор из коллекции, а затем используют его для итерации по своим элементам и обработки каждого по очереди. Если обработка различных элементов должна выполняться параллельно, за организацию этого отвечает код клиента, а не коллекция.

В Java 8 целью является предоставление коллекциям методов, которые будут принимать функции и использовать их, каждый по-своему, для обработки своих элементов. Преимущество этого изменения заключается в том, что коллекции теперь могут организовывать свою собственную итерацию внутри себя, передавая ответственность за распараллеливание из кода клиента в код библиотеки.

На странице Есть объекты лямбда-выражений? мы встречаем следующую цитату

Чтобы понять ситуацию, полезно знать, что существуют как краткосрочные цели, так и долгосрочная перспектива для реализации в Java 8. Краткосрочные цели состоят в том, чтобы поддерживать внутреннюю итерацию коллекций в интересах эффективного используя все более параллельное оборудование. Долгосрочная перспектива — направить Java в направлении, которое поддерживает более функциональный стиль программирования. В настоящее время преследуются только краткосрочные цели, но разработчики стараются не ставить под угрозу будущее функционального программирования на Java, которое в будущем может включать в себя полноценные типы функций, которые встречаются в таких языках, как Haskell и Scala.

Полезную строку можно найти в одном из сообщений в списке рассылки lambda-dev, указанном выше, т.е. Пронзить лямбду

Лямбда — это аванс за эту эволюцию, но это далеко от конца истории. Остальная часть истории еще не написана, но сохранение наших вариантов является ключевым аспектом того, как мы оцениваем решения, которые мы принимаем здесь.)

Whither Java Collections Framework

Многие из добавляемых новых функций не добавляются непосредственно в классы, в настоящее время формирующие среду Java Collections Framework. В центре этих изменений появился новый интерфейс Streamи вспомогательный класс Streams. Обоснование для Stream объясняется следующим образом в разделе «Куда движется среда Java Collections Framework»?

Анализ использования коллекций Java показывает, что один шаблон является очень распространенным, в котором массовые операции извлекаются из источника данных (массива или коллекции), а затем повторно применяют операции преобразования, такие как фильтрация и сопоставление данных, часто в конечном итоге обобщая его в виде одиночная операция, такая как суммирование числовых элементов. Текущее использование этого шаблона требует создания временных коллекций для хранения промежуточных результатов этих преобразований. Однако этот стиль обработки может быть преобразован в конвейер с использованием хорошо известного паттерна «Трубы и фильтры» с существенными полученными в результате преимуществами: устранение промежуточных переменных, сокращение промежуточного хранилища, отложенная оценка и более гибкие и составные определения операций. Более того, если каждая операция в конвейере определяется соответствующим образом,конвейер в целом часто может быть автоматически распараллелен (разделен для параллельного выполнения на многоядерных процессорах). Роль конвейеров, соединителей между конвейерными операциями, берется в библиотеках Java 8 реализациями интерфейса Stream; Изучение этого интерфейса прояснит, как работают конвейеры.

Другая страница Почему операции Stream не определены непосредственно в Collection? документально обосновывает, почему многие потоковые методы не определены непосредственно в интерфейсе Collection.

Реализация используется в этом посте

Я использовал версию платформы Java ™ b78, Standard Edition 8 Early Access с поддержкой лямбды, отсюда . Может случиться так, что некоторые методы API, на которые я ссылаюсь, продолжат развиваться. Документацию API JDK для этой конкретной версии можно найти здесь

Типичная структура трубопровода.

Давайте рассмотрим типичную структуру трубопровода. Для данной коллекции преобразуйте ее в поток, выполните набор преобразований и, наконец, соберите результаты в Collector. Мы рассмотрим детали сборщика позже, но на данный момент мы можем представить, что возвращаемое значение выполнения конвейера — это объект, который возвращается сборщиком.

List<String> result =  Arrays.asList("Larry", "Moe", "Curly")
                              .stream()
                              .map(s -> "Hello " + s)
                              .collect(Collectors.toList());

// result will be a List<String> containing "Hello Larry", "Hello Moe" and "Hello Curly"

Давайте оценим шаги в конвейере

  • Arrays.asList("Larry", "Moe", "Curly"): создает список с тремя строками, а именно. «Ларри», «Мо» и «Кудряшка».
  • .stream(): Преобразует список в поток
  • .map(s -> "Hello " + s): Выполняет операцию отображения, которая добавляет «Hello» к каждому элементу в потоке
  • .collect(Collectors.toList()): Определяет новый сборщик, который будет собирать результаты конвейера в список и вызывает сбор для сбора результатов в него (и в конечном итоге возвращает собранный список).

Как вы можете заметить, тип оригинальной коллекции не сохраняется во время преобразования. Фактически вы должны иметь возможность взять коллекцию и вызвать .stream()ее, в результате чего вы фактически потеряете тип самой исходной коллекции. Все преобразования определены в потоках, и в конечном итоге вы можете преобразовать результат Streamв желаемый тип коллекции, предоставив соответствующийCollector, Это приводит меня к мысли, что лямбда-преобразования — это действительно потоковые преобразователи с преобразователями на обоих концах — один для преобразования коллекции в поток и в конечном итоге для сбора потока обратно в коллекцию нужного типа. (Конечно, можно выбрать, чтобы конечный результат оставался потоком, который можно использовать впоследствии). Поток позволяет учесть соображения, касающиеся эффективности, распараллеливания и даже агностичности сбора. Коллекции на концах обеспечивают конкретную реализацию, лучше всего подходящую для других задач вне конвейера.

Streams

Потоки являются одними из наиболее важных типов, которые должны использоваться в лямбда-преобразованиях. Поэтому имеет смысл взглянуть на два связанных класса. Интерфейс java.util.stream.Streamи помощник java.util.stream.Streams.

Давайте сначала взглянем на Streams(Javadoc) . Это вспомогательный класс и имеет множество вспомогательных операций для создания потоков. (примечание: .boxed()как используется ниже, преобразует поток примитивных целых в поток целых чисел)

например. некоторые из методов (полный список см. в javadocs)

  • Streams.intRange(start, stop, step)будет генерировать поток примитивов int. (шаг, если не указано, равен 1)
  • Streams.longRange(start, stop, step)будет генерировать поток примитивов long. (шаг, если не указано, равен 1)
  • Streams.concat(stream1, stream2) объединит два потока
    IntStream str1 = Streams.intRange(1,10);
    IntStream str2 = Streams.intRange(21,30);
    Stream<Integer> joined = Streams.concat(str1.boxed(), str2.boxed());
    System.out.println(joined.collect(Collectors.toList()));
    
    // Expected output : [1, 2, 3, 4, 5, 6, 7, 8, 9, 21, 22, 23, 24, 25, 26, 27, 28, 29]

  • Streams.zip(stream1, stream2, function) вернет поток на основе функции, применяемой к последовательным парам элементов в stream1 и stream2
    Stream<Integer> ints = Streams.intRange(0,5).boxed();
    Stream<String> strs = Arrays.asList("foo", "bar", "baz", "qux").stream();
    System.out.println(Streams.zip(ints,strs, (i, s) -> i.toString() + s).collect(Collectors.toList()));
    
    // Expected output: ["0foo", "1bar", "2baz", "3qux"]

Примечание . Как упоминалось ранее, любую коллекцию можно преобразовать в поток, вызвав .stream()ее.

Ручей

Теперь, когда мы увидели, как создавать, объединять или объединять потоки, давайте рассмотрим некоторые из наиболее полезных операций, которые можно выполнять в потоке. Я не буду подробно останавливаться на этих трансформациях в этом посте. Я намерен вернуться к ним в будущем посте.

(Javadoc)

метод Описание
allMatch истина, если все элементы потока соответствуют предикату
AnyMatch истина, если какой-либо элемент потока соответствует предикату
фильтр вернуть поток подмножества элементов, соответствующих предикату
findAny вернуть элемент потока, соответствующий предикату
FindFirst вернуть первый элемент потока, соответствующий предикату
flatMap Вернуть поток, в котором каждый элемент ввода преобразуется в 0 или более значений.
Возврат потока, в котором каждый элемент ввода преобразуется в поток
для каждого Выполните операцию над каждым элементом потока (обычно для побочных эффектов)
предел Возврат потока с не более чем первыми элементами maxSize этого потока
карта Преобразуйте поток в другой, содержащий результаты, после применения функции mapper к каждому элементу потока.
Максимум Вернуть максимальный элемент этого потока на основе предоставленного компаратора
мин Вернуть минимальный элемент этого потока на основе предоставленного компаратора
заглядывать Вернуть тот же поток, даже если элементы также предоставлены потребителю
уменьшить Сократите поток до одного значения, выполнив операцию редуктора для каждого элемента вместе с накопленным значением (накопленное значение, начиная с идентификатора).
отсортированный Сортировка потока на основе натурального порядка или поставляемого компаратора
подпотоке Возврат потока после отбрасывания первых элементов initialOffset (и тех, которые после опционально предоставленных элементов endOffset)
ToArray Преобразовать поток в массив

Результат выполнения вышеуказанных операций может привести к нулю или 1 элементу (например, findFirst) из потока, логическому результату (например, allMatch), никакому результату (например, forEach), уменьшенному значению (например, уменьшить) или просто другой поток (например, map, filter, flatMap, limit, subStream и т. д.). Когда поток возвращается, аналогичные преобразования могут быть применены к нему снова, что приведет к конвейеру преобразований.

функции

Если вы взглянули на javadoc для описанных выше методов, вы увидите, что многие из них требуют каких-то функций в качестве предикатов или преобразователей. Это похоже на .map(s -> "Hello " + s)вызов, который мы видели ранее. И если вы привыкли к более ранним версиям Java, они, без сомнения, выглядят очень компактно.

Давайте на минутку представим, что мы работаем с Java 7 или раньше, и представим, как такое функциональное преобразование может быть передано другому методу в качестве входных данных. Полностью гипотетический эквивалент с использованием классической Java будет

Stream<String> strings;
strings.map(
    new MapperFunction<String,String>() {
        public String map(String str) {
            return "Hello" + str;
        }
    }
);

Сравните это с компактным объявлением, которое принимает метод java 8 Stream.map. Вы можете быстро отметить следующее:

  • Там нет анонимного класса экземпляра требуется ( MapperFunctionвыше)
  • Нет описания функции ( public String mapвыше)
  • Там нет объявлений типа (два Stringобъявления public String map(String str)выше)
  • return неявно.

Также обратите внимание, что вместо традиционного класса, являющегося параметром, мы передали то, что для практических целей кажется функцией. Но может ли Java принимать функции в качестве аргументов? И если мы не передадим сигнатуру для функции, то как она выведет ее?

Для ответов нам нужно взглянуть на функциональные интерфейсы.

Функциональные интерфейсы

Функциональные интерфейсы — очень интересное (и, насколько мне известно, уникальное) введение в лексику Java 8. За подробностями мы можем обратиться к Что такое функциональный интерфейс?

функциональный интерфейс определяется как любой интерфейс, который имеет ровно один абстрактный метод. (Квалификация необходима, потому что интерфейс имеет неабстрактные методы, унаследованные от Object, и может также иметь неабстрактные методы по умолчанию). Вот почему функциональные интерфейсы называли интерфейсами Single Abstract Method (SAM), термин, который до сих пор иногда встречается.

Таким образом, функциональный интерфейс может наследовать не абстрактные методы. Он также может иметь неабстрактные методы по умолчанию. Но у него может быть только один абстрактный метод.

Кроме того : Java 8 предоставляет возможность определять реализации по умолчанию в интерфейсах. Обработка того же выходит за рамки этого поста. Но вы могли бы прочитать о том, что методы по умолчанию? и последующие четыре страницы, если интересно.

Итак, то, что раньше передавалось в map (), было не функцией, а в терминах Java 8, реализацией функционального интерфейса. Если вы посмотрите на различные параметры, передаваемые в методы Stream, рассмотренные выше, вы обнаружите, что многие из аргументов являются функциональными интерфейсами.

Java 8 также вводит существенную степень синтаксического сахара, чтобы кратко определить реализацию функционального интерфейса. В приведенном выше случае это былоs -> "Hello " + s

Давайте посмотрим, как компилятор может иметь смысл из всего этого. Сначала аргумент, предоставленный mapметоду, является Function<T,R>интерфейсом. Если вы посмотрите на сигнатуру этого интерфейса, вы найдете не один, а два метода. К счастью, один из них вызвал composeреализацию по умолчанию. Таким образом, существует только один абстрактный метод, applyкоторый принимает Tи возвращает a R.

Учитывая, что требование функционального интерфейса было выполнено (поскольку существует один абстрактный метод — apply), мы можем посмотреть на сигнатуру и сказать, что, поскольку он Stream<T>имеет тип Stream<String>, входной аргумент метода apply будет иметь тип String. Теперь компилятор может рассматривать функцию, предоставленную в качестве аргумента метода map, как тело метода apply экземпляра Function<T,R>интерфейса. Входной аргумент для метода apply т.е. Tявляется String. Как насчет вывода?

Здесь мы встречаем еще один интересный аспект, представленный Java 8. т.е. вывод типа с использованием целевых типов. Конечный результат конвейера выше был List<String>. Который был результатом .collect(Collectors.toList())операции. Таким образом, вклад в сбор должен был быть Stream<String>. Поскольку выходные данные mapвызова метода должны быть a Stream<String>, это означает, что выходные данные метода apply должны быть a String. Это прекрасно соответствует нашим ожиданиям , что , так как вход является String, "Hello " + sдолжно быть также String. Так что все хорошо, и сборка может продолжаться. Вы можете прочитать больше об этом в Что тип лямбда-выражения?, Один из любопытных аспектов этого заключается в том, что тип лямбда-выражения не полностью выражен, пока один не назначит его переменной определенного типа или не использует ее возвращаемое значение, когда ожидаемый тип полностью известен. Примечание. Точная последовательность, с помощью которой здесь выводится тип лямбды, может немного отличаться, но важно то, что тип лямбды оценивается с учетом общего контекста, в который он вписывается.

Коллекторы

Другой интересный аспект Java 8 заключается в том, как он завершает конвейеры обработки, т.е. коллекторы. Вкратце, конвейеры могут начинаться с коллекции с .stream()методом, в котором она преобразуется в Stream. Многие операции в потоке, например. mapи flatMapт. д. могут быть прикованы к одному и тому же, что будет непрерывно преобразовывать поток в другой поток и еще один поток и так далее. На каком — то этапе вы могли бы вызвать такой метод, как findFirst, reduceили нечто подобное , который будет возвращать только одно значение. например. следующее вернет одно значение 109 из-за последнего вызова Redu.

Arrays.asList(1,2,3,4,5,6)
      .stream()
      .map(n -> n * n) // square the number
      .map(n -> n + 3) // add 3 to the value
      .reduce(0, (i, j) -> i + j)); // add the number to the accumulator

Однако во многих других случаях вы можете предпочесть коллекцию в результате. Здесь коллекционеры полезны.

Коллекционер обладает возможностями, необходимыми для

  • создать экземпляр необходимого объекта целевого класса. например. это может быть список (или другая выбранная вами коллекция)
  • взять каждый элемент из потока и соответствующим образом изменить целевой объект (например, добавить в список)
  • возьмите два экземпляра целевого объекта и объедините их в один экземпляр. (это связано с распараллеливанием — если несколько списков были построены параллельно, это помогает объединить их все в один).

Напомним, я повторю блок кода, который я показал ранее

List<String> result =  Arrays.asList("Larry", "Moe", "Curly")
                              .stream()
                              .map(s -> "Hello " + s)
                              .collect(Collectors.toList());

// result will be a List<String> containing "Hello Larry", "Hello Moe" and "Hello Curly"

В этом примере я использовал Collectors.toList()возвращаемый соответственно полезный коллектор. В большинстве случаев можно использовать аналогичные функции для создания соответствующего коллектора или создать коллектор, используя экземпляры трех функциональных интерфейсов для трех возможностей, перечисленных в предыдущем абзаце. Однако, если вы хотите тщательно изготовить свои коллекторы особым образом, вы можете написать свой коллектор и использовать то же самое. Это коллектор, который в конечном итоге возвращает результат конвейера, например. в случае выше, этоList<Integer>

Примечание. Не обязательно, чтобы сборщик всегда возвращал коллекцию. reduceОперация в предыдущем примере может также вместо этого была collectс коллектором , уменьшая значение в одно значение.

Ожидающие темы:

Надеюсь, я смог дать разумное представление о возможностях Java 8 Lambdas и о том, как их можно использовать. Однако есть много тем, которые я не смог осветить в этом посте и намерен осветить их в следующих постах. Вот некоторые из них:

  • Еще много примеров того, как использовать потоковые преобразования
  • Примитивы против объектов. Есть много методов и классов, которые помогают работать только на примитивах.
  • Затворы
  • Новые классы, такие как Optional
  • Аспекты, связанные с распараллеливанием
  • Ручная работа над некоторыми из ваших собственных классов