Файл: И в чем его отличие от программирования в стиле ооп или в процедурном стиле Функциональное программирование это парадигма (стиль) программирования, в которой программа.docx

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 06.11.2023

Просмотров: 23

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Зачем нужно функциональное программирование, где оно применяется и в чем его отличие от программирования в стиле ооп или в процедурном стиле?

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

Опирается на вычисление выражений (декларативный подход), а не на выполнение команд (императивный подход).

Применяется в Реактивном программированим: ФП является хорошей парадигмой для разработки реактивных систем, которые реагируют на изменения вход

Плюсы

  • Избегание побочных эффектов: ФП старается минимизировать изменяемое состояние и побочные эффекты, что делает программы более предсказуемыми и устойчивыми к ошибкам.

  • Упрощение параллельного и распараллеленного программирования: ФП поддерживает прозрачное параллелизм и позволяет использовать параллельные вычисления для ускорения программ.

  • Большой акцент на рекурсию: ФП обычно предпочитает рекурсивные алгоритмы перед циклами, что может сделать некоторые задачи более простыми для реализации и понимания.

  • Высокий уровень абстракции: ФП позволяет использовать функции в качестве абстрактных строительных блоков, которые можно комбинировать и компоновать для создания более сложных программ.

Минусы

  • У ФП есть проблемы с инкапсуляцией,

  • ФП не пытается моделировать реальный мир. В ФП функциям позволено существовать отдельно от объектов, а для человека это сложно понять.

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

Функциональный интерфейс в Java – это интерфейс, который содержит только 1 абстрактный метод.

Функциональный интерфейс может содержать любое количество методов с уровнем доступа `default` или `static`.

Default методы можно переопределить.

Основное назначение – использование в лямбда выражениях и ссылках на метод.

Если вы попытаетесь написать два абстрактных метода в интерфейсе, помеченном аннотацией @FunctionalInterface, то компилятор выдаст ошибку.

Зачем нужна аннотация @FunctionalInterface? Обязательна ли она?

Аннотация не обязательна, но при её наличии код не скомпилируется, если будет больше или меньше, чем 1 абстрактный метод.


Рекомендуется добавлять @FunctionalInterface. Это позволит использовать интерфейс в лямбда выражениях, не остерегаясь того, что кто-то добавит в интерфейс новый абстрактный метод и он перестанет быть функциональным

Что такое default методы в интерфейсе и для чего они были введены?

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

Если у вас есть интерфейс, который другие люди внедрили, то если вы добавите новый метод в интерфейс, все существующие реализации будут нарушены.

Добавив новый метод с реализацией default, вы остаётесь совместимым с исходным кодом с существующими реализациями.

default-метод интерфейса доступен для вызова в реализующем его классе, как любой другой метод.

Может ли функциональный интерфейс содержать что-то кроме абстрактного метода?

Функциональный интерфейс может содержать любое количество методов с уровнем доступа `default` или `static`.

Интерфейсы могут содержать поля, так же, как и обычные классы, но с несколькими отличиями:

- Поля должны быть проинициализированы.

- Поля считаются публичными статическими финальными.

- Модификаторы `public`, `static` и `final` не нужно указывать явно (они «проставляются» по умолчанию).

Основные семейства функциональных интерфейсов (они обобщённые <>)? (6 семейств)

Predicate

BinaryOperator

UnaryOperator

Function

Consumer

Supplier

Все способы реализации функционального интерфейса?

1. С помощью анонимного класса

2. С помощью лямбда - выражения.

3. С помощью ссылки на метод

4. С помощью обычного класса, путем имплементации

Что такое метод референс?

Ссылки на методы (Method References) – это компактные лямбда выражения для методов, у которых уже есть имя.

Он представляет собой альтернативный синтаксис для использования лямбда-выражений, когда лямбда-выражение просто вызывает метод или конструктор без дополнительной логики.

Существует 4 вида ссылок на методы:

1. На статический метод (`ContainingClass::staticMethodName`) –(имя класса:: имя стат. метода)

2. На метод конкретного объекта (`ContainingObject::instanceMethodName`)

3. На метод объекта конкретного типа(`ContainingType::methodName`) (объект класса:: имя метода)

4. На конструктор (`ClassName::new`), для дженериков((generics)`Class::new`) – (название класса:: new)

Что такое «анонимные классы»?

Анонимные классы (anonymous classes) в Java — это специальный вид классов, которые могут быть определены и созданы в одном выражении без явного объявления класса. Они позволяют создавать временные классы с реализацией интерфейсов или расширением других классов на месте, где они не требуются в дальнейшем.

Анонимный внутренний класс – это класс без имени, он должен использоваться, если вам необходимо переопределить методы класса или интерфейса.

Основные особенности анонимных классов:

  1. Они не имеют явного имени и не могут быть повторно использованы в других местах программы.

  2. Они могут быть созданы как реализации интерфейсов или расширения существующих классов.

  3. Обычно используются для создания и передачи реализации обратных вызовов (callback) или обработчиков событий.

  4. Могут переопределять методы, добавлять новые методы или определять поле на месте.

У анонимных классов нет Конструкторов.

Как создать экземпляр анонимного класса?

Для создания экземпляра анонимного класса в Java нужно выполнить следующие шаги:

  1. Определить интерфейс или класс, которые вы хотите реализовать или расширить анонимным классом.

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

  3. Внутри фигурных скобок определить методы или поля анонимного класса. Можно переопределить методы существующего класса или реализовать методы интерфейса.

  4. Завершить создание анонимного класса с помощью точки с запятой.

Данный синтаксис не позволяет указать extends, implements или имя класса. В следствии этого анонимный класс может реализовать только один интерфейс.

Анонимные классы, имеют доступ к локальным переменным своего блока кода которые объявлены как final или они должны быть effectively final.

Что такое лямбда-выражение? Как его записать?

Лямбда-выражение (lambda expression) в Java является компактным способом представления анонимной функции. Оно позволяет передавать функциональность как аргумент метода или хранить ее в переменных. Лямбда-выражения широко используются в функциональном программировании и в контексте использования функциональных интерфейсов.

Они позволяют написать метод и сразу же использовать его. Особенно полезно в случае однократного вызова метода, т.к. сокращает время на объявление и написание метода без необходимости создавать отдельный класс.

Лямбда-выражения могут использоваться только в том случае, если вам нужно переопределить не более одного метода.

Лямбда-выражение состоит из следующих элементов:

  1. Список параметров: Это список параметров, которые принимает функция. Он может быть пустым, содержать один параметр или несколько параметров. Каждый параметр указывается с его типом или без типа, если он может быть выведен автоматически.

  2. Стрелка ->: Это оператор стрелки, который разделяет список параметров и тело лямбда-выражения.

  3. Тело лямбда-выражения: Это блок кода или выражение, которое определяет функциональность лямбда-выражения. В случае блока кода он заключается в фигурные скобки {}. В случае выражения, которое должно быть возвращено, фигурные скобки могут быть опущены.

Расскажите про Comparator и Comparable?

`Comparator` и `Comparable` - это оба интерфейсы, которые используются для сравнения объектов.

Comparable содержит единственный метод compareTo(), который принимает другой объект в качестве аргумента и возвращает отрицательное целое число, ноль или положительное целое число в зависимости от того, какой объект считается "меньшим", "равным" или "большим".

Реализация интерфейса Comparable позволяет объектам быть сравниваемыми и использоваться в сортировке методами, такими как Collections.sort() или Arrays.sort().

Comparator, в отличие от этого — это способ сравнения объектов. Он содержит метод compare(), который принимает два объекта в качестве аргументов.

Реализация интерфейса Comparator позволяет определить кастомные правила сравнения для объектов, независимо от их собственной реализации Comparable.

Comparator может быть передан в методы сортировки (например, Collections.sort()) в качестве дополнительного аргумента, чтобы управлять порядком сортировки.

Comparable предоставляет "естественный" порядок сортировки объектов, тогда как интерфейс Comparator позволяет определить "пользовательский" порядок сортировки, независимо от реализации Comparable.

Что такое стримы? Для чего они нужны? Когда их лучше использовать?

Интерфейс java.util.Stream представляет собой последовательность элементов, над которой можно производить различные операции.

Преимущества и причины использования стримов:

  1. Удобство и выразительность кода: Стримы позволяют писать более чистый и компактный код для обработки данных.

  2. Потоковая обработка данных: Стримы поддерживают операцию "потоковой" обработки данных, что означает, что данные обрабатываются постепенно по мере их доступности, а не все сразу.

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

  4. Разделение ответственности: Использование стримов способствует разделению логики обработки данных на набор отдельных операций. Каждая операция выполняет определенную задачу, и их комбинация образует цепочку операций для обработки данных.

Стримы рекомендуется использовать в следующих случаях:

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

  2. Когда требуется удобство и выразительность кода при обработке данных.

  3. Когда необходима возможность параллельной обработки данных для повышения производительности.

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

Какие есть виды стримов?

Кроме универсальных объектных существуют особые виды стримов для работы с примитивными типами данных `int`, `long` и `double`: `IntStream`, `LongStream` и `DoubleStream`. Эти примитивные стримы работают так же, как и обычные объектные, но со следующими отличиями:

  • используют специализированные лямбда-выражения, например, `IntFunction` или `IntPredicate` вместо `Function` и `Predicate`;

  • поддерживают дополнительные конечные операции `sum()`, `average()`, `mapToObj()`.

Способы создания Стрима?

  1. Из коллекции:

Stream fromCollection = Arrays.asList("x", "y", "z").stream();

  1. Из набора значений:

Stream fromValues = Stream.of("x", "y", "z");

  1. Из массива:

Stream fromArray = Arrays.stream(new String[]{"x", "y", "z"});

  1. Из файла (каждая строка в файле будет отдельным элементом в стриме):

Stream fromFile = Files.lines(Paths.get("input.txt"));

  1. Из строки:

IntStream fromString = "0123456789".chars();

  1. С помощью `Stream.builder()`:

Stream fromBuilder = Stream.builder().add("z").add("y").add("z").build();

  1. С помощью `Stream.iterate()` (бесконечный):

Stream fromIterate = Stream.iterate(1, n -> n + 1);

  1. С помощью `Stream.generate()` (бесконечный):

Stream fromGenerate = Stream.generate(() -> "0");

Что такое терминальная операция?

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

Некоторые примеры терминальных операций в Java Stream API включают:

  • collect() - представление результатов в виде коллекций и других структур данных;

  • findFirst() возвращает первый элемент;

  • findAny() возвращает любой подходящий элемент;

  • forEach() применяет функцию к каждому объекту стрима, порядок при параллельном выполнении не гарантируется;

  • forEachOrdered() применяет функцию к каждому объекту с сохранением порядка элементов;

  • reduce(): Сводит (редуцирует) элементы стрима к одному значению, используя указанную функцию. Например, можно найти сумму элементов или наименьший элемент.

  • anyMatch(), allMatch(), noneMatch()

  • count(): Возвращает количество элементов в стриме.

  • min(), max(): Возвращает минимальный или максимальный элемент из стрима, используя заданный компаратор.

  • toArray() возвращает массив значений;

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

Попытка выполнить дополнительные операции после терминальной операции приведет к выбросу исключения IllegalStateException.

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

Для чего нужен метод collect() в стримах?

Метод `collect()` является конечной операцией, которая используется для представление результата в виде коллекции или какой-либо другой структуры данных.

`collect()` принимает на вход `Collector<Тип_источника, Тип_аккумулятора, Тип_результата>`, который содержит четыре этапа: *supplier* - инициализация аккумулятора, *accumulator* - обработка каждого элемента, *combiner* - соединение двух аккумуляторов при параллельном выполнении, *[finisher]* - необязательный метод последней обработки аккумулятора. В Java 8 в классе `Collectors` реализовано несколько распространённых коллекторов:

  • `toList()`, `toCollection()`, `toSet()` - представляют стрим в виде списка, коллекции или множества;

  • `toConcurrentMap()`, `toMap()` - позволяют преобразовать стрим в `Mindmap`;

  • `averagingInt()`, `averagingDouble()`, `averagingLong()` - возвращают среднее значение;

  • `summingInt()`, `summingDouble()`, `summingLong()` - возвращает сумму;

  • `summarizingInt()`, `summarizingDouble()`, `summarizingLong()` - возвращают `SummaryStatistics` с разными агрегатными значениями;

  • `partitioningBy()` - разделяет коллекцию на две части по соответствию условию и возвращает их как `Mindmap`;

  • `groupingBy()` - разделяет коллекцию на несколько частей и возвращает `Mindmap>`;

  • `mapping()` - дополнительные преобразования значений для сложных `Collector`.

Что возвращают промежуточные операции над стримом?

Промежуточные операции (intermediate operations) в Java Stream API преобразуют или модифицируют элементы стрима и возвращают новый стрим, который можно дальше использовать для выполнения других операций.

Промежуточные операции позволяют цепочку операций, в результате которых создается "пайплайн" (pipeline) обработки стрима. Вот некоторые примеры промежуточных операций:

  • filter(): Фильтрует элементы стрима на основе заданного условия. Возвращает стрим, содержащий только элементы, которые удовлетворяют условию.

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

  • sorted(): Сортирует элементы стрима в заданном порядке. Возвращает стрим с отсортированными элементами.

  • distinct(): Удаляет дублирующиеся элементы из стрима. Возвращает стрим с уникальными элементами.

  • limit(): Ограничивает количество элементов в стриме до заданного числа. Возвращает стрим с ограниченным количеством элементов.

  • skip(): Пропускает указанное количество элементов в стриме. Возвращает стрим, который начинается с элемента, следующего после пропущенных.

  • peek() возвращает тот же стрим, применяя к каждому элементу функцию;

  • mapToInt(), mapToDouble(), mapToLong() - аналоги map() возвращающие стрим числовых примитивов;

  • flatMap(), flatMapToInt(), flatMapToDouble(), flatMapToLong() - похожи на map(), но могут создавать из одного элемента несколько.

Параллельные стримы

Фактически применение параллельных стримов сводится к тому, что данные в стримах будут разделены на части, каждая часть обрабатывается на отдельном ядре процессора, и в конце эти части соединяются, и над ними выполняются конечные операции.

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

Чтобы использовать параллельные стримы, достаточно вызвать метод parallel() на стриме перед выполнением операций.

В чем разница map и flatMap?

Операция map() принимает каждый элемент стрима и применяет к нему заданную функцию, возвращая стрим, содержащий преобразованные элементы. То есть, для каждого элемента входного стрима будет создан соответствующий элемент выходного стрима с преобразованным значением. Размер стрима после операции map() будет таким же, как размер входного стрима.

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

Что такое ленивая инициализация стрима?

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

Может ли стрим использоваться повторно?

Нет, это одноразовая сущность

В чём разница между forEach и peek?

Обе ForEach и Peek по сути делают одно и тоже, меняют свойства объектов в стриме, единственная разница между ними в том что ForEach терминальная и она заканчивает работу со стримом, в то время как Peek конвейерная и работа со стримом продолжается.

К каким переменным можно обращаться внутри лямбды?

1. переменные внутри метода (эффективно-финальные)

2. static переменные класса

3. переменные интерфейса (с которым лямбда работает)

Какие бывают стримы (3 группы)

1. конечные и бесконечные

2. последовательные и параллельные

3. объектные и примитивные