Вопросы и ответы по стримам Java. 1 Функциональные интерфейсы, основные типы 2 Что такое лямбда выражение и ссылка на метод 3 Как они связаны с анонимным классом
![]()
|
1) Функциональные интерфейсы, основные типы 2) Что такое лямбда выражение и ссылка на метод 3) Как они связаны с анонимным классом 4) Что такое Стрим 5) Какие бывают стримы (По разным критериям, например "конечные и бесконечные") 6) Терминальные и промежуточные методы 7) Способы получения стрима Функциональное программирование (//1) Функциональное программирование в Java — это подход в программировании особого вида, когда акцент программного кода сводится к использованию функций, а не инструкций. При таком подходе каждая отдельная функция используется как объект. А это значит, что функцию можно: * присвоить переменным * передать в виде аргумента другой функции * возвратить в качестве результата от другой функции Функция в Java – это анонимный интерфейс, объявленный в каком-либо методе. С подобным интерфейсом можно делать то же самое, что и с обычной функцией в функциональных языках. Функциональное программирование в Java реализуется по следующим принципам: * Функции реализовываются их через «анонимные» интерфейсы и методы. * Чистые функции – функции которые при одинаковых входных данных, всегда возвращают одинаковый вывод. * Неизменяемые данные и состояния - при соблюдении этого принципа каждая функция воспроизводит один и тот же результат и не имеет зависимости от состояния программы. * Рекурсия. Это способ осуществлять перебор информации в функциональном программировании без использования цикла «if...else». * Первоклассность функций - позволяет применять функцию как обычное значение переменной. * Высший порядок функции - позволяет одной функции использовать другую функцию в качестве своего аргумента или возвращать ее. * Композиция функций - подразумевает построение структуры из функций, где результат выполнения одной функции будет передаваться в другую функцию, и так дальше по цепочке. Таким образом, при помощи вызова одной функции можно спровоцировать исполнение целой цепочки функций. Преимущества Более легкая отладка за счет использования «чистых» функций и неизменных данных Отложенные вычисления (Ленивая оценка) - функциональная программа вычисляется только при необходимости Модульность - «чистые» функции можно использовать в разных областях одного кода Облегченное параллельное программирование Простота понимания чистых функций, они зависят только от входных данных и не меняют состояние программы Недостатки Неизменяемость и рекурсия иногда могут приводить к снижению производительности Многие объекты создаются в момент кодирования, потому его сложно поддерживать Объединение чистых функций с другими операциями ввода-вывода Императивный vs декларативный подход. Декларативное программирование -парадигма программирования, в которой задаётсяспецификация решения задачи, то есть описывается ожидаемый результат, а не способ его получения. Императивное программирование — это парадигма программирования, в которой задаётся последовательность действий, необходимых для получения результата. Декларативный стиль Такой стиль, в котором вы описываете, какой именно результат вам нужен. И получатель такого сообщения уже сам разбирается, какие шаги для этого надо предпринять. ФУНКЦИОНАЛЬНЫЕ ИНТЕРФЕЙСЫ 1) Функциональные интерфейсы, основные типы (//1, //2) Функциональный интерфейс в Java – это интерфейс, который содержит только 1 абстрактный метод, а переменной этого типа-интерфейса можно присвоить значение, заданное лямбда-выражением. К функциональному интерфейсу можно добавить аннотацию @FunctionalInterface. Это не обязательно, но при наличии данной аннотации код не скомпилируется, если будет больше или меньше, одного абстрактного метода. Рекомендуется добавлять @FunctionalInterface. Это позволит использовать интерфейс в лямбда выражениях, не остерегаясь того, что кто-то добавит в интерфейс новый абстрактный метод и он перестанет быть функциональным. Основное назначение – использование в лямбда выражениях и ссылках на метод. Основные типы Predicate — функциональный интерфейс для проверки соблюдения некоторого условия. Если условие соблюдается, возвращает true, иначе — false. Содержит метод boolean test(T t). Consumer (с англ. — “потребитель”) — принимает в качестве входного аргумента объект типа T, совершает некоторые действия, но при этом ничего не возвращает. Содержит метод void accept(T t). BiConsumer (с англ. — “потребитель”) — принимает в качестве входного аргумента 2 объекта типа T, U, совершает некоторые действия, но при этом ничего не возвращает. Содержит метод void accept(T t, U u). Supplier (с англ. — поставщик) — не принимает никаких аргументов, но возвращает некоторый объект типа T. Содержит метод T get(). Function —принимает аргумент T и приводит его к объекту типа R, который и возвращается как результат. Содержит метод R apply(T t); UnaryOperator — принимает в качестве параметра объект типа T, выполняет над ним некоторые операции и возвращает результат операций в виде объекта того же типа T. Содержит метод T apply(T t). BinaryOperator — принимает в качестве параметра два объекта типа T, выполняет над ними бинарную операцию и возвращает ее результат также в виде объекта типа T. Содержит метод T apply(T t1, T t2). Отличие BinaryOperator от Function Function (функция) используется для преобразования входного параметра или в двух параметров (BiFunction) в какое-либо значение, тип значение может не совпадать с типом входных параметров. BinaryOperator - это разновидность Function, в которых входные и выходные обобщенные параметры должны совпадать. Если заглянуть в пакет java.util.function, то можно заметить, чтоUnaryOperator расширяет Function, а BinaryOperator расширяет BiFunction. Дополнительные функциональные интерфейсы: Runnable, Comparator Что такое абстрактный метод? У абстрактных методов нет тела Реализация абстрактных методов предоставляется классам, реализующим данный интерфейс. Абстрактные методы считаются публичными и абстрактными даже, если это не задано явно. Абстрактные методы не могут быть финальными, поскольку комбинация abstract и final запрещена. Может ли функциональный интерфейс содержать что-то кроме абстрактного метода? Функциональный интерфейс может содержать так же default и static методы, а так же дополнительные абстрактные методы, определенные в классе Object (equals/ toString / clone). Что такое default методы в интерфейсе и для чего они были введены? Дефолтные методы – это методы, которые реализованы непосредственно в теле интерфейса. Введены во избежание массового дублирования реализации в имплементирующих их классах (их можно переопределить). В функциональных интерфейсах можно реализовывать сколько угодно дефолтных методов. Вызвать default-метод интерфейса в реализуемом его классе можно используя ключевое слово super вместе с именем интерфейса. Так же, если интерфейс Y наследуется от функционального интерфейса I, и при этом в Y есть только дефолтные или статические методы, то Y так же считается функциональным. Static методы в функциональных интерфейсах? Static – методы в интерфейсе — это тоже, что и static методы в абстрактном классе. Статические методы в интерфейсе являются частью интерфейса, мы не можем использовать его для объектов класса реализации. Они нужны для обеспечения вспомогательных методов, например, проверки на null, сортировки коллекций и т.д. Статические методы в интерфейсе помогают обеспечивать безопасность, не позволяя классам, которые реализуют интерфейс, переопределить их. Все способы реализации функциональных интерфейсов? Экземпляры функциональных интерфейсов можно создавать с помощью: Лямбда-выражения (number -> System.out.println(number)) Ссылкой на метод (System.out::println) Ссылка на конструкторы (ClassName::new) / (Class Через обычную имплементацию в классе Через анонимный класс Расскажите про Comparator и Comparable? Интерфейс Comparable используется только для сравнения объектов класса, в котором данный интерфейс реализован. Т.е. interface Comparable определяет логику сравнения объекта определенного ссылочного типа внутри своей реализации (по правилам разработчика). Интерфейс Comparator представляет отдельную реализацию и ее можно использовать многократно и с различными классами. Т.е. interface Comparator позволяет создавать объекты, которые будут управлять процессом сравнения (например, при сортировках). В отличие от Comparable, Comparator может дополнительно разрешать сравнение нулевых аргументов. Контракт Comparator-а * отрицательный int (первый объект отрицательный, то есть меньше) * положительный int (первый объект положительный, хороший, то есть больший) * ноль = объекты равны Если класс по какой-то причине не может реализовать интерфейс Comparable, или же просто нужен другой вариант сравнения, используется интерфейс Comparator. Интерфейс содержит метод int compare(T o1, T o2), который должен быть реализован классом, реализующим компаратор. Метод compare возвращает числовое значение - если оно отрицательное, то объект o1 предшествует объекту o2, иначе - наоборот. А если метод возвращает ноль, то объекты равны. Для применения интерфейса нам необходимо создать класс компаратора. ЛЯМБДА 2) Что такое лямбда выражение и ссылка на метод Что такое лямбда выражение? Лямбда выражение – упрощенная запись анонимного класса, реализующего функциональный интерфейс. Основу лямбда-выражения составляет лямбда-оператор -> он разделяет лямбда-выражение на две части: левая часть содержит список параметров, а правая тело лямбда-выражения, где выполняются все действия Как взаимосвязаны лямбда и функциональный интерфейс? 1) Лямбда-выражение не выполняется само по себе, а образует реализацию абстрактного метода, определенного в функциональном интерфейсе. 2) Лямбда выражения не содержат информацию о том, какой функциональный интерфейс они реализуют. 3) Тип выражения выводится из контекста, в котором используется лямбда выражение. Этот тип называется целевой тип (target type). 4) Если лямбда выражение присваивается какому-то интерфейсу, лямбда выражение должно соответствовать синтаксису метода интерфейса. 5) Одно и то же лямбда выражение может использоваться с разными интерфейсами, если они имеют абстрактные методы, которые совместимы.
К каким переменным можно обращаться внутри лямбды? Изнутри лямбда-выражения можно не только обращаться ко всем «видимым» переменным, но и вызывать те методы, к которым есть доступ, а именно: К переменные внутри метода (эффективно-финальные) К static переменным класса К переменным интерфейса (с которым лямбда работает) К переменным из параметров метода Может ли лямбда-выражение быть в несколько строк? Да. Тело после -> должно обрамляться фигурными скобками {}, каждая строчка заканчиваться точкой с запятой. Если лямбда-выражение возвращает результат, ключевое слово return также требуется. Что такое Method References (ссылка на метод)? Если лямбда выражения вызывают только один существующий метод, лучше сделать ссылку на этот метод по его имени. Method References – это компактные лямбда выражения для методов, у которых уже есть имя. Например: Consumer можно переписать с помощью method references: Consumer В каком виде передается Method References (ссылка на метод)? имя_класса:: имя_стат_метода (для статического метода); объект_класса:: имя_метода (для метода экземпляра); название_класса:: new (для конструктора). АНОНИМНЫЕ КЛАССЫ 3) Как они связаны с анонимным классом (лямбда выражения) Анонимные классы Анонимный класс – это класс без имени. Использование таких классов обычно обусловлено необходимостью однократного создания объекта класса, реализующего тот или иной абстрактный класс или интерфейс. Анонимный класс является подклассом существующего класса или реализацией интерфейса, т.е. анонимный класс всегда от кого-то наследуется. Поскольку анонимный класс не имеет имени, он не может иметь явный конструктор. Анонимные классы никогда не могут быть статическими, либо абстрактными, и всегда являются конечными классами. Анонимный класс может реализовать только один интерфейс. Кроме того, каждое объявление анонимного класса уникально. Как создать? Объявление такого класса выполняется одновременно с созданием его объекта посредством использования оператора new.
Любой ли анонимный класс можно заменить на лямбду? Только анонимные классы, которые являются реализациями функциональных интерфейсов (например, Runnable, ActionListener, Comparator, Predicate), могут быть заменены выражением лямбда. Т.е. только те классы, которые реализуют 1 абстрактный метод. Можно ли создать анонимный класс от String? Нет, так как Анонимный класс является подклассом существующего класса или реализации интерфейса, т.е. АК всегда от кого-то наследуется, а класс String является final, т.е. от него нельзя наследоваться. Как лямбда выражения связаны с анонимными классами? Лямбда выражения являются альтернативой анонимным классам. Но они не одинаковы. Лямбда-выражения появились в Java 8, как способ имплементации анонимных методов и, в некоторых случаях, как альтернатива анонимным классам. Общее: Локальные переменные могут быть использованы только если они final или effective final. Разрешается доступ к переменным класса и статическим переменным класса. Они не должны выбрасывать больше исключений чем определено в throws метода функционального интерфейса. Различия: Для анонимных классов ключевое слово this ссылается на сам класс. Для лямбда выражений на внешний класс. Анонимные классы компилируются во внутренние классы. А лямбда выражения преобразуются в статические private методы класса, в котором они используют invokedynamic инструкцию. Лямбда более эффективны, так как не надо загружать ещё один класс. СТРИМЫ 4) Что такое Стрим Java Stream – это объект (поток данных), способный выполнять внутреннюю итерацию своих элементов, то есть он может выполнять итерацию своих элементов сам. Stream представляет собой последовательность элементов, над которой можно производить различные операции. Для чего нужен? Stream API — нужен для работы со структурами данных в функциональном стиле. С появлением Java 8 Stream API позволило программистам писать существенно короче то, что раньше занимало много строк кода, а именно — упростить работу с наборами данных, в частности, упростить операции фильтрации, сортировки и другие манипуляции с данными. Когда их лучше использовать? Стримы используются тогда, когда вам необходимо выполнить несколько промежуточных операций над данными, например коллекцией. В каком пакете находится Stream? Вся основная функциональность данного API сосредоточена в пакете java. util. stream. Чем Stream отличается от итератора? Порядок в обходе оп итератору может быть задан и заранее определен. Стоимость доступа к элементам в стримах гораздо ниже. Протокол Iterator принципиально менее эффективен. Для получения каждого элемента требуется вызов двух методов. Кроме того, поскольку итераторы должны быть устойчивы к таким вещам, как вызов next() без hasNext() или hasNext() несколько раз без next(), оба этих метода обычно должны выполнять некоторую защитную кодировку, что увеличивает неэффективность. Сравнение стримов с коллекцией? (дополнить) Разница между Collection и Stream в том, что коллекции позволяют работать с элементами по-отдельности, тогда как поток (Stream) не позволяет. Например, с использованием коллекций, вы можете добавлять элементы, удалять, и вставлять в середину. - Потоки не хранят элементы. Элементы, используемые в потоках, могут храниться в коллекции, либо при необходимости могут быть напрямую сгенерированы. 5) Какие бывают стримы 1) Конечные и бесконечные Бесконечные - создают бесконечный поток данных, до срабатывания условия (создание через iterate и generate). Конечные стримы создаются методом stream(). 2) Последовательные и параллельные Стримы бывают последовательными (sequential) и параллельными (parallel). Последовательные выполняются только в текущем потоке. Параллельные используют общий пул ForkJoinPool.commonPool(). При этом элементы разбиваются (если это возможно) на несколько групп и обрабатываются в каждом потоке отдельно. Затем на нужном этапе объединяются в один, для предоставления конечного результата. Чтобы получить параллельный стрим, нужно либо вызвать метод parallelStream() интерфейса Collection вместо stream(), либо превратить обычный стрим в параллельный, вызвав промежуточный оператор parallel. 3) Объектные и примитивные Стримы по умолчанию объектные, но существуют специальные стримы для примитивов: LongStream() DoubleStream() IntStream() Для чего нужны параллельные стримы? Распараллеливание потоков позволяет задействовать несколько ядер процессора (если целевая машина многоядерная) и тем самым может повысить производительность и ускорить вычисления. В то же время говорить, что применение параллельных потоков на многоядерных машинах однозначно повысит производительность - не совсем корректно. В то же время если целевая машина не является многоядерной, то поток будет выполняться как последовательный. Из каких частей состоит использование стримов? (этапы работы со стримом) 1. Создание стрима 2. Промежуточные (конвейерные) операции (или их отсутствие) 3. Терминальная операция (запускает весь процесс вычисления) 6) Терминальные и промежуточные методы Промежуточные операции возвращают трансформированный поток. К возвращенному потоку также можно применить промежуточные операции, либо терминальную операцию. Терминальные (конечные) операции возвращают конкретный результат. После этого никаких промежуточных операций применять нельзя. Две терминальные операции в одном выражении? нет Может ли стрим использоваться повторно? Нет, каждый стрим одноразовый. Приведи пример терминальной и промежуточной операции над стримом? Промежуточные операции concat - объединяет два потока distinct - возвращает поток, в котором имеются только уникальные данные с типом T dropWhile(predicate) - пропускает элементы, которые соответствуют условию в predicate, пока не попадется элемент, который не соответствует условию. Выбранные элементы возвращаются в виде потока filter(predicate) - фильтрует элементы в соответствии с условием в предикате limit - оставляет в потоке только maxSize элементов map - преобразует элементы типа T в элементы типа R и возвращает поток с элементами R flatMap - позволяет преобразовать элемент типа T в несколько элементов типа R и возвращает поток с элементами R skip - возвращает поток, в котором отсутствуют первые n элементов sorted - возвращает отсортированный поток sorted(comparator) - возвращает отсортированный поток (в соответствии с компаратором) takeWhile(predicate) - выбирает из потока элементы, пока они соответствуют условию в predicate. Выбранные элементы возвращаются в виде потока Терминальные операции void forEach(action) - для каждого элемента выполняется действие action collect(Collector super T,A,R> collector) - добавляет элементы в неизменяемый контейнер Optional Optional Object[] toArray() - возвращает массив из элементов потока Optional Optional boolean allMatch(predicate) - возвращает true, если все элементы потока удовлетворяют условию в предикате boolean anyMatch(predicate) - возвращает true, если хоть один элемент потока удовлетворяют условию в предикате long count() - возвращает количество элементов в потоке В чем разница методов map и flatMap? Оба map и flatMap могут быть применены к Stream Операция map(О работе map на русском) в качестве аргумента принимает Function(например, лямбду), которая вызывается для каждого значения входного стрима(который Т.е. map для каждого объекта в стриме возвращает по 1 объекту, потом преобразует все объекты в итоговый стрим. Операция flatMap(О работе flatMap на русском) принимает функцию (которая преобразует каждое значение входного стрима в стрим), применяет ее к каждому элементу, и на выходе возвращает стрим с одним, несколькими или ни c одним из элементов для каждого элемента входящего стрима. В чем разница методов peek и forEach? Оба применяют функцию к каждому элементу стрима, но peek возвращает стрим, а forEach возвращает другой объект. В чем разница методов forEach и forEachOrdered? У forEach порядок при параллельном выполнении не гарантируется. В чем разница методов list() и walk()? Files.list() возвращает массив файлов в указанной директории. Files.walk() возвращает поток файлов в указанной директории и субдиректориях. В чем разница методов range и rangeClosed? В range(1, 10) в диапазон не включено число 10, а в rangeClosed(1, 10) число 10 включено. Что такое Optional и зачем нужно? Задачей этого класса является предоставление решений на уровне типа-обертки для обеспечения удобства обработки возможных null-значений. Optional фактически обертывает результат операции. После выполнения операции с помощью метода get() объекта Optional мы можем получить его значение 7) Способы получения стрима Из коллекций – через метод .stream() Из значений – через Stream.of(….) Из массива – через Arrays.stream(arr[]) Из файла – через Files.walk() Из строки – через chars() получить IntStream chars из строки С помощью Stream.builder() и метода add() можно получить поток объектов С помощью Stream.generate() и Stream.iterate() можно получить бесконечные стримы С помощью Stream.empty() можно получить пустой стрим Как получить стрим диапазона чисел? IntStream.range(0, 10) .collect(Collectors.toList()); Что такое ленивая инициализация стрима? Ленивая инициализация — это концепция отсрочки создания объекта до тех пор, пока объект не будет фактически впервые использован, а все промежуточные операции выполняются только тогда, когда есть терминальная операция. При правильном использовании это может привести к значительному повышению производительности. Можно ли конкатенировать стримы? если да, то каким методом? Объединение с помощью метода .concat(stream1, stream2); В каком случае нужно закрывать стрим? Только потоки, источником которых является канал ввода-вывода, например Files.lines(Path, Charset) должны быть закрыты. Остальные реализуют AutoClosable. Что такое коллекторы? Коллекторы – это объекты, разработанные для какой-либо агрегации потоковых данных. Поток, пройдя через цепочку отображений, фильтров и других не терминальных операций, по итогу представляет из себя поток объектов. Их необходимо либо собрать воедино, либо выбрать какой-то один, а может быть, и вообще получить новый объект на основе каких-то характеристик. В этой точке и могут быть применены коллекторы. Что такое саплайер-поставщик? Интерфейс Supplier используется тогда, когда на вход не передаются значения, но необходимо вернуть результат. Вне вопросов: Лямбда представляет набор инструкций, которые можно выделить в отдельную переменную и затем многократно вызвать в различных местах программы. Основу лямбда-выражения составляет лямбда-оператор, который представляет стрелку ->. Синтаксис лямбда выражения имеет вид: параметры -> {тело функции}; ![]() |