Функция представляет собой небольшую программу, выполняющую определённые действия, когда её (функцию) вызывают по имени. В языке программирования Java есть много встроенных и готовых к работе функций, однако никто не мешает создавать пользователю свои функции.
Исходя из вышеизложенного, пользовательской функцией можно назвать функцию, которую создал сам разработчик для решения конкретной задачи. Непосредственный процесс написания функций считается процедурным подходом в программировании.
Что обычно помещают в функции? Как правило, речь идёт о повторяющемся программном коде. Соответственно, чтобы программисту не писать те же самые строчки кода снова и снова, он просто выносит эти строчки в отдельный блок, а потом вызывает этот блок в основном Java-коде программы тогда, когда это нужно.
Идём дальше, в Java выделяют 2 вида функций:
1. Функция, которая что-либо возвращает.
2. Функция, которая не возвращает ничего.
Вдобавок к этому, функция в Джаве может быть с параметрами и без них. Тут следует напомнить важный момент: переменная, которая создана в функции, после завершения этой функции «умирает», то есть больше не существует.
Рассмотрим формулу создания функции в Java:
<спецификатор доступа> <указание статичности функции> <тип функции> <имя функции> <(параметры функции)> {тело функции}Создание функций в Java
Для начала создадим пользовательскую функцию, которая что-нибудь возвращает. Этот тип функций используется чаще всего, ведь очень часто нужно что-либо посчитать, выполнить преобразование и т. п., то есть речь идёт о том, чтобы использовать полученный результат вне этой функции. А так как всё, что создано в функции, после её завершения «погибает», нам надо, чтобы в основной код программы вернулся результат работы этой функции. Для это используется оператор return.
Итак, создадим пользовательскую функцию, которая вернёт нам значение переменной, а также присвоит это значение переменной в основном коде.
public class Main { public static int function1(){ //идентификатор доступа, функция является статичной, тип возвращаемого значения, имя функции без параметров int a = 5; //создаём переменную со значением return a; //возвращаем значение при вызове данной функции } public static void main(String[] args) { //блок основного кода нашей программы } }Таким образом, создаётся переменная, которая потом возвращается в нужном разработчику месте. Но вообще, в теле пользовательской функции мы можем прописать любой код, например, по созданию массивов, циклов и другой структуры.
Теперь давайте создадим функцию в Java, которая ничего возвращать не будет. Такой тип функции может пригодиться во время работы с глобальными переменными либо если надо что-либо напечатать и вывести на экран.
По большему счёту, особых отличий между написанием функций обоих видов нет. Главное — указать другой тип (void) и не применять return.
public class Main { public static void function2(){ System.out.println("Записывайтесь на курсы OTUS!"); //выводим сообщение на экран } public static void main(String[] args) { //основной блок нашей программы } }
Вызываем функции без параметров
Чтобы работать с функциями, получая от них какой-либо результат, надо вызвать функцию в нужном месте по имени.
Давайте воспользуемся написанными нами функциями и вызовем их в основном коде.
public class Main { public static int function1(){ int a = 5; return a; } public static void function2(){ System.out.println("Записывайтесь на курсы OTUS!"); } public static void main(String[] args) { //основной блок программы int b = function1(); //присваиваем переменной значение, которое возвратит первая функция System.out.println(b); //выводим на экран значение нашей переменной function2(); //вызываем по имени вторую функцию } }Вывод будет следующим:
5 Записывайтесь на курсы OTUS!Следует добавить, что функция, которая что-либо возвращает, обязательно должна вызываться так, как указано в примере, то есть возвращаемое значение должно быть чему-то присвоено.
Создаём функции с параметрами
Иногда надо произвести над значениями какие-нибудь действия. Для этого нужно передать нашей пользовательской функции эти самые значения. Когда значения передаются в нашу функцию, они становятся её аргументами функции.
Итак, давайте создадим функцию с параметрами, а потом вызовем её в основном коде с помощью аргументов. Возведём переменную в определённую степень, а потом вернём значение в переменную.
Необходимые параметры нужно указывать при создании функции (речь идёт о переменных в скобках после имени функции). При этом аргументы надо передать в обязательном порядке, иначе функция попросту не заработает, ведь у неё просто не будет значения, с которым надо взаимодействовать. Аргументы надо указывать при вызове функции (2 целочисленных значения).
public class Main { public static int function3(int var_1, int var_2){ //функция с параметрами int a = 1; //создаём переменную, в которой будет находиться расчётное значение for(int i = 0; i < var_2; i++){ //используем цикл для возведения в степень a = a * var_1; //перемножаем новое значение со старым, возводя тем самым в степень } return a; //возвращаем посчитанное значение } public static void main(String[] args) { //основной блок программы int result = function3(5,3); //вызываем функцию, передав 2 аргумента (возводим 5 в третью степень) System.out.println(result); //выводим значение переменной } }В консоли увидим следующее значение:
Таким образом, в функцию в Java мы можем помещать, что угодно. В нашем же случае, аргументы надо передать обязательно, иначе возникнет ошибка.
Вот и всё, надеемся, что теперь вы разобрались с темой по созданию пользовательских функций на языке Java. Если же интересуют более сложные задачи, добро пожаловать на наш курс для опытных Java-разработчиков!
Функция — это небольшая программа, которая выполняет определённые действия при вызове функции по имени.
В языке Java существуют свои встроенные и готовые к использованию функции, но никто не отменял пользовательских функций.
Пользовательская функция — это функция созданная разработчиком для конкретной задачи.
Написание функций относят к процедурному подходу в программировании.
Чаще всего в функции помещают повторяющийся программный код. Для того, чтобы не писать одни и те же строчки кода, их выносят в отдельный блок, а затем вызывают по имени в основном коде программы когда это необходимо.
Различают два основных вида функций:
- Функция, которая что-то возвращает;
- Функция, которая ничего не возвращает;
Также функции можно разделить на две категории:
- Функция без параметров;
- Функция с параметрами;
Запомните: переменная созданная в функции, умирает после завершения функции и более не существует!
Формула создания функции:
<спецификатор доступа> <указание статичности функции> <тип функции> <имя функции> <(параметры функции)> {тело функции}
Создание функции, которая возвращает
Данный тип функций является наиболее используемым, так как почти всегда требуется что-то посчитать, произвести некое преобразование и тому подобное, следовательно полученный результат необходимо использовать вне функции. Всё что было создано в функции в конечном счёте умирает в ней же, в связи с этим необходимо вернуть результат в основной код программы.
Сделать это помогает оператор return
Создадим функцию, которая вернёт значение переменной и присвоит переменной в основном коде это значение.
public class Main { public static int func(){ //идентификатор доступа, функция статична, тип возвращаемого значения и имя функции без параметров int n = 10; //создание переменной со значением return n; //возврат значения при вызове данной функции } public static void main(String[] args) { //блок основного кода программы } } |
Создание функции очень похоже на основной блок кода программы. Собственно, это всё.
Создаётся переменная, а потом она возвращается в нужном разработчику месте.
В теле функции можно прописать любой код, создать массивы, циклы и любая другая структура.
Создание функции, которая ничего не возвращает
Данный тип функции используется при работе с глобальными переменными и если нужно что-то напечатать, вывести на экран.
Нет, почти, никакого отличия между данным типом функции и предыдущим. Необходимо лишь указать другой тип функции и не использовать return.
public class Main { public static void func1(){ //идентификатор доступа, функция статична, тип возвращаемого значения и имя функции без параметров System.out.println(«Привет из функции!»); //вывод на экран сообщения } public static void main(String[] args) { //основной блок программы } } |
Вызов функции без параметров
Чтобы работать с функциями и получать от них какой-то результат, необходимо вызвать функцию по имени в нужном месте.
Используем предыдущие функции и вызовем их в основном блоке.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Main { public static int func(){ //идентификатор доступа, функция статична, тип возвращаемого значения и имя функции без параметров int n = 10; //создание переменной со значением return n; //возврат значения при вызове данной функции } public static void func1(){ //идентификатор доступа, функция статична, тип возвращаемого значения и имя функции без параметров System.out.println(«Привет из функции!»); //вывод на экран сообщения } public static void main(String[] args) { //основной блок программы int u = func(); //присвоение переменной значения возвращённого функцией System.out.println(u); //вывод на экран значения переменной func1(); //вызов функции по имени } } |
В результате получим на экран
Функция, которая что-то возвращает, обязательно должна вызываться таким образом, чтобы возвращаемое значение было чему-то присвоено.
Создание функции с параметрами
Бывает необходимость провести над значениями некие действия. Для этого необходимо передать функции эти самые значения. Когда значения передаются в функцию, то они становятся аргументами функции.
Создадим функцию с параметрами и вызовем её в основном блоке используя аргументы.
Возведём переменную в определённую степень и вернём значение в переменную.
Указываем параметры при создании функции (переменные в скобках после имени функции). Аргументы должны быть переданы обязательно, иначе функция не заработает, у неё элементарно не будет значения с которым она должна взаимодействовать.
Указываем аргументы при вызове функции (два целочисленных значения).
public class Main { public static int func2(int var_1, int var_2){ //функция с параметрами int n = 1; //создаём переменную, в которую будем помещать расчётное значение for(int i = 0; i < var_2; i++){ //цикл для возведения в степень n = n * var_1; //умножаем новое значение со старым и тем самым возводим в степень } return n; //возвращаем посчитанное значение } public static void main(String[] args) { //основной блок программы int result = func2(10,2); //вызываем функцию передав два аргумента (возводим десять во вторую степень) System.out.println(result); //вывод значения переменной } } |
В результате получаем
В функции вы можете помещать всё что угодно. В данном случае аргументы должны быть переданы обязательно, иначе произойдёт ошибка.
Чтобы ошибки не возникало, можно использовать значения по умолчанию при создании функции, но к сожалению Java обделена такой возможностью, значение по умолчанию в функции задать невозможно.
Таким нехитрым образом мы разобрались с темой «Создание функций на языке Java»!
Время на прочтение
5 мин
Количество просмотров 55K
Эта статья о:
- О применении функционального стиля программирования в языке Java.
- О некоторых базовых паттернах для работы с коллекциями данных из функционального программирования в примерах на Java.
- Немного о библиотеке Google Collections.
Если вы программируете на языках Java, C#, C++, PHP, или любом другом ОО языке, хотели бы познакомиться с функциональным программированием, но не имеет возможности/желания изучать Haskell/Scala/Lisp/Python, — эта статья специально для вас.
Тем, кто знаком с функциональным программированием, но никогда не применял его в Java, думаю, это будет тоже интересно.
Вводим конструкцию «функция» в языке Java
Что такое функциональное программирование? Если в двух словах, то функциональное программирование — это программирование, в котором функции являются объектами, и их можно присваивать переменным, передавать в качестве аргументов другим функциям, возвращать в качестве результата от функций и т. п. Преимущества, которые раскрывает такая возможность, будут понятны чуть позже. Пока нам надо разобраться, как в Java можно использовать саму конструкцию «функция».
Как известно, в Java нету функций, там есть только классы, методы и объекты классов. Зато в Java есть анонимные классы, то есть классы без имени, которые можно объявлять прямо в коде любого метода. Этим мы и воспользуемся. Для начала объявим такой интерфейс:
public final interface Function<F, T> {
T apply(F from);
}
Теперь в коде какого-нибудь метода мы можем объявить анонимную реализацию этого интерфейса:
public static void main() {
// Объявляем "функцию", присваиваем ее переменной intToString.
Function<Integer, String> intToString = new Function<Integer, String>() {
@Override public String apply(Integer from) {
return from.toString();
}
};
intToString.apply(9000); // Вызываем нашу функцию. Получаем строку "9000".
}
Такую реализацию мы и будем называть «анонимной функцией». С точки зрения функционального программирования с ней можно делать все то же самое, что и с функцией из функциональных языков: присваивать переменным, передавать в качестве аргумента другим функциям(и методам классов), получать в качестве результата от функций(и методов классов).
Теперь можно перейти к изложению некоторых базовых паттернов функционального программирования.
Работа с коллекциями в функциональном стиле
Допустим, у нас есть некая коллекция целых чисел. Мы хотим их вывести на экран в виде строки, и каждое число в строке будет разделено через запятую. Нефункциональное решение выглядело бы примерно так:
public String joinNumbers(Collection<? extends Integer> numbers) {
StringBuilder result = new StringBuilder();
boolean first = true;
for (Integer number : numbers) {
if (first)
first = false;
else
result.append(", ");
result.append(number);
}
return result;
}
Для реализации функционального решения нам потребуется сперва подготовить несколько функций и методов. Будем объявлять их в качестве статических полей класса:
public static final Function<Integer, String> INT_TO_STRING = ... // Уже реализовали выше
// Берет поэлементно значения из коллекции from, преобразует их с помощью функции transformer
// и возвращает список результатов преобразования в том же порядке.
public static <F, T> List<T> map(Collection<F> from, Function<? super F,? extends T> transformer) {
ArrayList<T> result = new ArrayList<T>();
for (F element : from)
result.add(transformer.apply(element));
return result;
}
// Берет коллекцию произвольных элементов и конкатенирует их в строку
public static <T> String join(Collection<T> from, String separator) {
StringBuilder result = new StringBuilder();
boolean first = true;
for (T element : from) {
if (first)
first = false;
else
result.append(separator);
result.append(element);
}
return result.toString();
}
Теперь наш метод joinNumbers будет выглядить следующим образом:
public String joinNumbers(Collection<? extends Integer> numbers) {
return join(map(numbers, INT_TO_STRING), ", ");
}
Метод реализован ровно в одну простую строку.
Хотелось бы отметить несколько важных моментов:
- Методы
map
иjoin
являются достаточно обобщенными, то есть их можно применять не только для решения данной задачи. Это значит, что их можно было бы выделить в некий утилитный класс, и использовать потом этот класс в разных частях проекта. - Вместо класса
Collection
в методеmap
можно было бы передаватьIterable
и возвращать новыйIterable
, извлекая из переданной коллекции данные по мере обхода данных в возвращаемой коллекции, то есть извлекать элементы лениво, поэтапно, а не все сразу. Такая реализация, позволит, например, создавать цепочки преобразования данных, выделяя каждый этап преобразования в отдельную простую функцию, при этом эффективность алгоритма будет оставаться порядка O(n):
map(map(numbers, MULTIPLY_X_2), INT_TO_STRING); // каждый элемент умножаем на два и приводим к строке.
- Создавая какой-нибудь класс, вы можете создавать для некоторых его методов статические поля, являющиеся функциями-обертками, делегирующими вызов apply на вызов соответствующего метода класса. Это позволит использовать «методы» объектов в функциональном стиле, например, в представленных выше конструкциях.
Работа с коллекциями с помощью Google Collections
Ребята из Google как раз создали удобную библиотеку с утилитными классами, позволяющую работать с коллекциями в Java в функциональном стиле. Вот некоторые из возможностей, которые она предоставляет:
interface Function<F, T>
. Интерфейс, аналогичный приведенному мной выше.Iterables.filter
. Берет коллекцию и функцию-предикат(функцию, возвращающую булево значение). В ответ возвращает коллекцию, содержающую все элементы исходной, на которые указанная функция вернула true. Удобно, например, если мы хотим отсеить из коллекции все четные числа:Iterables.filter(numbers, IS_ODD);
Iterables.transform
. Делает то же самое, что функция map в моем примере выше.Functions.compose
. Берет две функции. Возвращает новую функция — их композицию, то есть функцию, которая получает элемент, подает его во вторую функцию, результат подает в первую функцию, и полученный из первой функции результат возвращает пользователю. Композицию можно использовать, например, так:Iterables.transform(numbers, Functions.compose(INT_TO_STRING, MULTIPLY_X_2));
В Google Collections конечно есть еще много других полезных вещей как для функционального программирования, так и для работы с коллекциями в императивном стиле.
Ссылки
- Статья в Википедии о функциональном программировании.
- Google Guava, проект, частью которого является Google Collections.
- Видеопрезентация Google Collections с Joshua Bloch.
- Apache Commons Collections. Решает схожие с Google Collections задачи, но был написан под Java 4, то есть без параметрических типов.
О чем хотелось бы рассказать еще
Дорогие друзья, если вам понравилась моя статья, я с удовольствием напишу еще что-нибудь интересное о применении функционального программирования в Java и других императивных языках. Вот некоторые из вещей, о которых есть желание рассказать, но нету возможности изложить сразу в одной статье:
- Мутабельные и иммутабельные замыкания.
- Pattern-matcher.
- Монады.
- Распараллеливание с использованием функционального подхода.
- Комбинаторы парсеров.
Буду рад услышать ваши комментарии и предложения.
Функции выполняют роль небольших подпрограмм, что могут содержать повторяющийся код из проекта. В языке Джава можно использовать встроенные функции и создавать свои собственные. За урок мы научимся разрабатывать свои функции (методы) в языке Джава.
Функции можно назвать небольшими подпрограммами, куда можно вынести повторяющийся код и обращаться к нему, когда это будет нужно. Функции значительно облегчают построение программ, так как нам не надо копировать однотипный код множество раз, а можно просто воспользоваться одной общей функцией.
Многие путают функции и методы и не понимают отличий между ними. На самом деле отличий нет, так как что методы, что функции являются одним и тем же. Функции что записаны вне классов называют функциями, а функции что записаны внутри классов называются методами. Поскольку Java это объектно ориентированный язык, то лучше говорить методы, хотя это не имеет никакого значения.
Точно такая же ситуация обстоит с переменным. В классах переменные называются полями, а вне классов — переменными.
Для создания функций необходимо указать возвращаемый тип данных, указать название и параметры. В случае когда функция ничего не возвращает, то указывается тип данных void
.
Перед типом данных всегда прописывается модификатор доступа. Сейчас мы их детально рассматривать не будем, так как это тема ООП. В уроке мы будем постоянно устанавливать модификатор public, тем самым будем говорить что функция доступна всем внутри класса.
Зачем static?
Поскольку мы хотим обращаться к функциям напрямую без создания объекта, то мы прописываем ключевое слово static
. Оно говорит компилятору что функция принадлежит всему классу, а не конкретным объектам.
В будущем мы более детально ознакомимся с данным модифактором, но пока будем представлять его как удобное слово для обращения к функциям.
Создание функции
На основе всех данных наша функция будет выглядеть следующим образом:
public static void test() {
System.out.print("Вывод чего-либо в консоль");
}
Функция выше не принимает никаких параметров и ничего не возвращает. Она просто пишет слово в консоль. Давайте разнообразим функцию и добавим параметр:
public static void test(String word) {
System.out.print(word);
}
Теперь функция принимает параметр, который будет отображен в консоли.
Если функция должна что-либо вернуть, то прописываем тип данных который будет возвращен. Для возвращения данных используем ключевое слово return
:
public static double test(double some_number) {
some_number *= 2;
return some_number;
}
Часто какой-то код в программе нужно повторять много раз в разных местах. Тогда этот код можно поместить в функцию. Функция — это блок кода, у которого есть имя, параметры и возвращаемое значение. Функции можно вызывать по имени в любых местах, в main или в других функциях. Это гораздо удобнее, чем без конца копировать кучу кода, когда вам нужно выполнить его несколько раз.
Функцией называют блок кода, у которого есть имя, параметры и возвращаемое значение. По имени можно вызывать этот блок в любом месте программы.
Параметры — это входные данные, на которые опирается наша функция. Например, функция возводит число в квадрат — ей нужно знать, чему равно в данный момент это число. При одном вызове мы можем указать число и получить его в квадрате, а при другом вызове мы укажем уже другое число, и получим другой квадрат. Так что пока мы пишем код функции, то просто говорим, что нам нужно какое-то дробное число, а какое именно — будет указано в скобках при вызове функции, каждый раз новое.
Многие функции создаются, чтобы посчитать какой-то результат. Этот результат нужно вернуть, то есть передать из кода функции в место, где её вызвали. Например, функция, которая возводит число в квадрат, будет возвращать результат расчета, то есть квадрат числа, которое её дали на вход.
Посмотрим код функции для возведения числа в квадрат. Сначала пишется заголовок функции, где указывается слева направо тип возвращаемого значения, имя функции и в скобках её параметры. Функцию возведения в квадрат часто называют sqr от английского слова square — «квадрат». Поскольку её нужно число, возможно дробное, и возвращает она тоже число, то тип параметра будет double и тип возвращаемого значения тоже будет double .
// Наш заголовок читается так:
// статическая, возвращает double, называется sqr, принимает параметром один double
static double sqr ( double number) {
// Фигурная скобка открывает тело функции - её блок кода.
// Чтобы посчитать квадрат числа, нужно его умножить само на себя.
// Результат перемножения нужно вернуть из функции словом return вот так:
return number*number;
}
// фигурная скобка закрывает блок функции.
Слово static в заголовке функции пока что будем писать всегда, а разберём подробнее, когда будем делать свои классы.
Фигурные скобки {…} обрамляют блок функции. Если закрывающую не поставить, то компилятор решит, что у функции нет конца и так и скажет «missing }«, то есть «отсутствует }«. А если наоборот закрывающих слишком много, то скажет «unbalanced brackets», то есть «скобки не сбалансированы» как про круглые (), так и фигурные {}
Теперь посмотрим на вызов нашей функции, например, в main. Чтобы вызвать функцию нужно написать её имя и в скобках указать значения параметров. Результат работы функции имеет смысл сохранить в какой-нибудь переменной, чтобы потом ей пользоваться, к примеру, чтобы распечатать:
public static void main (String[] args) {
// Объявим x равный 10
double x = 10;
// Вызвать sqr с параметром x:
double result = sqr (x); // Возвращаемое значение sqr подставится прямо сюда
// получится double result = 100
System.out.println ("10 в квадрате будет " + result);
}
При вызове функции программа выполняет весь её код, вместо параметров подставляя значения, указанные в скобках. Сейчас мы сделали вызов sqr (x) . Значит в параметр подставится число x, а оно сейчас равно 10. В то самое место кода, где мы написали вызов sqr (x)
Тип переменной для хранения результата функции должен соответствовать типу самого результата, поэтому в нашем случае мы сделали double result, а не, например, int, ведь sqr возвращает double.
Теперь на основе функции sqr сделаем в main простой калькулятор — он будет спрашивать у пользователя числа и возводить их в квадрат. Заодно посмотрим, как ещё можно использовать функции.
public static void main (String[] args) {
System.out.println ("Это программа для расчета квадратов чисел");
System.out.println ("Введите число:");
// Считаем в x дробное число с клавиатуры
Scanner scan = new Scanner (System.in); // Создадим сканер клавиш клавиатуры
// Кстати тут мы тоже вызываем функцию - nextDouble, только она уже есть в Java
// Мы указываем scan, чтобы подчеркнуть, что вызов именно
// для нашего конкретного сканера scan, созданного строчкой выше
double x = scan.nextDouble ();
// Вызвать sqr с параметром x:
double result = sqr (x); // Возвращаемое значение sqr подставится прямо сюда
System.out.println (x + " в квадрате будет " + result);
System.out.println ("Введите новое число:");
// Мы используем уже введенную раньше переменную x,
// поэтому указываем просто x, а не double x:
x = scan.nextDouble ();
// Снова вызвать sqr с параметром x:
result = sqr (x); // Возвращаемое значение sqr подставится прямо сюда
System.out.println (x + " в квадрате будет " + result);
// Теперь посмотрим другой вариант для той же задачи:
System.out.println ("Введите новое число:");
x = scan.nextDouble ();
// sqr(x) в программе будет уже просто число, и его можно подставлять куда угодно:
System.out.println (x + " в квадрате будет " + sqr (x) );
// И самый короткий вариант:
System.out.println ("Введите новое число:");
System.out.println ("В квадрате это будет " + sqr ( scan.nextDouble () ) );
}
В последней строчке мы вызвали сразу nextDouble, sqr и println:
System.out.println («В квадрате это будет » + sqr ( scan.nextDouble () ) );
Компилятор идёт слева направо и видит, что здесь стоит распечатка println. Он пробует её выполнить, но обнаруживает, что строчку для распечатки он получит, только выполнив функцию sqr, а значит, он выполняет сначала её. Но тут снова, чтобы получить число для возведения в квадрат, нужно выполнить scan.nextDouble .
Поэтому прежде всего заработает scan.nextDouble и число будет введено с клавиатуры. Оно будет передано на вход функции sqr и будет возведено в квадрат. Тогда уже оно попадёт в строчку и будет распечатано на экране в функции println.
Получается, что возвращаемое значение одной функции можно сразу дать в качестве входного параметра для другой функции. Это нормально, ведь возвращаем мы число, и на вход нам тоже нужно число. Но при этом, поскольку мы сейчас не заводили никаких переменных, мы уже не сможем использовать результаты работы этой функции где бы то ни было ещё в коде программы. Итак, каскад вызовов делают для краткости кода, но если вы хотите использовать результат где-то ещё в программе, то нужно писать несколько строк и сохранять значения в переменных.
Сейчас в нашем небольшом калькуляторе мы 4 раза подряд совершили один и тот же набор действий — считать число, возвести в квадрат, вывести на экран. Имеет смысл для этих действий завести свою функцию, и таким образом сократить объём кода:
// Для полноты картины приводим нашу функцию sqr
static double sqr (double num) {
return num*num;
}
// Назовём функцию ask_and_calc, то есть спросить и посчитать квадрат.
// Она ничего не возвращает, поэтому вместо типа возвращаемого значения
// пишется void, по-английски пустота.
// Данные функция сама возьмёт с клавиатуры и параметры ей не нужны.
// Поэтому в круглых скобках ничего нет - это будет функция без параметров.
static void ask_and_calc () {
System.out.println ("Введите число:");
Scanner scan = new Scanner (System.in);
double x = scan.nextDouble ();
double result = sqr (x);
System.out.println (x + " в квадрате будет " + result);
}
// Кстати main тоже ничего не возвращает, поэтому он void .
// А параметром идёт String[] args, что означает "набор String".
// Эти строки можно указывать при запуске приложения из командной строки,
// но мы их пока не будем использовать.
public static void main (String[] args) {
System.out.println ("Это программа для расчета квадратов чисел");
// Больше не копируем один и тот же код 4 раза, а просто вызываем функцию
ask_and_calc(); // название читается по-английски, и сразу понятен смысл
ask_and_calc();
ask_and_calc();
ask_and_calc();
}
Итак, функция ask_and_calc ничего не возвращает, а значит вместо типа возвращаемого значения пишется void — «пустота». Параметров у этой функции тоже нет, так что пишутся пустые скобки () .
Как сократился main! Он раньше занимал целый экран, а теперь всего несколько строчек. И как понятен он теперь — практически можно читать на английском языке! В этом и состоит удобство использования функций — код разбит на блоки с понятными названиями и их можно вызывать много раз подряд.
О термине «процедура». Строго говоря, функцией называется блок кода, который что-то возвращает. Когда используется слово void, и ничего не возвращается, то говорят, что это процедура. Но разница так тонка, что в обиходе слова «функция» и «процедура» используются практически как синонимы.
О термине «метод». И функции, и процедуры ещё называют методами. Слово метод употребляют, когда хотят подчеркнуть, что функция или процедура принадлежат классу. Классы мы будем изучать позже и тогда вернёмся к вопросу терминологии. В простом понимании функция, процедура и метод — это всё синонимы.
О термине «аргумент». Также параметры ещё называют аргументами. Так что можно сказать «параметр функции», а можно «аргумент функции» и это будет одно и то же.
Наш main можно было бы сократить ещё больше, если бы мы вызывали ask_and_calc в цикле вот так:
public static void main (String[] args) {
System.out.println ("Это программа для расчета квадратов чисел");
// спросить число и возвести его в квадрат 4 раза
for (int i = 0; i < 4; i ++)
ask_and_calc();
}
Когда мы делаем функцию и затем вызываем её в коде много раз или пишем цикл, который вызывает один и тот же код много раз, мы экономим объём программы, и при этом делаем её понятной для чтения человеку. Это называется принцип повторного использования кода. Один из краеугольных камней современного программирования.
Заметьте, что в нашем примере мы запускаем функцию sqr внутри функции ask_and_calc, а её внутри main. Так что как видите, запуск одной функции внутри другой — это нормально и удобно.
При каждом запуске функции для всех её переменных заново выделяется память, а когда функция заканчивается — эта память освобождается под нужды других программ. Все числа нужно хранить где-то в памяти, а нужны они только на время выполнения функции.
Более точно можно сказать, что переменная объявленная внутри блока фигурных скобок в функции, в операторе if или в цикле «живёт» только до конца этого блока, то есть пока не будет достигнута закрывающая фигурная скобка. Блок из фигурных скобок задаёт «область жизни», а также «область видимости» переменной. Область жизни, потому что вне блока память, выделенная под переменную, уже освобождена. Область видимости, потому что к этой переменной больше не имеет смысла обращаться, и для компилятора эта переменная больше не видна.
Это правило даёт нам возможность в разных функциях называть переменные одинаковыми именами. Это бывает удобно, например, любой ответ пользователя на вопрос можно поместить в переменную answer, по-английски «ответ». В одной функции answer будет означать ответ на один вопрос, а в другой функции уже совсем на другой.
То же самое касается параметров функций. В разных функциях могут быть параметры с одинаковым названием, но это их никак не связывает, каждый выполняет свою задачу для своей функции.
Вы дочитали тему до конца — это успех Пришло время решать задачи. Ответьте на несколько вопросов, чтобы лучше усвоился новый материал:
Он состоит в том, что повторяющиеся куски кода можно использовать повторно. Так устроен цикл — один и тот же код в нём прокручивается много раз. Так устроена функция — мы запускаем её много раз, а пишем её код всего один раз. В будущем мы пройдём классы, внутри которых можно создать группу функций. Классы также помогают использовать код повторно ещё более эффективным образом.
На уровне данного раздела — никак. Можно возвращать только по одной переменной. В дальнейшем мы пройдём классы, внутри которых можно заводить группу переменных, и тогда сможем возвращать объект класса, а в нём все необходимые переменные.
Ещё в следующем разделе мы пройдём массивы чисел, и можно будет возвращая массив, передавать наборы чисел одного и того же типа.
Задача 1. Сколько раз в этом коде будет считано число с клавиатуры? В этой теме мы просили пользователя ввести число и выводили на экран квадрат этого числа. Будет ли данный краткий код делать то же самое?
package javaapplication1;
import java.util.Scanner;
public class JavaApplication1 {
static double sqr (double num) {
return num*num;
}
public static void main(String[] args) {
Scanner scan = new Scanner (System.in);
System.out.println ("Введите число");
System.out.println (scan.nextDouble () + "в квадрате будет " + sqr ( scan.nextDouble () ) );
}
}
Будет введено два разных числа — одно для первого запуска scan.nextDouble () и одно для второго запуска. Дело в том, что в строке
System.out.println (scan.nextDouble () + «в квадрате будет » + sqr(scan.nextDouble () ) ); функция nextDouble вызвана дважды. Поэтому программа сработает совсем иначе, чем ожидается. Мы хотели считать число и вывести на экран квадрат этого числа. А здесь получится, мы сначала считали одно число — оно будет выведено на экран вместо первого запуска nextDouble. Затем считали второе число, и уже его квадрат будет выведен на экран.
То есть считываем лишнее число, а первое вообще непонятно зачем нужно. Имейте это в виду, имея дело в такими функциями как nextDouble. Чтобы считать одно число и всё время работать с ним, его имеет смысл сохранить в переменную:
double num = scan.nextDouble ();
System.out.println (num + «в квадрате будет » + sqr(num ) );
Теперь nextDouble вызвана всего один раз, и поэтому в распечатке в начале и в конце будет использоваться одно и то же число, как мы и хотели.
Решайте больше задач по этому разделу здесь.
В следующей теме на основе функций сделаю программу «Угадай пропущенную букву»
% Идиомы функционального программирования в Java 8
В этой статье даётся краткий обзор нескольких основных понятий функционального программирования, а также их использование в Java 8.
Обозначения
Прежде всего, введём условное обозначение, принятое в разговорах о функциях. Запись
$f: T rightarrow R$
будет означать «функция f, принимающая аргумент типа T и возвращающая результат типа R». В Java такая функция могла бы быть реализована функциональным интерфейсом Function<T, R>
. Функциональный интерфейс, соответствующий этой функции, мог бы иметь, например, такой абстрактный метод:
Обозначение
$f: T times U rightarrow R$
означает функцию от двух аргументов типа T и U, возвращающую результат типа R.
Чистые функции
Основным строительным блоком функционального программирования являются чистые функции (pure functions). Функция является чистой, если выполняются два условия:
- Функция является детерминированной, то есть всегда возвращает один и тот же результат для одних и тех же аргументов. Это означает, что функция не должна зависеть ни от какого изменяемого внешнего состояния, в том числе от изменяемых объектов и внешних по отношению к программе ресурсов (таких, как потоки ввода-вывода).
- Функция не имеет побочных эффектов (side effects), то есть не изменяет состояние никаких объектов и внешних ресурсов.
Вот примеры чистых функций:
- Обычные арифметические операции, например, лямбда-выражение
(int x) -> x + 1
. Несмотря на то, что возвращаемый результат не равенx
, самx
при этом не меняется. - Большинство методов-геттеров, например,
List.get
иList.size
. - Методы неизменяемых объектов, чьей единственной операцией является создание нового объекта на основе существующего объекта, например,
String.toCharArray
,BigDecimal.add
иLocalDateTime.atZone
.
Вот примеры функций, которые не являются чистыми:
- Методы вывода, в том числе
println
. Они детерминированы, но имеют побочные эффекты. - Методы ввода, в том числе
BufferedReader.readLine
. Они не имеют побочных эффектов, но не детерминированы, потому что их результат зависит от данных в потоке ввода. - Методы-мутаторы для изменяемого объекта, например,
List.set
иList.add
.
Подход, основанный на чистых функциях, естественным образом приводит к архитектуре, в которой большинство объектов являются неизменяемыми или де факто неизменяемыми, а вычисления над ними производятся путём составления конвейеров из чистых функций, которые возвращают новые объекты, созданные на основе существующих.
Все промежуточные операции Stream API, в том числе map
и filter
, являются чистыми функциями при правильном использовании. Оконечные операции, например, forEach
, не могут быть чистыми уже потому, что после вызова оконечной операции поток исчерпывается и более не может быть переиспользован. Кроме того, операции Stream API требуют, чтобы их аргументы по крайней мере не меняли элементы самого потока. Желательно, чтобы передаваемые потоку пользовательские функции были чистыми, потому что в этом случае операции над потоком будут эффективно параллелизуемыми и потокобезопасными (в смысле других потоков, тех, которые thread). Например, следующий код не идиоматичен:
IntStream ints = ...; AtomicInteger sumOfSquares = new AtomicInteger(0); ints.forEach(x -> sumOfSquares.addAndGet(x * x));
Это будет работать, но идиоматичный код с чистыми функциями проще для понимания и, скорее всего, будет эффективнее и для последовательного, и для параллельного потока:
IntStream ints = ...; int sumOfSquares = ints.map(x -> x * x).sum();
Ленивые вычисления
С чистыми функциями тесно связана идея ленивых вычислений (lazy evaluation). Если у нас есть конвейер из чистых функций, то мы можем оптимизировать процесс вычислений, выполняя все вычисления только по требованию, когда клиентский код запросит их результат. Мы видим это в Stream API, где все промежуточные операции только определяют конвейер вычислений (и возвращают новый поток, являющийся обёрткой над нижележащим потоком), а вызов оконечной операции является сигналом, приводящим конвейер в движение.
Ленивые вычисления просто необходимы для корректной работы бесконечных потоков, потому что в противном случае их вычисление никогда не завершилось бы.
Например, следующий код находит первую степень двойки, большую 1000 (то есть 1024). Обратите внимание, что метод iterate
производит бесконечный поток, но код тем не менее выполняется за конечное время именно потому, что метод findFirst
обрывает вычисление элементов потока, как только будет вычислен первый.
IntStream.iterate(1, x -> x * 2) .filter(x -> x > 1000) .findFirst();
Ещё один пример — добавленные в класс java.util.logging.Logger
в Java 8 методы, принимающие Supplier<String>
вместо String
. Поскольку построение строк логирования зачастую является относительно дорогой операцией, часто бывает желательно создавать эти строки только тогда, когда соответствующий уровень логирования включён. Вот так бы мы написали в Java 7 и ниже:
if (log.isLoggable(Level.DEBUG)) { log.debug("Server configuration: " + buildConfigParamsString()); }
В Java 8 можно просто обернуть этот код в лямбда-выражение, и тогда логгер вычислит строку, записываемую в лог, только при необходимости:
log.debug(() -> "Server configuration: " + buildConfigParamsString());
Функции высшего порядка
Функции высшего порядка — это функции, оперирующие функциями. Другими словами, они принимают функции, или возвращают функции, или и то и другое сразу.
Распространённые функции высшего порядка в функциональном программировании — это композиция, частичное применение функции и каррирование.
Композиция функций
Композиция — это применение одной функции к результату другой функции.
Композицией функций $f: T rightarrow U$ и $g: U rightarrow R$ по определению является функция $f circ g: T rightarrow R$, такая, что
$(f circ g)(x) = g(f(x)).$
То есть сначала применяется f
, а потом g
.
В Java, если функции f
и g
являются объектами типа Function
, то мы можем получить результирующую функцию лямбда-выражением x -> g(f(x))
или вызовом метода f.andThen(g)
. А вот метод f.compose(g)
возвращает $g circ f$, то есть применяет к аргументу сначала g
, а потом f
.
Частичное применение функции
Пусть у нас есть функция $f(x_1, …, x_N)$ от $N$ аргументов. Тогда, зафиксировав значения M из этих аргументов, где $M < N$, мы получим новую функцию $g(x_{M+1}, …, x_N)$ от оставшихся $M — N$ аргументов.
Операция частичного применения, таким образом, является функцией высшего порядка, которая принимает функцию и фиксируемые аргументы и возвращает новую функцию от меньшего числа аргументов:
$textrm{partial}(f(x_1, …, x_N), C_1, …, C_M) = g,$
где $g(x_{M+1}, …, x_N) = f(C_1, …, C_M, x_{M + 1}, …, x_N).$
Пример — прибавление к числу фиксированного аргумента:
// Частичное применение операции + IntUnaryOperator plusOne = x -> x + 1; IntStream.of(1, 2, 3).map(plusOne).collect(Collectors.toList()); // 2, 3, 4
Вот методы, превращающие любую функцию с двумя аргументами в функцию от первого (или второго) аргумента с фиксированным вторым (или, соответственно, первым):
<T, U, R> Function<T, R> bindSecond(BiFunction<T, U, R> func, U second) { return first -> func.apply(first, second); } <T, U, R> Function<U, R> bindFirst(BiFunction<T, U, R> func, T first) { return second -> func.apply(first, second); }
Каррирование
Каррирование (currying) — пожалуй, самая концептуально сложная для понимания функция высшего порядка из представленных. Она тоже принимает функцию от нескольких аргументов и возвращает новую функцию от одного аргумента. Возвращённая функция сама является функцией высшего порядка: она принимает первый аргумент и частично применяет его, возвращая функцию от всех остальных аргументов. Функцию, возвращённую операцией каррирования, можно таким образом рассматривать как фабрику частично применённых функций.
$textrm{curry}(f(x_1, …, x_N)) = g,$
где $g(x_{1}) = h,$
$h(x_2, …, x_N) = f(x_1, …, x_N).$
Зачем нужна такая сложная операция? Последовательно применяя каррирование, мы можем любую функцию от N аргументов переделать в последовательность вызовов N функций от одного аргумента. Это важно в контекстах, где функции могут принимать только один аргумент. В языке Haskell, например, все функции имеют каррированную форму, то есть с точки зрения синтаксиса языка вызов функции, например, с тремя аргументами состоит из вызова функции с первым аргументом, результат этого вызова (тоже функция) применяется ко второму аргументу, в результате чего получается функция, которая применяется, наконец, к третьему аргументу.
В Java каррирование менее важно, и мне неизвестны примеры его применения на практике. Тем не менее вот пример его реализации для произвольной BiFunction
:
<T, U, R> Function<T, Function<U, R>> curry(BiFunction<T, U, R> func) { return t -> u -> func(t, u); }
Кстати, операция каррирования и язык Haskell названы в честь одного и того же человека: математика Хаскелла Карри (Haskell Curry).
Функции высшего порядка на контейнерах
Теперь перейдём к рассмотрению типов, которые могут содержать в себе некоторые значения и при этом поддерживают операции из функционального программирования. Все они концептуально являются обобщёнными типами (generic), поскольку их тип зависит от типа элементов. Не всегда это означает, что они являются обобщёнными именно с точки зрения синтаксиса языка Java: из-за ограничений, не позволяющих сделать примитивные типы параметрами обобщённых, примитивные типы из соображений эффективности имеют свои собственные реализации Stream
и Optional
. Таковы IntStream
, LongStream
и DoubleStream
, а также OptionalInt
, OptionalLong
и OptionalDouble
. Концептуально, однако, эти типы можно рассматривать наряду с обобщёнными типами Stream<T>
и Optional<T>
.
Операция reduce
Операция свёртки (reduce) имеет смысл только для контейнеров, которые могут содержать более одного элемента. Поэтому из новых типов Java 8 её предоставляет только семейство Stream.
Пусть у нас имеется обобщённый тип G<T>
и функция $accum: T times T rightarrow T$, называемая аккумулятором (accumulator, накопитель). Тогда функция высшего порядка
$textrm{reduce}: textit{G} times T times (accum: T times T rightarrow T) rightarrow T$
берёт контейнер, начальное значение типа T и функцию, которую она последовательно применяет к каждому элементу, получая на выходе некоторое значение, основанное на всех элементах потока.
Например, сумма и произведение и являются частными случаями операции reduce:
// Сумма (но лучше использовать метод sum) IntStream ints1 = ...; int sum = ints1.reduce(0, (x, y) -> x + y); // Произведение IntStream ints2 = ...; int product = ints2.reduce(1, (x, y) -> x * y);
Есть ещё два варианта reduce. Один из них позволяет опустить начальное значение, и тогда в качестве такового выступит первый элемент потока. Эта версия возвращает Optional
: в случае, если поток пуст, возвращаемый Optional
тоже будет пустым, в противном случае он будет содержать результат операции reduce. Наконец, последний вариант позволяет вернуть значение типа, отличного от типа элементов потока, но ему нужно передать ещё одну функцию, которая будет отвечать за слияние двух значений этого нового типа:
// Подсчёт числа элементов потока // (но лучше используйте метод count!) Stream<String> strings = ...; int elementCount = strings.reduce(0, // аккумулятор (count, str) -> count + 1, // функция комбинации (count1, count2) -> count1 + count2);
Функция комбинации используется только в параллельных потоках. Она нужна затем, чтобы слить результаты выполнения reduce для разных частей потока, к которым операция reduce будет применяться параллельно и независимо друг от друга.
Функторы и map
Обобщённый тип G<T>
называется функтором (functor), если для него определена функция высшего порядка map
, которая принимает функцию $f: T rightarrow R$ и возвращает новый объект G<R>
, состоящий из результатов применения переданной функции к каждому элементу исходного объекта.
$textrm{map}: textit{G} times (f: T rightarrow R) rightarrow textit{G}$
В Java функторами являются типы Stream
, Optional
и CompletableFuture
. При этом Stream
применяет функцию, переданную в map
, к каждому элементу, а Optional
применяет её к единственному аргументу, если он есть (и возвращает пустой Optional
, если и исходный был пустым). Важно, что, в соответствии с принципами использования чистых функций, map не изменяет исходный объект, а возвращает новый.
Для типа CompletableFuture
эквивалентом map
является метод thenApply
, который применяет указанную функцию к значению, как только это значение будет получено.
Stream<LocalDate> dates = ...; Stream<String> dateStrings = dates.map(Object::toString); Optional<LocalDate> maybeADate = ...; Optional<String> maybeAString = maybeADate.map(Object::toString); CompletableFuture<LocalDate> eventuallyADate = ...; CompletableFuture<String> eventuallyAString = eventuallyADate.thenApply(Object::toString);
Монады и flatMap
Обобщённый тип G<T>
называется монадой (monad), если для него определена функция высшего порядка flatMap
, которая принимает функцию $f: T rightarrow textit{G}$ и возвращает новый объект G<R>
, состоящий из объединения исходной монады и монад, возвращённых применением переданной функции к каждому элементу исходного объекта.
$textrm{flatMap}: textit{G} times (f: T rightarrow textit{G}) rightarrow textit{G}$
Обратите внимание, что функция, которую мы передаём во flatMap
, возвращает новую монаду, а не обычное значение! В этом отличие flatMap
от map
. При этом та монада, которую возвращает сам метод flatMap
, каким-то образом комбинирует исходную монаду со всеми теми, которые вернули вызовы переданной функции.
В традиционной терминологии функционального программирования вместо flatMap
используется термин «связывание» (bind). Типы Stream
, Optional
и CompletableFuture
являются не только функторами, но и монадами, а смысл связывания зависит от конкретной монады:
Stream
склеивает подряд все потоки, в которые превращаются его элементы.Optional
применяет переданную функцию к аргументу, если он не пуст, и возвращает полученныйOptional
(который может быть пустым или непустым). Если жеOptional
пуст, то вызывать переданную функцию он не будет и просто вернёт пустойOptional
.- Для
CompletableFuture
эквивалентомflatMap
является методthenCompose
. Когда вCompletableFuture
появляется значение, запускается обработчик, переданный вthenCompose
, который возвращает новыйCompletableFuture
. При этом сам методthenCompose
возвращаетCompletableFuture
, оборачивающий оба асинхронных вычисления последовательно, и методыthenXXX
у этой обёртки вызовут свои обработчики тогда, когда завершится сначала первое вычисление, а потом второе.
Stream<String> strings = Stream.of("Hello", "World"); Stream<Character> chars = strings.flatMap(String::chars); // 'H', 'e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd' // CompletableFuture // Первый асинхронный запрос к серверу: CompletableFuture<User> server.login("username", "password") // Второй асинхронный запрос к серверу: CompletableFuture<List<Post>> .thenCompose(user -> server.getLastPosts(user, 10)) .thenAccept(posts -> { for (Post post: Posts) { showPost(post); } });
Кроме flatMap
, у монады должен быть ещё метод, позволяющий сразу завернуть («поднять») в монаду обычное значение. Такой метод в обычной терминологии называется unit. В Java такие методы действительно есть: это статические методы Stream.of
, Optional.of
и CompletableFuture.completedFuture
.
Ценность монад состоит в том, что они предоставляют абстракцию цепочки последовательных вычислений, при этом инкапсулируя «состояние мира» на каждом шаге вычисления. Это особенно ярко видно на примере CompletableFuture
, где аргумент каждого метода thenXXX
выполняется после завершения соответствующего этапа асинхронных вычислений. При этом монада сама знает, как «развернуть» полученный аргумент и передать его обработчику, который работает именно с обычным значением, а не с монадой. В Haskell, в котором в принципе нет функций с побочными эффектами, монада под названием IO
используется для описания обычных императивных последовательных вычислений, которые в императивном языке наподобие Java были бы разделены точками с запятой. Чтобы понять, о чём идёт речь, представим себе, как мог бы выглядеть подобный синтаксис в Java:
public class IO<T> { <R> IO<R> thenCompose(Function<T, IO<R>> func); } IO<String> readLine(); IO<Void> writeLine(String str);
Тогда обычная императивная программа:
public static void main(String[] args) { System.out.println("Как вас зовут?"); String name = new Scanner(System.in).nextLine(); System.out.println("Привет, " + name); }
перепишется с использованием монады IO
вот так:
public static IO<?> main(String[] args) { return writeLine("Как вас зовут?") .thenCompose(x -> readLine()) .thenCompose(name -> writeLine("Привет," + name)); }
Иными словами, в Haskell функция main
(да, она там есть) сама, строго говоря, не запускает никаких вычислений. С помощью монады IO
она возвращает описание процесса вычислений, который Haskell должен применить к внешней среде (включая возможности ввода-вывода), в которой выполняется программа.
Ковариантность и контравариантность
Пусть тип Sub
— это подтип типа Super
. Тогда:
- Обобщённый тип
G<T>
называется ковариантным по типуT
, если логически каждыйG<Sub>
является подтипом (частным случаем)G<Super>
. - Обобщённый тип
G<T>
называется контравариантным по типуT
, если логически каждыйG<Super>
является подтипом (частным случаем)G<Sub>
.
Ключевое слово здесь — логически, а не на уровне синтаксиса Java. Например, Stream
логически ковариантен, то есть любой Stream<String>
можно было бы использовать там, где нужен Stream<Object>
. Но в Java обобщённые типы инвариантны, то есть с точки зрения языка Java для любых типов T1
и T2
любой обобщённый тип G<T1>
никогда не является ни подтипом, ни супертипом G<T2>
.
Как мы знаем из Effective Java, в параметрах типа можно обойти это ограничение, если использовать <? extends T>
для ковариантных типов и <? super T>
для контравариантных.
Обязательная картинка в тему:
Отсюда видно, что производители значений (producers) всегда ковариантны, а потребители значений (consumers) всегда контравариантны. Отсюда проистекает правило из Effective Java, описывающее, когда мы должны использовать extends
и super
в объявлениях методов:
**PECS значит производитель — `extends`,
потребитель — `super`
(producer — `extends`, consumer — `super`).**
Разберёмся, как этот принцип применяется в функциональном программировании на Java. Замечательное свойство чистых функций состоит в том, что из-за отсутствия побочных эффектов (чистые функции не имеют права изменять ни свои аргументы, ни какие-либо внешние объекты) для них свойства производителя и потребителя всегда чётко определены.
**Чистые функции всегда являются потребителями своих аргументов и производителями своих возвращаемых значений.**
Из этого прямо следует, что:
**Объявление метода, использующее чистые функции, должно объявлять их как контравариантные (`super`) по типам принимаемых аргументов и как ковариантные (`extends`) по типу возвращаемого значения.**
Теперь вы и сами можете понять, почему методы forEach
, map
и sort
интерфейса Stream<T>
объявлены именно так:
void forEach(Consumer<? super T>) <R> Stream<R> map(Function<? super T, ? extends R>) Stream<T> sorted(Comparator<? super T>)
Functional Programming — Overview
In functional programming paradigm, an application is written mostly using pure functions. Here pure function is a function having no side effects. An example of side effect is modification of instance level variable while returning a value from the function.
Following are the key aspects of functional programming.
-
Functions − A function is a block of statements that performs a specific task. Functions accept data, process it, and return a result. Functions are written primarily to support the concept of re usability. Once a function is written, it can be called easily, without having to write the same code again and again.
Functional Programming revolves around first class functions, pure functions and high order functions.
-
A First Class Function is the one that uses first class entities like String, numbers which can be passed as arguments, can be returned or assigned to a variable.
-
A High Order Function is the one which can either take a function as an argument and/or can return a function.
-
A Pure Function is the one which has no side effect while its execution.
-
-
Functional Composition − In imperative programming, functions are used to organize an executable code and emphasis is on organization of code. But in functional programming, emphasis is on how functions are organized and combined. Often data and functions are passed together as arguments and returned. This makes programming more capable and expressive.
-
Fluent Interfaces − Fluent interfaces helps in composing expressions which are easy to write and understand. These interfaces helps in chaining the method call when each method return type is again reused. For example −
LocalDate futureDate = LocalDate.now().plusYears(2).plusDays(3);
-
Eager vs Lazy Evaluation − Eager evaluation means expressions are evaluated as soon as they are encountered whereas lazy evaluation refers to delaying the execution till certain condition is met. For example, stream methods in Java 8 are evaluated when a terminal method is encountered.
-
Persistent Data Structures
− A persistent data structure maintains its previous version. Whenever data structure state is changed, a new copy of structure is created so data structure remains effectively immutable. Such immutable collections are thread safe.
-
Recursion − A repeated calculation can be done by making a loop or using recursion more elegantly. A function is called recursive function if it calls itself.
-
Parallelism − Functions with no side effects can be called in any order and thus are candidate of lazy evaluation. Functional programming in Java supports parallelism using streams where parallel processing is provided.
-
Optionals − Optional is a special class which enforces that a function should never return null. It should return value using Optional class object. This returned object has method isPresent which can be checked to get the value only if present.
Functional Programming with Java — Functions
A function is a block of statements that performs a specific task. Functions accept data, process it, and return a result. Functions are written primarily to support the concept of re usability. Once a function is written, it can be called easily, without having to write the same code again and again.
Functional Programming revolves around first class functions, pure functions and high order functions.
-
A First Class Function is the one that uses first class entities like String, numbers which can be passed as arguments, can be returned or assigned to a variable.
-
A High Order Function is the one which can take a function as an argument and/or can return a function.
-
A Pure Function is the one which has no side effect while its execution.
First Class Function
A first class function can be treated as a variable. That means it can be passed as a parameter to a function, it can be returned by a function or can be assigned to a variable as well. Java supports first class function using lambda expression. A lambda expression is analogous to an anonymous function. See the example below −
public class FunctionTester { public static void main(String[] args) { int[] array = {1, 2, 3, 4, 5}; SquareMaker squareMaker = item -> item * item; for(int i = 0; i < array.length; i++){ System.out.println(squareMaker.square(array[i])); } } } interface SquareMaker { int square(int item); }
Output
1 4 9 16 25
Here we have created the implementation of square function using a lambda expression and assigned it to variable squareMaker.
High Order Function
A high order function either takes a function as a parameter or returns a function. In Java, we can pass or return a lambda expression to achieve such functionality.
import java.util.function.Function; public class FunctionTester { public static void main(String[] args) { int[] array = {1, 2, 3, 4, 5}; Function<Integer, Integer> square = t -> t * t; Function<Integer, Integer> cube = t -> t * t * t; for(int i = 0; i < array.length; i++){ print(square, array[i]); } for(int i = 0; i < array.length; i++){ print(cube, array[i]); } } private static <T, R> void print(Function<T, R> function, T t ) { System.out.println(function.apply(t)); } }
Output
1 4 9 16 25 1 8 27 64 125
Pure Function
A pure function does not modify any global variable or modify any reference passed as a parameter to it. So it has no side-effect. It always returns the same value when invoked with same parameters. Such functions are very useful and are thread safe. In example below, sum is a pure function.
public class FunctionTester { public static void main(String[] args) { int a, b; a = 1; b = 2; System.out.println(sum(a, b)); } private static int sum(int a, int b){ return a + b; } }
Output
3
Functional Programming with Java — Composition
Functional composition refers to a technique where multiple functions are combined together to a single function. We can combine lambda expression together. Java provides inbuilt support using Predicate and Function classes. Following example shows how to combine two functions using predicate approach.
import java.util.function.Predicate; public class FunctionTester { public static void main(String[] args) { Predicate<String> hasName = text -> text.contains("name"); Predicate<String> hasPassword = text -> text.contains("password"); Predicate<String> hasBothNameAndPassword = hasName.and(hasPassword); String queryString = "name=test;password=test"; System.out.println(hasBothNameAndPassword.test(queryString)); } }
Output
true
Predicate provides and() and or() method to combine functions. Whereas Function provides compose and andThen methods to combine functions. Following example shows how to combine two functions using Function approach.
import java.util.function.Function; public class FunctionTester { public static void main(String[] args) { Function<Integer, Integer> multiply = t -> t *3; Function<Integer, Integer> add = t -> t + 3; Function<Integer, Integer> FirstMultiplyThenAdd = multiply.compose(add); Function<Integer, Integer> FirstAddThenMultiply = multiply.andThen(add); System.out.println(FirstMultiplyThenAdd.apply(3)); System.out.println(FirstAddThenMultiply.apply(3)); } }
Output
18 12
Eager vs Lazy Evaluation
Eager evaluation means expression is evaluated as soon as it is encountered where as lazy evaluation refers to evaluation of an expression when needed. See the following example to under the concept.
import java.util.function.Supplier; public class FunctionTester { public static void main(String[] args) { String queryString = "password=test"; System.out.println(checkInEagerWay(hasName(queryString) , hasPassword(queryString))); System.out.println(checkInLazyWay(() -> hasName(queryString) , () -> hasPassword(queryString))); } private static boolean hasName(String queryString){ System.out.println("Checking name: "); return queryString.contains("name"); } private static boolean hasPassword(String queryString){ System.out.println("Checking password: "); return queryString.contains("password"); } private static String checkInEagerWay(boolean result1, boolean result2){ return (result1 && result2) ? "all conditions passed": "failed."; } private static String checkInLazyWay(Supplier<Boolean> result1, Supplier<Boolean> result2){ return (result1.get() && result2.get()) ? "all conditions passed": "failed."; } }
Output
Checking name: Checking password: failed. Checking name: failed.
Here checkInEagerWay() function first evaluated the parameters then executes its statement. Whereas checkInLazyWay() executes its statement and evaluates the parameter on need basis. As && is a short-circuit operator, checkInLazyWay only evaluated first parameter which comes as false and does not evaluate the second parameter at all.
Persistent Data Structure
A data structure is said to be persistent if it is capable to maintaining its previous updates as separate versions and each version can be accessed and updated accordingly. It makes the data structure immutable and thread safe. For example, String class object in Java is immutable. Whenever we make any change to string, JVM creates another string object, assigned it the new value and preserve the older value as old string object.
A persistent data structure is also called a functional data structure. Consider the following case −
Non-Persistent way
public static Person updateAge(Person person, int age){ person.setAge(age); return person; }
Persistent way
public static Person updateAge(Person pPerson, int age){ Person person = new Person(); person.setAge(age); return person; }
Functional Programming with Java — Recursion
Recursion is calling a same function in a function until certain condition are met. It helps in breaking big problem into smaller ones. Recursion also makes code more readable and expressive.
Imperative vs Recursive
Following examples shows the calculation of sum of natural numbers using both the techniques.
public class FunctionTester { public static void main(String[] args) { System.out.println("Sum using imperative way. Sum(5) : " + sum(5)); System.out.println("Sum using recursive way. Sum(5) : " + sumRecursive(5)); } private static int sum(int n){ int result = 0; for(int i = 1; i <= n; i++){ result = result + i; } return result; } private static int sumRecursive(int n){ if(n == 1){ return 1; }else{ return n + sumRecursive(n-1); } } }
Output
Sum using imperative way. Sum(5) : 15 Sum using recursive way. Sum(5) : 15
Using recursion, we are adding the result of sum of n-1 natural numbers with n to get the required result.
Tail Recursion
Tail recursion says that recursive method call should be at the end. Following examples shows the printing of a number series using tail recursion.
public class FunctionTester { public static void main(String[] args) { printUsingTailRecursion(5); } public static void printUsingTailRecursion(int n){ if(n == 0) return; else System.out.println(n); printUsingTailRecursion(n-1); } }
Output
5 4 3 2 1
Head Recursion
Head recursion says that recursive method call should be in the beginning of the code. Following examples shows the printing of a number series using head recursion.
public class FunctionTester { public static void main(String[] args) { printUsingHeadRecursion(5); } public static void printUsingHeadRecursion(int n){ if(n == 0) return; else printUsingHeadRecursion(n-1); System.out.println(n); } }
Output
1 2 3 4 5
Functional Programming with Java — Parallelism
Parallelism is a key concept of functional programming where a big task is accomplished by breaking in smaller independent tasks and then these small tasks are completed in a parallel fashion and later combined to give the complete result. With the advent of multi-core processors, this technique helps in faster code execution. Java has Thread based programming support for parallel processing but it is quite tedious to learn and difficult to implement without bugs. Java 8 onwards, stream have parallel method and collections has parallelStream() method to complete tasks in parallel fashion. See the example below:
import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class FunctionTester { public static void main(String[] args) { Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8 }; List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray)); System.out.println("List using Serial Stream:"); listOfIntegers .stream() .forEach(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("List using Parallel Stream:"); listOfIntegers .parallelStream() .forEach(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("List using Another Parallel Stream:"); listOfIntegers .stream() .parallel() .forEach(e -> System.out.print(e + " ")); System.out.println(""); System.out.println("List using Parallel Stream but Ordered:"); listOfIntegers .parallelStream() .forEachOrdered(e -> System.out.print(e + " ")); System.out.println(""); } }
Output
List using Serial Stream: 1 2 3 4 5 6 7 8 List using Parallel Stream: 6 5 8 7 3 4 2 1 List using Another Parallel Stream: 6 2 1 7 4 3 8 5 List using Parallel Stream but Ordered: 1 2 3 4 5 6 7 8
Optionals and Monads
Monad is a key concept of Functional Programming. A Monad is a design pattern which helps to represent a missing value. It allows to wrap a potential null value, allows to put transformation around it and pull actual value if present. By definition, a monad is a set of following parameters.
-
A parametrized Type − M<T>
-
A unit Function − T −> M<T>
-
A bind operation − M<T> bind T −> M<U> = M<U>
Key Operations
-
Left Identity − If a function is bind on a monad of a particular value then its result will be same as if function is applied to the value.
-
Right Identity − If a monad return method is same as monad on original value.
-
Associativity − Functions can be applied in any order on a monad.
Optional Class
Java 8 introduced Optional class which is a monad. It provides operational equivalent to a monad. For example return is a operation which takes a value and return the monad. Optional.of() takes a parameters and returns the Optional Object. On similar basis , bind is operation which binds a function to a monad to produce a monad. Optional.flatMap() is the method which performs an operation on Optional and return the result as Optional.
-
A parametrized Type − Optional<T>
-
A unit Function − Optional.of()
-
A bind operation − Optional.flatMap()
Example − Left Identity
Following example shows how Optional class obeys Left Identity rule.
import java.util.Optional; import java.util.function.Function; public class FunctionTester { public static void main(String[] args) { Function<Integer, Optional<Integer>> addOneToX = x −> Optional.of(x + 1); System.out.println(Optional.of(5).flatMap(addOneToX) .equals(addOneToX.apply(5))); } }
Output
true
Example − Right Identity
Following example shows how Optional class obeys Right Identity rule.
import java.util.Optional; public class FunctionTester { public static void main(String[] args) { System.out.println(Optional.of(5).flatMap(Optional::of) .equals(Optional.of(5))); } }
Output
true
Example — Associativity
Following example shows how Optional class obeys Associativity rule.
import java.util.Optional; import java.util.function.Function; public class FunctionTester { public static void main(String[] args) { Function<Integer, Optional<Integer>> addOneToX = x −> Optional.of(x + 1); Function<Integer, Optional<Integer>> addTwoToX = x −> Optional.of(x + 2); Function<Integer, Optional<Integer>> addThreeToX = x −> addOneToX.apply(x).flatMap(addTwoToX); Optional.of(5).flatMap(addOneToX).flatMap(addTwoToX) .equals(Optional.of(5).flatMap(addThreeToX)); } }
Output
true
Functional Programming with Java — Closure
A closure is a function which is a combination of function along with its surrounding state. A closure function generally have access to outer function’s scope. In the example given below, we have created a function getWeekDay(String[] days) which returns a function which can return the text equivalent of a weekday. Here getWeekDay() is a closure which is returning a function surrounding the calling function’s scope.
Following example shows how Closure works.
import java.util.function.Function; public class FunctionTester { public static void main(String[] args) { String[] weekDays = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }; Function<Integer, String> getIndianWeekDay = getWeekDay(weekDays); System.out.println(getIndianWeekDay.apply(6)); } public static Function<Integer, String> getWeekDay(String[] weekDays){ return index -> index >= 0 ? weekDays[index % 7] : null; } }
Output
Sunday
Functional Programming with Java — Currying
Currying is a technique where a many arguments function call is replaced with multiple method calls with lesser arguments.
See the below equation.
(1 + 2 + 3) = 1 + (2 + 3) = 1 + 5 = 6
In terms of functions:
f(1,2,3) = g(1) + h(2 + 3) = 1 + 5 = 6
This cascading of functions is called currying and calls to cascaded functions must gives the same result as by calling the main function.
Following example shows how Currying works.
import java.util.function.Function; public class FunctionTester { public static void main(String[] args) { Function<Integer, Function<Integer, Function<Integer, Integer>>> addNumbers = u -> v -> w -> u + v + w; int result = addNumbers.apply(2).apply(3).apply(4); System.out.println(result); } }
Output
9
Functional Programming with Java — Reducing
In functional programming, reducing is a technique to reduce a stream of values to a single result by apply a function on all the values. Java provides reduce() function in a Stream class from Java 8 onwards. A stream has inbuilt reducing methods like sum(), average(), count() as well which works on all elements of the stream and returns the single result.
Following example shows how Reducing works.
import java.util.stream.IntStream; public class FunctionTester { public static void main(String[] args) { //1 * 2 * 3 * 4 = 24 int product = IntStream.range(1, 5) .reduce((num1, num2) -> num1 * num2) .orElse(-1); //1 + 2 + 3 + 4 = 10 int sum = IntStream.range(1, 5).sum(); System.out.println(product); System.out.println(sum); } }
Output
24 10
Functional Programming — Lambda Expressions
Lambda expressions are introduced in Java 8 and are touted to be the biggest feature of Java 8. Lambda expression facilitates functional programming, and simplifies the development a lot.
Syntax
A lambda expression is characterized by the following syntax.
parameter -> expression body
Following are the important characteristics of a lambda expression.
-
Optional type declaration − No need to declare the type of a parameter. The compiler can inference the same from the value of the parameter.
-
Optional parenthesis around parameter − No need to declare a single parameter in parenthesis. For multiple parameters, parentheses are required.
-
Optional curly braces − No need to use curly braces in expression body if the body contains a single statement.
-
Optional return keyword − The compiler automatically returns the value if the body has a single expression to return the value. Curly braces are required to indicate that expression returns a value.
Lambda Expressions Example
Create the following Java program using any editor of your choice in, say, C:> JAVA.
Java8Tester.java
public class Java8Tester { public static void main(String args[]) { Java8Tester tester = new Java8Tester(); //with type declaration MathOperation addition = (int a, int b) -> a + b; //with out type declaration MathOperation subtraction = (a, b) -> a - b; //with return statement along with curly braces MathOperation multiplication = (int a, int b) -> { return a * b; }; //without return statement and without curly braces MathOperation division = (int a, int b) -> a / b; System.out.println("10 + 5 = " + tester.operate(10, 5, addition)); System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction)); System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication)); System.out.println("10 / 5 = " + tester.operate(10, 5, division)); //without parenthesis GreetingService greetService1 = message -> System.out.println("Hello " + message); //with parenthesis GreetingService greetService2 = (message) -> System.out.println("Hello " + message); greetService1.sayMessage("Mahesh"); greetService2.sayMessage("Suresh"); } interface MathOperation { int operation(int a, int b); } interface GreetingService { void sayMessage(String message); } private int operate(int a, int b, MathOperation mathOperation) { return mathOperation.operation(a, b); } }
Verify the Result
Compile the class using javac compiler as follows −
C:JAVA>javac Java8Tester.java
Now run the Java8Tester as follows −
C:JAVA>java Java8Tester
It should produce the following output −
10 + 5 = 15 10 - 5 = 5 10 x 5 = 50 10 / 5 = 2 Hello Mahesh Hello Suresh
Following are the important points to be considered in the above example.
-
Lambda expressions are used primarily to define inline implementation of a functional interface, i.e., an interface with a single method only. In the above example, we’ve used various types of lambda expressions to define the operation method of MathOperation interface. Then we have defined the implementation of sayMessage of GreetingService.
-
Lambda expression eliminates the need of anonymous class and gives a very simple yet powerful functional programming capability to Java.
Scope
Using lambda expression, you can refer to any final variable or effectively final variable (which is assigned only once). Lambda expression throws a compilation error, if a variable is assigned a value the second time.
Scope Example
Create the following Java program using any editor of your choice in, say, C:> JAVA.
Java8Tester.java
public class Java8Tester { final static String salutation = "Hello! "; public static void main(String args[]) { GreetingService greetService1 = message -> System.out.println(salutation + message); greetService1.sayMessage("Mahesh"); } interface GreetingService { void sayMessage(String message); } }
Verify the Result
Compile the class using javac compiler as follows −
C:JAVA>javac Java8Tester.java
Now run the Java8Tester as follows −
C:JAVA>java Java8Tester
It should produce the following output −
Hello! Mahesh
Functional Programming — Default Methods
Java 8 introduces a new concept of default method implementation in interfaces. This capability is added for backward compatibility so that old interfaces can be used to leverage the lambda expression capability of Java 8.
For example, ‘List’ or ‘Collection’ interfaces do not have ‘forEach’ method declaration. Thus, adding such method will simply break the collection framework implementations. Java 8 introduces default method so that List/Collection interface can have a default implementation of forEach method, and the class implementing these interfaces need not implement the same.
Syntax
public interface vehicle { default void print() { System.out.println("I am a vehicle!"); } }
Multiple Defaults
With default functions in interfaces, there is a possibility that a class is implementing two interfaces with same default methods. The following code explains how this ambiguity can be resolved.
public interface vehicle { default void print() { System.out.println("I am a vehicle!"); } } public interface fourWheeler { default void print() { System.out.println("I am a four wheeler!"); } }
First solution is to create an own method that overrides the default implementation.
public class car implements vehicle, fourWheeler { public void print() { System.out.println("I am a four wheeler car vehicle!"); } }
Second solution is to call the default method of the specified interface using super.
public class car implements vehicle, fourWheeler { default void print() { vehicle.super.print(); } }
Static Default Methods
An interface can also have static helper methods from Java 8 onwards.
public interface vehicle { default void print() { System.out.println("I am a vehicle!"); } static void blowHorn() { System.out.println("Blowing horn!!!"); } }
Default Method Example
Create the following Java program using any editor of your choice in, say, C:> JAVA.
Java8Tester.java
public class Java8Tester { public static void main(String args[]) { Vehicle vehicle = new Car(); vehicle.print(); } } interface Vehicle { default void print() { System.out.println("I am a vehicle!"); } static void blowHorn() { System.out.println("Blowing horn!!!"); } } interface FourWheeler { default void print() { System.out.println("I am a four wheeler!"); } } class Car implements Vehicle, FourWheeler { public void print() { Vehicle.super.print(); FourWheeler.super.print(); Vehicle.blowHorn(); System.out.println("I am a car!"); } }
Verify the Result
Compile the class using javac compiler as follows −
C:JAVA>javac Java8Tester.java
Now run the Java8Tester as follows −
C:JAVA>java Java8Tester
It should produce the following output −
I am a vehicle! I am a four wheeler! Blowing horn!!! I am a car!
Functional Programming — Functional Interfaces
Functional interfaces have a single functionality to exhibit. For example, a Comparable interface with a single method ‘compareTo’ is used for comparison purpose. Java 8 has defined a lot of functional interfaces to be used extensively in lambda expressions. Following is the list of functional interfaces defined in java.util.Function package.
Sr.No. | Interface & Description |
---|---|
1 |
BiConsumer<T,U> Represents an operation that accepts two input arguments, and returns no result. |
2 |
BiFunction<T,U,R> Represents a function that accepts two arguments and produces a result. |
3 |
BinaryOperator<T> Represents an operation upon two operands of the same type, producing a result of the same type as the operands. |
4 |
BiPredicate<T,U> Represents a predicate (Boolean-valued function) of two arguments. |
5 |
BooleanSupplier Represents a supplier of Boolean-valued results. |
6 |
Consumer<T> Represents an operation that accepts a single input argument and returns no result. |
7 |
DoubleBinaryOperator Represents an operation upon two double-valued operands and producing a double-valued result. |
8 |
DoubleConsumer Represents an operation that accepts a single double-valued argument and returns no result. |
9 |
DoubleFunction<R> Represents a function that accepts a double-valued argument and produces a result. |
10 |
DoublePredicate Represents a predicate (Boolean-valued function) of one double-valued argument. |
11 |
DoubleSupplier Represents a supplier of double-valued results. |
12 |
DoubleToIntFunction Represents a function that accepts a double-valued argument and produces an int-valued result. |
13 |
DoubleToLongFunction Represents a function that accepts a double-valued argument and produces a long-valued result. |
14 |
DoubleUnaryOperator Represents an operation on a single double-valued operand that produces a double-valued result. |
15 |
Function<T,R> Represents a function that accepts one argument and produces a result. |
16 |
IntBinaryOperator Represents an operation upon two int-valued operands and produces an int-valued result. |
17 |
IntConsumer Represents an operation that accepts a single int-valued argument and returns no result. |
18 |
IntFunction<R> Represents a function that accepts an int-valued argument and produces a result. |
19 |
IntPredicate Represents a predicate (Boolean-valued function) of one int-valued argument. |
20 |
IntSupplier Represents a supplier of int-valued results. |
21 |
IntToDoubleFunction Represents a function that accepts an int-valued argument and produces a double-valued result. |
22 |
IntToLongFunction Represents a function that accepts an int-valued argument and produces a long-valued result. |
23 |
IntUnaryOperator Represents an operation on a single int-valued operand that produces an int-valued result. |
24 |
LongBinaryOperator Represents an operation upon two long-valued operands and produces a long-valued result. |
25 |
LongConsumer Represents an operation that accepts a single long-valued argument and returns no result. |
26 |
LongFunction<R> Represents a function that accepts a long-valued argument and produces a result. |
27 |
LongPredicate Represents a predicate (Boolean-valued function) of one long-valued argument. |
28 |
LongSupplier Represents a supplier of long-valued results. |
29 |
LongToDoubleFunction Represents a function that accepts a long-valued argument and produces a double-valued result. |
30 |
LongToIntFunction Represents a function that accepts a long-valued argument and produces an int-valued result. |
31 |
LongUnaryOperator Represents an operation on a single long-valued operand that produces a long-valued result. |
32 |
ObjDoubleConsumer<T> Represents an operation that accepts an object-valued and a double-valued argument, and returns no result. |
33 |
ObjIntConsumer<T> Represents an operation that accepts an object-valued and an int-valued argument, and returns no result. |
34 |
ObjLongConsumer<T> Represents an operation that accepts an object-valued and a long-valued argument, and returns no result. |
35 |
Predicate<T> Represents a predicate (Boolean-valued function) of one argument. |
36 |
Supplier<T> Represents a supplier of results. |
37 |
ToDoubleBiFunction<T,U> Represents a function that accepts two arguments and produces a double-valued result. |
38 |
ToDoubleFunction<T> Represents a function that produces a double-valued result. |
39 |
ToIntBiFunction<T,U> Represents a function that accepts two arguments and produces an int-valued result. |
40 |
ToIntFunction<T> Represents a function that produces an int-valued result. |
41 |
ToLongBiFunction<T,U> Represents a function that accepts two arguments and produces a long-valued result. |
42 |
ToLongFunction<T> Represents a function that produces a long-valued result. |
43 |
UnaryOperator<T> Represents an operation on a single operand that produces a result of the same type as its operand. |
Functional Interface Example
Predicate <T> interface is a functional interface with a method test(Object) to return a Boolean value. This interface signifies that an object is tested to be true or false.
Create the following Java program using any editor of your choice in, say, C:> JAVA.
Java8Tester.java
import java.util.Arrays; import java.util.List; import java.util.function.Predicate; public class Java8Tester { public static void main(String args[]) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); // Predicate<Integer> predicate = n -> true // n is passed as parameter to test method of Predicate interface // test method will always return true no matter what value n has. System.out.println("Print all numbers:"); //pass n as parameter eval(list, n->true); // Predicate<Integer> predicate1 = n -> n%2 == 0 // n is passed as parameter to test method of Predicate interface // test method will return true if n%2 comes to be zero System.out.println("Print even numbers:"); eval(list, n-> n%2 == 0 ); // Predicate<Integer> predicate2 = n -> n > 3 // n is passed as parameter to test method of Predicate interface // test method will return true if n is greater than 3. System.out.println("Print numbers greater than 3:"); eval(list, n-> n > 3 ); } public static void eval(List<Integer> list, Predicate<Integer> predicate) { for(Integer n: list) { if(predicate.test(n)) { System.out.println(n + " "); } } } }
Here we’ve passed Predicate interface, which takes a single input and returns Boolean.
Verify the Result
Compile the class using javac compiler as follows −
C:JAVA>javac Java8Tester.java
Now run the Java8Tester as follows −
C:JAVA>java Java8Tester
It should produce the following output −
Print all numbers: 1 2 3 4 5 6 7 8 9 Print even numbers: 2 4 6 8 Print numbers greater than 3: 4 5 6 7 8 9
Functional Programming — Method References
Method references help to point to methods by their names. A method reference is described using «::» symbol. A method reference can be used to point the following types of methods −
-
Static methods — static method can be reference using ClassName::Method name notation.
//Method Reference - Static way Factory vehicle_factory_static = VehicleFactory::prepareVehicleInStaticMode;
-
Instance methods — instance method can be reference using Object::Method name notation.
//Method Reference - Instance way Factory vehicle_factory_instance = new VehicleFactory()::prepareVehicle;
Following example shows how Method references works in Java 8 onwards.
interface Factory { Vehicle prepare(String make, String model, int year); } class Vehicle { private String make; private String model; private int year; Vehicle(String make, String model, int year){ this.make = make; this.model = model; this.year = year; } public String toString(){ return "Vehicle[" + make +", " + model + ", " + year+ "]"; } } class VehicleFactory { static Vehicle prepareVehicleInStaticMode(String make, String model, int year){ return new Vehicle(make, model, year); } Vehicle prepareVehicle(String make, String model, int year){ return new Vehicle(make, model, year); } } public class FunctionTester { public static void main(String[] args) { //Method Reference - Static way Factory vehicle_factory_static = VehicleFactory::prepareVehicleInStaticMode; Vehicle carHyundai = vehicle_factory_static.prepare("Hyundai", "Verna", 2018); System.out.println(carHyundai); //Method Reference - Instance way Factory vehicle_factory_instance = new VehicleFactory()::prepareVehicle; Vehicle carTata = vehicle_factory_instance.prepare("Tata", "Harrier", 2019); System.out.println(carTata); } }
Output
Vehicle[Hyundai, Verna, 2018] Vehicle[Tata, Harrier, 2019]
Constructor References
Constructor references help to point to Constructor method. A Constructor reference is accessed using «::new» symbol.
//Constructor reference Factory vehicle_factory = Vehicle::new;
Following example shows how Constructor references works in Java 8 onwards.
interface Factory { Vehicle prepare(String make, String model, int year); } class Vehicle { private String make; private String model; private int year; Vehicle(String make, String model, int year){ this.make = make; this.model = model; this.year = year; } public String toString(){ return "Vehicle[" + make +", " + model + ", " + year+ "]"; } } public class FunctionTester { static Vehicle factory(Factory factoryObj, String make, String model, int year){ return factoryObj.prepare(make, model, year); } public static void main(String[] args) { //Constructor reference Factory vehicle_factory = Vehicle::new; Vehicle carHonda = factory(vehicle_factory, "Honda", "Civic", 2017); System.out.println(carHonda); } }
Output
Vehicle[Honda, Civic, 2017]
Functional Programming with Java — Collections
With Java 8 onwards, streams are introduced in Java and methods are added to collections to get a stream. Once a stream object is retrieved from a collection, we can apply various functional programming aspects like filtering, mapping, reducing etc. on collections. See the example below −
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class FunctionTester { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); //Mapping //get list of unique squares List<Integer> squaresList = numbers.stream().map( i -> i*i) .distinct().collect(Collectors.toList()); System.out.println(squaresList); //Filering //get list of non-empty strings List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); List<String> nonEmptyStrings = strings.stream() .filter(string -> !string.isEmpty()).collect(Collectors.toList()); System.out.println(nonEmptyStrings); //Reducing int sum = numbers.stream().reduce((num1, num2) -> num1 + num2).orElse(-1); System.out.println(sum); } }
Output
[9, 4, 49, 25] [abc, bc, efg, abcd, jkl] 25
Functional Programming — High Order Functions
A function is considered as a High Order function if it fulfils any one of the following conditions.
-
It takes one or more parameters as functions.
-
It returns a function after its execution.
Java 8 Collections.sort() method is an ideal example of a high order function. It accepts a comparing method as an argument. See the example below −
import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; public class FunctionTester { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(3, 4, 6, 7, 9); //Passing a function as lambda expression Collections.sort(numbers, (a,b) ->{ return a.compareTo(b); }); System.out.println(numbers); Comparator<Integer> comparator = (a,b) ->{ return a.compareTo(b); }; Comparator<Integer> reverseComparator = comparator.reversed(); //Passing a function Collections.sort(numbers, reverseComparator); System.out.println(numbers); } }
Output
[3, 4, 6, 7, 9] [9, 7, 6, 4, 3]
Functional Programming — Returning a Function
As a High Order function can return a function but how to implement using Java 8. Java 8 has provided Function interface which can accept a lambda expression. A high order function can return a lamdba expression and thus this high order function can be used to create any number of functions. See the example below −
import java.util.function.Function; public class FunctionTester { public static void main(String[] args) { Function<Integer, Integer> addOne = adder(1); Function<Integer, Integer> addTwo = adder(2); Function<Integer, Integer> addThree = adder(3); //result = 4 + 1 = 5 Integer result = addOne.apply(4); System.out.println(result); //result = 4 + 2 = 6 result = addTwo.apply(4); System.out.println(result); //result = 4 + 3 = 7 result = addThree.apply(4); System.out.println(result); } //adder - High Order Function //returns a function as lambda expression static Function<Integer, Integer> adder(Integer x){ return y -> y + x; } }
Output
5 6 7
Functional Programming — First Class Function
A function is called a first class function if it fulfills the following requirements.
-
It can be passed as a parameter to a function.
-
It can be returned from a function.
-
It can be assigned to a variable and then can be used later.
Java 8 supports functions as first class object using lambda expressions. A lambda expression is a function definition and can be assigned to a variable, can be passed as an argument and can be returned. See the example below −
@FunctionalInterface interface Calculator<X, Y> { public X compute(X a, Y b); } public class FunctionTester { public static void main(String[] args) { //Assign a function to a variable Calculator<Integer, Integer> calculator = (a,b) -> a * b; //call a function using function variable System.out.println(calculator.compute(2, 3)); //Pass the function as a parameter printResult(calculator, 2, 3); //Get the function as a return result Calculator<Integer, Integer> calculator1 = getCalculator(); System.out.println(calculator1.compute(2, 3)); } //Function as a parameter static void printResult(Calculator<Integer, Integer> calculator, Integer a, Integer b){ System.out.println(calculator.compute(a, b)); } //Function as return value static Calculator<Integer, Integer> getCalculator(){ Calculator<Integer, Integer> calculator = (a,b) -> a * b; return calculator; } }
Output
6 6 6
Functional Programming — Pure Function
A function is considered as Pure Function if it fulfils the following two conditions −
-
It always returns the same result for the given inputs and its results purely depends upon the inputs passed.
-
It has no side effects means it is not modifying any state of the caller entity.
Example- Pure Function
public class FunctionTester { public static void main(String[] args) { int result = sum(2,3); System.out.println(result); result = sum(2,3); System.out.println(result); } static int sum(int a, int b){ return a + b; } }
Output
5 5
Here sum() is a pure function as it always return 5 when passed 2 and 3 as parameters at different times and has no side effects.
Example- Impure Function
public class FunctionTester { private static double valueUsed = 0.0; public static void main(String[] args) { double result = randomSum(2.0,3.0); System.out.println(result); result = randomSum(2.0,3.0); System.out.println(result); } static double randomSum(double a, double b){ valueUsed = Math.random(); return valueUsed + a + b; } }
Output
5.919716721877799 5.4830887819586795
Here randomSum() is an impure function as it return different results when passed 2 and 3 as parameters at different times and modifies state of instance variable as well.
Functional Programming — Type Inference
Type inference is a technique by which a compiler automatically deduces the type of a parameter passed or of return type of a method. Java 8 onwards, Lambda expression uses type inference prominently.
See the example below for clarification on type inference.
Example- Type Inference
public class FunctionTester { public static void main(String[] args) { Join<Integer,Integer,Integer> sum = (a,b) -> a + b; System.out.println(sum.compute(10,20)); Join<String, String, String> concat = (a,b) -> a + b; System.out.println(concat.compute("Hello ","World!")); } interface Join<K,V,R>{ R compute(K k ,V v); } }
Output
30 Hello World!
A lambda expression treats each parameter and its return type as Object initially and then inferred the data type accordingly. In first case, the type inferred is Integer and in second case type inferred is String.
Exception Handling in Lambda Expressions
Lambda expressions are difficult to write when the function throws a checked expression. See the example below −
import java.net.URLEncoder; import java.util.Arrays; import java.util.stream.Collectors; public class FunctionTester { public static void main(String[] args) { String url = "www.google.com"; System.out.println(encodedAddress(url)); } public static String encodedAddress(String... address) { return Arrays.stream(address) .map(s -> URLEncoder.encode(s, "UTF-8")) .collect(Collectors.joining(",")); } }
The above code fails to compile because URLEncode.encode() throws UnsupportedEncodingException and cannot be thrown by encodeAddress() method.
One possible solution is to extract URLEncoder.encode() into a separate method and handle the exception there.
import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Arrays; import java.util.stream.Collectors; public class FunctionTester { public static void main(String[] args) { String url = "www.google.com"; System.out.println(encodedAddress(url)); } public static String encodedString(String s) { try { URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return s; } public static String encodedAddress(String... address) { return Arrays.stream(address) .map(s -> encodedString(s)) .collect(Collectors.joining(",")); } }
But above approach is not good when we have multiple such methods which throws exception. See the following generalized solution using functional interface and a wrapper method.
import java.net.URLEncoder; import java.util.Arrays; import java.util.function.Function; import java.util.stream.Collectors; public class FunctionTester { public static void main(String[] args) { String url = "www.google.com"; System.out.println(encodedAddress(url)); } public static String encodedAddress(String... address) { return Arrays.stream(address) .map(wrapper(s -> URLEncoder.encode(s, "UTF-8"))) .collect(Collectors.joining(",")); } private static <T, R, E extends Exception> Function<T, R> wrapper(FunctionWithThrows<T, R, E> fe) { return arg -> { try { return fe.apply(arg); } catch (Exception e) { throw new RuntimeException(e); } }; } } @FunctionalInterface interface FunctionWithThrows<T, R, E extends Exception> { R apply(T t) throws E; }
Output
www.google.com
Intermediate Methods
Stream API was introduced in Java 8 to facilitate functional programming in Java. A Stream API is targeted towards processing of collections of objects in functional way. By definition, a Stream is a Java component which can do internal iteration of its elements.
A Stream interface has terminal as well as non-terminal methods. Non-terminal methods are such operations which adds a listener to a stream. When a terminal method of stream is invoked, then internal iteration of stream elements get started and listener(s) attached to stream are getting called for each element and result is collected by the terminal method.
Such non-terminal methods are called Intermediate methods. The Intermediate method can only be invoked by called a terminal method. Following are some of the important Intermediate methods of Stream interface.
-
filter − Filters out non-required elements from a stream based on given criteria. This method accepts a predicate and apply it on each element. If predicate function return true, element is included in returned stream.
-
map − Maps each element of a stream to another item based on given criteria. This method accepts a function and apply it on each element. For example, to convert each String element in a stream to upper-case String element.
-
flatMap − This method can be used to maps each element of a stream to multiple items based on given criteria. This method is used when a complex object needs to be broken into simple objects. For example, to convert list of sentences to list of words.
-
distinct − Returns a stream of unique elements if duplicates are present.
-
limit − Returns a stream of limited elements where limit is specified by passing a number to limit method.
Example — Intermediate Methods
import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class FunctionTester { public static void main(String[] args) { List<String> stringList = Arrays.asList("One", "Two", "Three", "Four", "Five", "One"); System.out.println("Example - Filtern"); //Filter strings whose length are greater than 3. Stream<String> longStrings = stringList .stream() .filter( s -> {return s.length() > 3; }); //print strings longStrings.forEach(System.out::println); System.out.println("nExample - Mapn"); //map strings to UPPER case and print stringList .stream() .map( s -> s.toUpperCase()) .forEach(System.out::println); List<String> sentenceList = Arrays.asList("I am Mahesh.", "I love Java 8 Streams."); System.out.println("nExample - flatMapn"); //map strings to UPPER case and print sentenceList .stream() .flatMap( s -> { return (Stream<String>) Arrays.asList(s.split(" ")).stream(); }) .forEach(System.out::println); System.out.println("nExample - distinctn"); //map strings to UPPER case and print stringList .stream() .distinct() .forEach(System.out::println); System.out.println("nExample - limitn"); //map strings to UPPER case and print stringList .stream() .limit(2) .forEach(System.out::println); } }
Output
Example - Filter Three Four Five Example - Map ONE TWO THREE FOUR FIVE ONE Example - flatMap I am Mahesh. I love Java 8 Streams. Example - distinct One Two Three Four Five Example - limit One Two
Functional Programming — Terminal Methods
When a terminal method in invoked on a stream, iteration starts on stream and any other chained stream. Once the iteration is over then the result of terminal method is returned. A terminal method does not return a Stream thus once a terminal method is invoked over a stream then its chaining of non-terminal methods or intermediate methods stops/terminates.
Generally, terminal methods returns a single value and are invoked on each element of the stream. Following are some of the important terminal methods of Stream interface. Each terminal function takes a predicate function, initiates the iterations of elements, apply the predicate on each element.
-
anyMatch − If predicate returns true for any of the element, it returns true. If no element matches, false is returned.
-
allMatch − If predicate returns false for any of the element, it returns false. If all element matches, true is returned.
-
noneMatch − If no element matches, true is returned otherwise false is returned.
-
collect − each element is stored into the collection passed.
-
count − returns count of elements passed through intermediate methods.
-
findAny − returns Optional instance containing any element or empty instance is returned.
-
findFirst − returns first element under Optional instance. For empty stream, empty instance is returned.
-
forEach − apply the consumer function on each element. Used to print all elements of a stream.
-
min − returns the smallest element of the stream. Compares elements based on comparator predicate passed.
-
max − returns the largest element of the stream. Compares elements based on comparator predicate passed.
-
reduce − reduces all elements to a single element using the predicate passed.
-
toArray − returns arrays of elements of stream.
Example — Terminal Methods
import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class FunctionTester { public static void main(String[] args) { List<String> stringList = Arrays.asList("One", "Two", "Three", "Four", "Five", "One"); System.out.println("Example - anyMatchn"); //anyMatch - check if Two is present? System.out.println("Two is present: " + stringList .stream() .anyMatch(s -> {return s.contains("Two");})); System.out.println("nExample - allMatchn"); //allMatch - check if length of each string is greater than 2. System.out.println("Length > 2: " + stringList .stream() .allMatch(s -> {return s.length() > 2;})); System.out.println("nExample - noneMatchn"); //noneMatch - check if length of each string is greater than 6. System.out.println("Length > 6: " + stringList .stream() .noneMatch(s -> {return s.length() > 6;})); System.out.println("nExample - collectn"); System.out.println("List: " + stringList .stream() .filter(s -> {return s.length() > 3;}) .collect(Collectors.toList())); System.out.println("nExample - countn"); System.out.println("Count: " + stringList .stream() .filter(s -> {return s.length() > 3;}) .count()); System.out.println("nExample - findAnyn"); System.out.println("findAny: " + stringList .stream() .findAny().get()); System.out.println("nExample - findFirstn"); System.out.println("findFirst: " + stringList .stream() .findFirst().get()); System.out.println("nExample - forEachn"); stringList .stream() .forEach(System.out::println); System.out.println("nExample - minn"); System.out.println("min: " + stringList .stream() .min((s1, s2) -> { return s1.compareTo(s2);})); System.out.println("nExample - maxn"); System.out.println("min: " + stringList .stream() .max((s1, s2) -> { return s1.compareTo(s2);})); System.out.println("nExample - reducen"); System.out.println("reduced: " + stringList .stream() .reduce((s1, s2) -> { return s1 + ", "+ s2;}) .get()); } }
Output
Example - anyMatch Two is present: true Example - allMatch Length > 2: true Example - noneMatch Length > 6: true Example - collect List: [Three, Four, Five] Example - count Count: 3 Example - findAny findAny: One Example - findFirst findFirst: One Example - forEach One Two Three Four Five One Example - min min: Optional[Five] Example - max min: Optional[Two] Example - reduce reduced: One, Two, Three, Four, Five, One
Functional Programming — Infinite Streams
Collections are in-memory data structure which have all the elements present in the collection and we have external iteration to iterate through collection whereas Stream is a fixed data structure where elements are computed on demand and a Stream has inbuilt iteration to iterate through each element. Following example shows how to create a Stream from an array.
int[] numbers = {1, 2, 3, 4}; IntStream numbersFromArray = Arrays.stream(numbers);
Above stream is of fixed size being built from an array of four numbers and will not return element after 4th element. But we can create a Stream using Stream.iterate() or Stream.generate() method which can have lamdba expression will pass to a Stream. Using lamdba expression, we can pass a condition which once fulfilled give the required elements. Consider the case, where we need a list of numbers which are multiple of 3.
Example — Infinite Stream
import java.util.stream.Stream; public class FunctionTester { public static void main(String[] args) { //create a stream of numbers which are multiple of 3 Stream<Integer> numbers = Stream.iterate(0, n -> n + 3); numbers .limit(10) .forEach(System.out::println); } }
Output
0 3 6 9 12 15 18 21 24 27
In order to operate on infinite stream, we’ve used limit() method of Stream interface to restrict the iteration of numbers when their count become 10.
Functional Programming — Fixed length Streams
There are multiple ways using which we can create fix length streams.
-
Using Stream.of() method
-
Using Collection.stream() method
-
Using Stream.builder() method
Following example shows all of the above ways to create a fix length stream.
Example — Fix Length Stream
import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class FunctionTester { public static void main(String[] args) { System.out.println("Stream.of():"); Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5); stream.forEach(System.out::println); System.out.println("Collection.stream():"); Integer[] numbers = {1, 2, 3, 4, 5}; List<Integer> list = Arrays.asList(numbers); list.stream().forEach(System.out::println); System.out.println("StreamBuilder.build():"); Stream.Builder<Integer> streamBuilder = Stream.builder(); streamBuilder.accept(1); streamBuilder.accept(2); streamBuilder.accept(3); streamBuilder.accept(4); streamBuilder.accept(5); streamBuilder.build().forEach(System.out::println); } }
Output
Stream.of(): 1 2 3 4 5 Collection.stream(): 1 2 3 4 5 StreamBuilder.build(): 1 2 3 4 5