Что такое ленивая инициализация стрима
Немного о Stream API(Java 8)
Небольшая статья с примерами использования Stream API в Java8, которая, надеюсь, поможет начинающим пользователям освоить и использовать функционал.
Итак, что такое Stream API в Java8? «Package java.util.stream» — «Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections». Попробую дать свой вариант перевода, фактически это — поддержка функционального стиля операций над потоками, такими как обработка и «свёртка» обработанных данных.
«Stream operations are divided into intermediate and terminal operations, and are combined to form stream pipelines. A stream pipeline consists of a source (such as a Collection, an array, a generator function, or an I/O channel); followed by zero or more intermediate operations such as Stream.filter or Stream.map; and a terminal operation such as Stream.forEach or Stream.reduce» — описание с сайта.
Попробуем разобраться в этом определении. Авторы говорят нам о наличии промежуточных и конечных операций, которые объедены в форму конвейеров. Потоковые конвейеры содержат источник (например, коллекции и т.п.) за которым следуют промежуточные и конечные операции и приводятся их примеры. Тут стоит заметить, что все промежуточные операции над потоками — ленивые(LAZY). Они не будут исполнены, пока не будет вызвана терминальная (конечная) операция.
Еще одна интересная особенность, это – наличие parallelStream(). Данные возможности я использую для улучшения производительности при обработке больших объемов данных. Параллельные потоки позволят ускорить выполнение некоторых видов операций. Я использую данную возможность, когда знаю, что коллекция достаточно большая для обработки ее в «ForkJoin» варианте. Подробнее про ForkJoin читайте в предыдущей статье на эту тему — «Java 8 в параллель. Учимся создавать подзадачи и контролировать их выполнение».
Закончим с теоретической частью и перейдем к несложным примерам.
Пример показывает нахождение максимального и минимального значения из коллекции.
Немного усложним пример и добавим исключения (в виде null) при максимального значения в пример №2.
Усложним примеры. Создадим коллекцию «спортивный лагерь», состоящую из полей «Имя» и «Количество дней в спортивном лагере». Сам пример создания класса ниже.
А теперь примеры работы с новыми данными:
В примере было найдено имя, Ирина, которая будет находиться в лагере всех дольше.
Преобразуем пример и создадим ситуацию, когда у нас вкралась ошибка, и одна из записей null в имени.
В этом случае вы получите результат, равный «Name=null».Согласитесь, что мы хотели не этого.Немного изменим поиск по коллекции на новый вариант.
Полученный результат, «Ira» — верен.
В примерах показано нам нахождение минимальных и максимальных значений по коллекциям с небольшими дополнениями в виде исключения null значений.
Как мы говорили доступные методы можно разделить на две большие группы промежуточные операции и конечные. Авторы могут называть их различно, например, вариант названия конвейерные и терминальные методы употребляется в литературе и статьях. При работе с методами существует одна конструктивная особенность, вы можете «накидывать» множество промежуточных операций, в конце производя вызов одного терминального метода.
В новом примере добавим сортировку и вывод определенного элемента, например, добавим фильтр по именам с встречающимся «Ivan» и произведем подсчет таких элементов (исключим null значения).
Добавив в коллекцию new SportsCamp(«Ivan», 17), получим результат равный «countName=2». Нашли две записи.
В данных примерах использовалось создание стрима из коллекции, доступны и другие варианты, например, создание стрима из требуемых значений, например, Stream streamFromValues = Stream.of(«test1», «test2», «test3»), возможны и другие варианты.
Как говорилось выше, у пользователей есть возможность использовать «обработку» используя parallelStream().
Немного изменив пример, получим новый вариант реализации:
Особенность этого варианта состоит в реализации параллельного стрима. Хочется обратить внимание, что parallelStream() оправданно использовать на мощных серверах(многоядерных) для больших коллекций. Я не даю четкого определения и точного размера коллекций, т.к. очень много параметров необходимо выявить и просчитать. Часто только тестирование может показать вам увеличение производительность.
Мы немного познакомились с простыми операциями, поняли отличие между конвейерными и терминальными операциями, попробовали и те и другие. А теперь давайте посмотрим примеры более сложных операций, например, collect и Map, Flat и Reduce.
Еще раз заглянем в официальную документацию документацию и попробуем реализовать свои примеры.
В новом примере попробуем преобразовать одну коллекцию в другую, по именам начинающимся с «I» и запишем это в List.
Результат будет равен трём. Тут нужно обратить внимание, что порядок указания исключения null элементов значим.
Обратите внимание, что Collectors обладает массой возможностей, включая вывод среднего значения или информации со статистикой. Как пример, попробуем соединить данные, вот так:
Ленивая инициализация в C#
Отложенная инициализация или «ленивая» инициализация — это способ доступа к объекту, скрывающий за собой механизм, позволяющий отложить создание этого объекта до момента первого обращения. Необходимость ленивой инициализации может возникнуть по разным причинам: начиная от желания снизить нагрузку при старте приложения и заканчивая оптимизацией редко используемого функционала. И действительно, не все функции приложения используются всегда и, тем более, сразу, потому создание объектов, реализующих их, вполне рационально отложить до лучших времён. Я хотел бы рассмотреть варианты ленивой инициализации, доступные в языке C#.
Для демонстрации примеров я буду использовать класс Test, у которого есть свойство BlobData, возвращающее объект типа Blob, который по легенде создаётся довольно медленно, и было решено создавать его лениво.
Проверка на null
Самый простой вариант, доступный с первых версий языка, — это создание неинициализированной переменной и проверка её на null перед возвращением. Если переменная равна null, создаём объект и присваиваем этой переменной, а потом его возвращаем. При повторном обращении объект уже будет создан и мы сразу его вернём.
Объект типа Blob тут создаётся при первом обращении к свойству. Либо не создаётся, если он по какой-то причине в этой сессии программе не понадобился.
В C# есть тернарный оператор, позволяющий проверить условие и, если оно истинно вернуть одно значение, а если ложно, — другое. Мы можем использовать его для того, чтобы немного сократить и упростить код.
Суть осталась той же. Если объект не инициализирован, инициализируем и возвращаем. Ежели уже инициализирован, то просто сразу возвращаем.
is null
Ситуации бывают разные и мы, например, можем столкнуться с такой, в которой у класса Blob перегружен оператор ==. Для этого, вероятно, нам может потребоваться сделать проверку is null вместо == null. Доступно в свежих версиях языка.
Но это так, небольшое отступление.
Второй операнд пришлось взять в круглые скобки из-за приоритета операций.
Это позволило ещё немного сократить код.
Потоки
Если есть вероятность, что к данному ресурсу могут обращаться сразу несколько потоков, нам стоит сделать его потокобезопасным. В противном случае может случиться такая ситуация, что, например, оба потока проверят объект на null, результат окажется false, а затем будет создано два объекта типа Blob, нагрузив систему в два раза больше, чем нам хотелось, и кроме того, один из этих объектов сохранится, а второй будет потерян.
Так как у класса Blob есть конструктор без параметров, то Lazy сможет создать его в нужный момент без лишних вопросов. Если же нам нужно выполнить какие-то дополнительные действия во время создания объекта Blob, конструктор класса Lazy может принимать ссылку на Func
Кроме того, во втором параметре конструктора мы можем указать, нужна ли нам потокобезопасность (тот самый lock).
Свойство
Теперь давайте сократим запись readonly свойства, благо современный C# позволяет это делать красиво. В конечном итоге выглядеть всё это станет так:
LazyInitializer
Ещё есть вариант не оборачивать класс в обёртку Lazy, а вместо этого использовать функционал LazyInitializer. Этот класс имеет один статический метод EnsureInitialized с кучей перегрузок, позволяющих творить всякое, в том числе делать потокобезопасность и писать кастомный код для создания объекта, но основная суть которого заключается в следующем. Проверить, не инициализирован ли объект. Если нет, то инициализировать. Вернуть объект. С использованием данного класса, мы можем переписать наш код так:
Русские Блоги
java8 Stream Lazy объяснение (принцип неисполнения)
Использование «ленивой» операции типа Stream
Многие операции в коде являются нетерпеливыми, например, когда происходит вызов метода, параметры будут оцениваться немедленно. В целом, использование метода Eager упрощает само кодирование, но использование метода Lazy обычно означает лучшую эффективность.
В этой статье показано, как новые функции в Java 8 позволяют нам писать код Lazy более удобно.
Задержка инициализации
Для объектов, которые потребляют больше ресурсов, ленивая инициализация является лучшим выбором. Это не только экономит некоторые ресурсы, но и ускоряет создание объектов, тем самым повышая общую производительность.
Однако при реализации отложенной инициализации объекта важно отметить, что эти подробности реализации не должны предоставляться пользователю, то есть пользователь может использовать объект в соответствии с обычным процессом.
Типичная реализация
Используйте приведенный выше код:
Хотя он может обеспечить создание только одного тяжелого экземпляра, недостаток также очевиден: каждый раз, когда вы вызываете метод getHeavy, вам необходимо вводить область дорогостоящего синхронизированного кода. Фактически, только когда вам необходимо создать экземпляр Heavy в первый раз, вам необходимо обеспечить безопасность потоков. После создания экземпляра нет необходимости использовать синхронизированный для обеспечения безопасности потока.
Используйте лямбда-выражения
Здесь нам нужно использовать интерфейс функции Supplier, который определяет метод get для получения требуемого экземпляра:
Помимо использования лямбда-выражений для получения экземпляров, вы также можете использовать ссылки на методы (которые ссылаются на конструкторы), чтобы выполнить то же самое.
Когда экземпляр Holder создан, экземпляр Heavy еще не был создан. Ниже мы предполагаем, что три потока будут вызывать метод getHeavy, из которых первые два потока будут вызваны одновременно, а третий поток будет вызван позднее.
Когда текущие два потока вызывают этот метод, они вызывают метод createAndCacheHeavy, потому что этот метод синхронизирован. Таким образом, первый поток входит в тело метода, а второй поток начинает ждать. В теле метода сначала определяется, является ли текущее значение Heavy экземпляром HeavyInstance. Если нет, тяжелый объект будет заменен экземпляром типа HeavyFactory. Очевидно, что когда первый поток выполняет суждение, тяжелый объект является только экземпляром поставщика, поэтому тяжелый объект будет заменен экземпляром HeavyFactory, а затем будет фактически создан экземпляр Heavy. Когда второй поток входит для выполнения метода, heavy уже является экземпляром HeavyFactory, поэтому он немедленно вернется. Когда третий поток выполняет метод getHeavy, поскольку тяжелый объект в это время уже является экземпляром HeavyFactory, он будет напрямую возвращать требуемый экземпляр, который не имеет ничего общего с методом синхронизации createAndCacheHeavy.
Приведенный выше код фактически реализует облегченный шаблон виртуального прокси (Virtual Proxy Pattern). Убедитесь в правильности ленивой загрузки в различных средах.
Ленивая оценка
Фактически, некоторые места в языке Java применяют концепцию отложенной оценки, например, оценку логических выражений:
В исполнении fn1() || fn2() В это время, когда fn1 () возвращает true, fn2 () не будет выполняться. Аналогично в реализации fn1() && fn2() В это время, когда fn1 () возвращает false, fn2 () не будет выполняться. Это известно как «операция короткого замыкания».
Однако для вызова метода все входящие параметры оцениваются до фактического вызова, даже если некоторые параметры вообще не используются в методе. Следовательно, это может привести к потере производительности, поэтому мы можем использовать лямбда-выражения для улучшения.
Когда в списке параметров есть лямбда-выражения и ссылки на методы, этот тип параметра будет оцениваться компилятором Java только тогда, когда он действительно должен быть использован. Мы можем использовать это для реализации отложенной оценки. Многие методы недавно добавленного типа Stream в Java 8 реализуют отложенную оценку. Например, интерфейс функции Predicate, принятый методом filter, может вызываться не всеми элементами в коллекции. Поэтому мы можем рассмотреть параметры метода, чтобы сформировать функциональный интерфейс для достижения отложенной оценки.
Нетерпеливая оценка
В приведенном выше коде, хотя я хочу использовать операцию короткого замыкания для получения окончательного результата (input1 && input2), уже слишком поздно. При оценке параметров значения input1 и input2 фактически были подтверждены, как видно из выходных данных выше. Этот код будет выполняться не менее 4 секунд, что явно не оптимально.
Дизайн для отложенной оценки
Если мы знаем, что некоторые параметры в методе могут не использоваться, мы можем реорганизовать их и заменить их функциональными интерфейсами для реализации отложенной оценки. Например, операция короткого замыкания используется в вышеприведенном коде, указывая, что оценка input2 может быть ненужной, тогда вы можете заменить ее на интерфейс поставщика:
После замены функциональным интерфейсом типа поставщика, только когда вызывается его метод get, операция оценки будет фактически выполнена. Тогда приведенная выше операция короткого замыкания имеет смысл. Когда input1.get () возвращает false, input2.get () вообще не будет вызываться:
В это время время выполнения составляет чуть более 2 секунд. По сравнению с предыдущими 4 секундами производительность увеличилась почти на 100%. В ситуациях, когда определенные параметры не нужны, использование лямбда-выражений или ссылок на методы для реализации этих параметров действительно может повысить производительность, но это также делает код немного более сложным, но эти затраты также имеют смысл для повышения производительности.
Использование потока «ленивый»
Тип Stream был представлен в предыдущей статье, но нет упоминания о том, что тип Stream является «ленивым». На самом деле, благодаря этому «ленивому», производительность программы можно улучшить. Фактически, при использовании Stream ранее мы воспользовались его «ленью», и Stream будет выполнять операции оценки только тогда, когда это действительно необходимо.
Промежуточное и Терминальное Управление
Тип Stream имеет два типа методов:
Секрет «ленивости» Stream в том, что каждый раз, когда вы используете Stream, он соединяет несколько промежуточных операций и присоединяет конечную операцию в конце. Такие методы, как map () и filter (), являются промежуточными операциями, и при их вызове немедленно возвращается другой объект Stream. Для таких методов, как redu () и findFirst (), они являются конечными операциями, а реальные операции выполняются при их вызове для получения требуемых значений.
Например, когда нам нужно напечатать первое заглавное имя длины 3:
Вы можете подумать, что приведенный выше код будет выполнять множество операций над коллекцией имен, например сначала обойти коллекцию, чтобы получить все имена длиной 3, а затем обойти коллекцию из фильтра один раз, чтобы преобразовать имя в верхний регистр. Наконец, найдите первое из набора заглавных букв и вернитесь.
Но реальная ситуация не такова, не забывайте, что Stream очень «ленив», он не будет выполнять никаких дополнительных операций.
Порядок оценки метода
Но на самом деле, только когда вызывается метод findFirst, методы filter и map будут фактически запущены. Фильтр не будет фильтровать всю коллекцию сразу, он будет фильтровать по одному. Если найден элемент, который удовлетворяет условиям, он будет помещен в следующую промежуточную операцию, которая является методом карты. Следовательно, реальный порядок исполнения таков:
\
Вывод консоли такой:
Когда конечная операция получает необходимые ответы, весь процесс расчета заканчивается. Если ответ не получен, потребуется промежуточная операция для вычисления большего количества элементов коллекции, пока не будет найден ответ или пока не будет обработана вся коллекция.
JDK объединит все промежуточные операции в одну. Этот процесс называется операцией объединения. Следовательно, в худшем случае (то есть в коллекции нет элементов, отвечающих требованиям), коллекция будет проходиться только один раз, вместо того, чтобы выполнять несколько обходов, как мы себе представляли.
Чтобы увидеть, что происходит внизу, мы можем разделить вышеуказанные операции над Stream по типу:
По результатам вывода мы можем обнаружить, что после объявления промежуточной операции над объектом Strema промежуточная операция не была выполнена. Только когда вызов findFirst () действительно произойдет, промежуточная операция будет выполнена.
Создать бесконечную коллекцию
Еще одной особенностью типа Stream является то, что они могут быть неограниченными. Это не то же самое, что типы коллекций. Типы коллекций в Java должны быть ограничены. Причина, по которой Stream может быть бесконечным, также обусловлена характеристикой «лени» Stream.
Например, мы можем использовать тип Stream для выражения последовательности простых чисел. Во-первых, нам нужен инструментальный метод, чтобы определить, является ли число простым числом:
Здесь используется другая особенность IntStream, то есть метод rangeClosed используется для получения объекта IntStream, представляющего определенный диапазон. Затем используется метод noneMatch объекта Stream. Этот метод будет принимать интерфейс функции типа Predicate в качестве параметра. Только когда все элементы в Stream не удовлетворяют Predicate, он возвращает true.
Таким образом, мы можем произвольно указать начальную точку, чтобы получить все простые числа, начиная с этой начальной точки:
После реализации метода concat, если вы запустите этот код, он быстро вернет большое StackOverflowError. Это потому, что набор Java должен быть ограничен, и, очевидно, приведенный выше код пытается использовать ограниченный набор для представления бесконечной последовательности простых чисел. Причина этого StackOverflowError в том, что существует слишком много слоев рекурсивных вызовов.
Так почему же Stream может представлять бесконечную коллекцию? Это также вытекает из «ленивых» характеристик Stream. Stream возвращает только нужные вам элементы, а не всю бесконечную коллекцию одновременно.
В интерфейсе Stream есть статический метод iterate (), который может создать для вас бесконечный объект Stream. Необходимо принять два параметра:
seed представляет начальную точку этой бесконечной последовательности, а UnaryOperator представляет, как получить следующий элемент на основе предыдущего элемента, например, второй элемент в последовательности может быть определен следующим образом: f.apply(seed) 。
Поэтому, основываясь на начальной точке и требуемом количестве простых чисел, мы можем написать следующий код:
Для итерации и ограничения они являются только промежуточными операциями, а результирующий объект по-прежнему имеет тип Stream. Для метода сбора это конечная операция, которая инициирует промежуточную операцию для получения желаемого результата.
Вызов метода простых чисел также очень интуитивен:
Ленивая инициализация в Spring Boot 2.2
От переводчика: поскольку Spring Framework является одним из основных фреймворков, на которых мы строим CUBA, то новости о новых возможностях Spring не проходят незаметно для нас. Ленивая инициализация — один из способов уменьшить время первой загрузки приложения, что в наш век повсеместного использования микросервисов является важной метрикой. Для тех, кто предпочитает чтению просмотр видео, есть 10-ти минутное выступление Josh Long, посвященное теме статьи.
Недавно анонсированный первый milestone релиз Spring Boot 2.2 добавляет поддержку ленивой инициализации. В этой статье мы рассмотрим новую функциональность и объясним, как ее включить.
Что это значит — быть ленивым?
Spring Framework поддерживает ленивую инициализацию с тех пор, как его исходный код переехал в git одиннадцать лет назад. По умолчанию, когда контекст приложения обновляется, каждый бин создается заново и производится внедрение его зависимостей. В отличие от этого, если бин сконфигурирован для ленивой инициализации, он не будет создан и его зависимости не будут проставлены, пока в этом нет необходимости.
Включение ленивой инициализации
Преимущества ленивой инициализации
Ленивая инициализация может заметно уменьшить время старта вашего приложения, поскольку на этом этапе загружается меньше классов и создается меньше бинов. Например, маленькое веб-приложение которое использует Actuator и Spring Security, обычно стартует 2,5 секунды. А с ленивой инициализацией этот процесс занимает 2 секунды. Точные величины ускорения будут меняться от приложения к приложению, в зависимости от структуры графа зависимостей бинов.
Примечание переводчика: я запускал вот этот пример, прописав в зависимостях Spring Boot 2.2, и время запуска с ленивой инициализацией было 3 секунды, а без нее — 4. Думаю, что на более серьезных приложениях, существенного выигрыша во времени старта за счет использования ленивой инициализации мы не увидим. Upd: по совету alek_sys отключил валидацию и обновление схемы БД и включил ленивую инициализацию JPA для обоих случаев — получилось 2.7 и 3.7 секунд до появления надписи Started WebApplication in. соответственно
А что там насчет DevTools?
Spring Boot DevTools предоставляют заметное ускорение разработки. Вместо перезапуска JVM и приложения каждый раз, когда вы что-то меняете, DevTools делают “горячий перезапуск” приложения в той же самой JVM. Значительное преимущество такого перезапуска в том, что он дает JIT возможность оптимизировать код, который исполняется при старте приложения. После нескольких перезапусков, исходное время в 2,5 секунды уменьшается почти на 80% до 500 мс. С ленивой инициализацией все обстоит ещё лучше. Установка свойства spring.main.lazy-initialization показывает время перезапуска непосредственно в IDE равное 400 мс.
Обратная сторона ленивой инициализации
Как было показано выше, включение ленивой инициализации может достаточно серьезно уменьшить время запуска приложения. И, возможно, у вас будет непреодолимое желание использовать это постоянно или, как минимум, вы будете задаваться вопросом, почему ленивая инициализация не включена по умолчанию. Есть несколько возможных негативных эффектов, которые лучше прояснить сразу.
Тот факт, что классы не загружаются, а бины не создаются до того момента, пока они не потребуются, может маскировать проблемы, которые раньше могли бы быть выявлены уже на этапе запуска приложения. Например, это может быть отсутствие нужного класса, переполнение памяти или ошибка, связанная с неправильной конфигурацией.
В веб-приложениях ленивая конфигурация может увеличить латентность HTTP запросов, которые вызывают инициализацию бинов. Обычно это первый запрос, но могут быть дополнительные нежелательные эффекты, затрагивающие балансировку нагрузки или автоматическое масштабирование.
Эта штука включена?
Когда включать ленивую инициализацию?
Как мы уже видели выше, ленивая инициализация предлагает заметные улучшения во время запуска приложения, но также есть и обратные стороны, так что нужно очень аккуратно использовать эту возможность.
Одна область, где ленивая инициализация может принести дивиденды (почти без накладных расходов) — это процесс разработки приложения. Пока вы пишете приложение, уменьшенное время перезапуска, которое обеспечивается ленивой инициализацией в комбинации с DevTools, может значительно сэкономить вам время.
Где ещё можно получить преимущества от использования ленивой инициализации — так это в интеграционных тестах. Вы, возможно, уже используете «нарезку» тестов для уменьшения времени выполнения, ограничивая количество инициализирующихся бинов в некоторых типах тестов. Ленивая же инициализация предоставляет альтернативную возможность для достижения такого же результата. Если вы не в той должности, чтобы менять структуру приложения для “нарезать” тестов, или для конкретно ваших тестов нет подходящей “нарезки”, то включение ленивой инициализации ограничит количество бинов теми, которые используются только в вашем тесте. Это уменьшит время выполнения теста, особенно если они запускаются в изолированной среде во время разработки.