Лямбда выражения
Определения:
ЦЕЛЕВОЙ ТИП - целевым типом лямбда-выражения называется тип контекста, в котором это выражение встречается, – например, тип локальной переменной, которой оно присваивается, или тип параметра метода, вместо которого оно передается.
ВЫЗЫВАЮЩИЙ ОБЪЕКТ - объект который вызывает текущий метод.
Введение в лямбда-выражения
Лямбда-выражение, по существу, является анонимным (т.е. безымянным) методом. Но этот метод не выполняется самостоятельно, а служит для реализации метода, определяемого в функциональном интерфейсе. Нередко лямбда-выражения называют также замыканиями.
Функциональным называется такой интерфейс, который содержит один и только один абстрактный метод. Как правило, в таком методе определяется предполагаемое назначение интерфейса. Следовательно, функциональный интерфейс представляет единственное действие. Например, стандартный интерфейс Runnable является функциональным, поскольку в нем определяется единственный метод run(), который, в свою очередь, определяет действие самого интерфейса Runnable. Кроме того, в функциональном интерфейсе определяется ЦЕЛЕВОЙ ТИП лямбда-выражения. В связи с этим необходимо подчеркнуть следующее: лямбда-выражение можно использовать только в том контексте, в котором определен его целевой тип.
И еще одно замечание: функциональный интерфейс иногда еще называют SАМ-типом, где сокращение SAM обозначает Single Abstract Method - единственный абстрактный метод.
Основные положения о лямбда-выражениях
Лямбда-выражение вносит новый элемент в синтаксис и операцию в язык Java. Эта новая операция называется лямбда-операцией или операцией-стрелкой ->. Она разделяет лямбда-выражение на две части:
- в левой части указываются любые параметры, требующиеся в лямбда-выражении. (Если же параметры не требуются, то они указываются пустым списком.)
- а в правой части находится ТЕЛО лямбда-выражения, где определяются действия, выполняемые лямбда-выражением.
Операция -> буквально означает "становиться" или "переходить".
В языке Java определены две разновидности тел лямбда-выражений. Одна из них состоит из единственного выражения, а другая - из блока кода.
Рассмотрим сначала лямбда-выражения, в теле которых определяется единственное выражение.
Так выглядит самое простое лямбда-выражение, какое только можно написать. В приведенном ниже лямбда-выражении вычисляется значение константы:
() -> 123.45
Это лямбда-выражение не принимает никаких параметров, т.е. список его параметров оказывается пустым. Оно возвращает значение константы 123.45. Это выражение аналогично вызову следующего метода:
double myMeth() { return 123.45; }
Ниже приведено более интересное лямбда-выражение:
() -> Math.random() * 100
В этом лямбда-выражении из метода Math.random() получается псевдослучайное значение, которое умножается на 100, и затем возвращается результат.
Ниже приведен простой пример лямбда-выражения с одним параметром:
(n) -> (n % 2) == 0
Это выражение возвращает логическое значение true, если числовое значение параметра n оказывается четным. Тип параметра (в данном случае n) можно указывать явно, но зачастую в этом нет никакой нужды, поскольку его тип в большинстве случаев выводится. Как и в именованном методе, в лямбда-выражении можно указывать столько параметров, сколько требуется.
Функциональные интерфейсы
Как пояснялось ранее, функциональным называется такой интерфейс, в котором определяется единственный абстрактный метод. Те, у кого имеется предыдущий опыт программирования на Java, могут возразить, что все методы интерфейса неявно считаются абстрактными, но так было до внедрения лямбда-выражений. Как пояснялось в главе 9, начиная с версии JDK 8, для метода, объявляемого в интерфейсе, можно определить стандартное поведение по умолчанию, и поэтому он называется методом с реализацией по умолчанию.
Ниже приведен пример объявления функционального интерфейса.Отныне метод интерфейса считается абстрактным лишь в том случае, если у него отсутствует реализация по умолчанию.
interface MyNumber { double getValue(); }В данном примере метод getValue() неявно считается абстрактным и единственным, определяемым в интерфейсе MyNumber. (нестатические, незакрытые и не реализуемые по умолчанию методы интерфейса неявно считаются абстрактными)
Как упоминалось ранее, лямбда-выражение не выполняется самостоятельно, а скорее образует реализацию абстрактного метода, определенного в функциональном интерфейсе, где указывается его целевой тип. Таким образом, лямбда-выражение может быть указано только в том контексте, в котором определен его целевой тип.
interface MyNumber { double getValue(); } MyNumber x; x = () -> 123.45; //Здесь целевой тип - MyNumberОдин из таких контекстов создается в том случае, когда лямбда-выражение присваивается ссылке на функциональный интерфейс. К числу других контекстов целевого типа относятся инициализация переменных, операторы return и аргументы методов.
в контексте присваивания:
Рассмотрим пример, демонстрирующий применение лямбда-выражения в контексте присваивания. С этой целью сначала объявляется ссылка на функциональный интерфейс MyNumber, как показано ниже.
// создать ссылку на функциональный интерфейс MyNumber MyNumber myNum;Затем лямбда-выражение присваивается этой ссылке на функциональный интерфейс следующим образом:
//использовать лямбда-выражение в контексте присваивания myNum = () -> 123.45;Когда лямбда-выражение появляется в контексте своего целевого типа (MyNumber), автоматически создается экземпляр класса (myNum), реализующего функциональный интерфейс, причем лямбда-выражение определяет поведение абстрактного метода, объявляемого в функциональном интерфейсе. А когда этот метод вызывается через свой адресат, выполняется лямбда-выражение. Таким образом, лямбда-выражение позволяет преобразовать сегмент кода в объект.
Чтобы лямбда-выражение использовалось в контексте своего целевого типа, абстрактный метод и лямбда-выражение должны быть совместимыми по типу. Так, если в абстрактном методе указываются два параметра типа int, то и в лямбдавыражении должны быть указаны два параметра, тип которых явно обозначается как int или неявно выводится как int из самого контекста. В общем, параметры лямбда-выражения должны быть совместимы по типу и количеству с параметрами абстрактного метода. Это же относится и к возвращаемым типам. А любые исключения, генерируемые в лямбда-выражении, должны быть приемлемы для абстрактного метода.
Некоторые примеры лямбда-выражений
// Еще один функциональный интерфейс interface NumericTest { boolean test(int n); } // Лямбда-выражение, в котором проверяется, // является ли число четным NumericTest isEven = (n) -> (n % 2) == 0; int x = -22; if (isEven.test(x)) System.out.println("Число " + x + " чётное!"); if (!isEven.test(x)) System.out.println("Число " + x + " нечётное!"); // А теперь воспользоваться лямбда-выражением, // в котором проверяется, является ли число // неотрицательным NumericTest isNonNeg = (n) -> n >= 0; if (isNonNeg.test(x)) System.out.println("Чиcлo " + x + " неотрицательное!"); if (!isNonNeg.test(x)) System.out.println("Чиcлo " + x + " отрицательное!");В данном примере программы демонстрируется главная особенность лямбда-выражений, требующая более подробного рассмотрения. Обратите особое внимание на следующее лямбда-выражение, в котором выполняется проверка на равенство:
(n) -> (n % 2) == 0;Как видите, тип переменной n здесь не указан, но выводится из контекста. В данном случае тип переменной n выводится из типа int параметра метода test(), определяемого в функциональном интерфейсе NumericTest. Впрочем, ничто не мешает явно указать тип параметра в лямбда-выражении. Например, следующее лямбда-выражение так же достоверно, как и предыдущее:
(int n) -> (n % 2)==0Как правило, явно указывать тип параметров лямбда-выражений необязательно, хотя в некоторых случаях это может все же потребоваться.
В данном примере программы демонстрируется еще одна важная особенность лямбда-выражений. Ссылка на функциональный интерфейс может быть использована для выполнения любого совместимого с ней лямбда-выражения. Обратите внимание на то, что в данной программе определяются два разных лямбда-выражения, совместимых с методом test() из функционального интерфейса NumericTest. Но в любом случае проверяется значение параметра n. А поскольку каждое из этих лямбда-выражений совместимо с методом test() по типу данных, то оно выполняется по ссылке на функциональный интерфейс NumericTest.
Прежде чем продолжить, следует сделать еще одно замечание. Если у лямбдавыражения имеется единственный параметр, его совсем не обязательно заключать в круглые скобки в левой части лямбда-оператора. Например, приведенный ниже способ написания лямбда-выражения также допустим в исходном коде программы:
n -> (n % 2) == 0; // Продемонстрировать применение лямбда-выражения, // принимающего два параметра interface NumericTest2 { boolean test(int n, int d); } // В этом лямбда-выражении проверяется, //является ли одно число множителем другого NumericTest2 isFactor = (n, d) -> (n % d) == 0; //if (isFactor.test(10, 2)) ....Следует, однако, иметь в виду, что если требуется явно объявить тип одного из параметров лямбда-выражения, это следует сделать и для всех остальных параметров. Например, следующее лямбда-выражение достоверно:
(int n, int d) -> (n % d) == 0А это лямбда-выражение недостоверно:
(int n, d) -> (n % d) == 0
Блочные лямбда-выражения
Тело лямбда-выражений в предыдущих примерах состояло из единственного выражения. Такая его разновидность называется телом выражения, а лямбда-выражения с телом выражения иногда еще называют одиночными. В теле выражения код, указываемый в правой части лямбда-оператора, должен состоять из одного выражения. Несмотря на все удобство одиночных лямбда-выражений, иногда в них требуется вычислять не одно выражение. Для подобных случаев в Java предусмотрена вторая разновидность лямбда-выражений, где код, указываемый в правой части лямбда-оператора, может состоять из нескольких операторов. Такие лямбда-выражения называются блочными, а их тело - телом блока.
interface StringFunc { String func(String n); } // Это блочное выражение изменяет на обратный // порядок следования символов в строке StringFunc reverse = (str) -> { String result = ""; int i; for (i = str.length() - 1; i >= 0; i--) result += str.charAt(i); return result; }; //вызов System.out.println("Лямбдa обращается на " + reverse.func("Лямбдa"));
Обобщенные функциональные интерфейсы
Функциональный интерфейс, связанный с лямбда-выражением, может быть обобщенным. В этом случае целевой тип лямбда-выражения отчасти определяется аргументом типа или теми аргументами, которые указываются при объявлении ссылки на функциональный интерфейс.
Чтобы понять и оценить значение обобщенных функциональных интерфейсов, вернемся к двум примерам из предыдущего раздела. В них применялись два разных функциональных интерфейса: NumericFunc и StringFunc. Но в обоих этих интерфейсах был определен метод func(), возвращавший результат. В первом случае этот метод принимал параметр и возвращал значение типа int, а во втором случае - значение типа String. Следовательно, единственное отличие обоих вариантов этого метода состояло в типе требовавшихся данных. Вместо того чтобы объявлять два функциональных интерфейса, методы которых отличаются только типом данных, можно объявить один обобщенный интерфейс, который можно использовать в обоих случаях. Именно такой подход и принят в следующем примере программы:
см. package ms.learning.lambda; GenericFunctionalinterfaceDemo
В данном примере программы обобщенный функциональный интерфейс SomeFunc объявляется следующим образом:
interface SomeFunc { T func(T t); }Здесь T обозначает как возвращаемый тип, так и тип параметра метода func(). Это означает, что он совместим с любым лямбда-выражением, принимающим один параметр и возвращающим значение того же самого типа.
Обобщенный функциональный интерфейс SomeFunc служит для предоставления ссылки на два разных типа лямбда-выражений. В первом из них используется тип String, а во втором - тип Integer. Таким образом, один и тот же интерфейс может быть использован для обращения к обоим лямбда-выражениям - reverse и factorial.
Отличается лишь аргумент типа, передаваемый обобщенному функциональному интерфейсу SomeFunc.
Передача лямбда-выражений в качестве аргументов
Для передачи лямбда-выражения в качестве аргумента параметр, получающий это выражение в качестве аргумента, должен иметь тип функционального интерфейса, совместимого с этим лямбда-выражением. Несмотря на всю простоту применения лямбда-выражений в качестве передаваемых аргументов, полезно все же показать, как это происходит на практике. В следующем примере программы демонстрируется весь этот процесс:
interface StringFunc { String func(String n); } // Первый параметр этого метода имеет тип // функционального интерфейса. Следовательно, ему // можно передать ссылку на любой !!!экземпляр!!! этого // интерфейса, включая экземпляр, создаваемый** в // лямбда-выражении. А второй параметр обозначает // обрабатываемую символьную строку static String stringOp(StringFunc sf, String s) { return sf.func(s); }
"создаваемый в лямбда-выражении" - условно говоря - вместо лямбда выражения, можно представить экземпляр класса (sf), реализующего функциональный интерфейс этого лямбда- выражения (а не результат вычисления этого лямбда-выражения например), в данном случае StringFunc. А сам результат вычисления лямбда-выражения - будет получен там, где будет вызван (один-единственный) метод этого экземпляра класса (sf). В данном случае sf.func(s)
String inStr = "Лямбда-выражения повышают эффективность Java"; String outStr; // Ниже приведено простое лямбда-выражение, // преобразующее в прописные все буквы в исходной // строке, передаваемой методу stringOp() outStr = stringOp((str) -> str.toUpperCase(), inStr); System.out.println("Этo строка прописными буквами: " + outStr); // А здесь передается блочное лямбда-выражение, // удаляющее пробелы из исходной символьной строки outStr = stringOp((str) -> { String result = ""; int i; for (i = 0; i < str.length(); i++) if (str.charAt(i) != ' ') result += str.charAt(i); return result; }, inStr); System.out.println("Этo строка с удаленными пробелами: " + outStr); // Можно, конечно, передать и экземпляр // функционального интерфейса StringFunc, // созданный в предыдущем лямбда-выражении. // Например, после следующего объявления ссылка // reverse делается на экземпляр // интерфейса StringFunc StringFunc reverse = (str) -> { String result = ""; int i; for (i = str.length() - 1; i >= 0; i--) result += str.charAt(i); return result; }; // А теперь ссылку reverse можно передать в // качестве первого параметра методУ stringOp(), // поскольку она делается на объект типа StringFunc System.out.println("Этo обращенная строка: " + stringOp(reverse, inStr) );Прежде всего обратите внимание в данном примере программы на метод stringOp(), у которого имеются два параметра. Первый параметр относится к типу StringFunc, т.е. к функциональному интерфейсу. Следовательно, этот параметр может получать ссылку на любой экземпляр функционального интерфейса StringFunс, в том числе и создаваемый в лямбда-выражении. А второй параметр метода, stringOp(), относится к типу String и обозначает обрабатываемую символьную строку.
Затем обратите внимание на первый вызов метода stringOp():
outStr = stringOp( (str) -> str.toUpperCase(), inStr);Здесь в качестве аргумента данному методу передается простое лямбда-выражение. При этом создается экземпляр функционального интерфейса StringFunc и ссылка на данный объект передается первому параметру метода stringOp().
Таким образом, код лямбда-выражения, встраиваемый в экземпляр класса, передается данному методу. Контекст целевого типа лямбда-выражения определяется типом его параметра. А поскольку лямбда-выражение совместимо с этим типом, то рассматриваемый здесь вызов достоверен.
Встраивать в метод такие простые лямбда-выражения, как упомянутое выше, нередко оказывается очень удобно, особенно когда лямбда-выражение предназначается для однократного употребления.
Если блочное выражение кажется слишком длинным для встраивания в вызов метода, то его можно просто присвоить переменной ссылки на функциональный интерфейс, как это делалось в предыдущих примерах. И тогда остается только передать эту ссылку вызываемому методу. Такой прием показан в конце рассматриваемого здесь примера программы, где определяется блочное лямбда-выражение, изменяющее порядок следования символов в строке на обратный. Это лямбда-выражение присваивается переменной reverse, ссылающейся на функциональный интерфейс StringFunc. Следовательно, переменную reverse можно передать в качестве аргумента первому параметру метода stringOp().
Именно так и делается в конце данной программы, где методу stringOp() передаются переменная reverse и обрабатываемая символьная строка. Экземпляр, получаемый в результате вычисления каждого лямбда-выражения, является реализацией функционального интерфейса StringFunc, поэтому каждое из этих выражений может быть передано в качестве первого аргумента вызываемому методу stringOp().
!И последнее замечание: помимо инициализации переменных, присваивания и передачи аргументов, следующие операции образуют контекст целевого типа лямбда-выражений: приведение типов, тернарная операция ? , инициализация массивов, операторы return, а также сами лямбда-выражения.
Лямбда-выражения и исключения
Лямбда-выражение может генерировать исключение. Но если оно генерирует проверяемое исключение, то последнее должно быть совместимо с исключениями, перечисленными в выражении throws из объявления абстрактного метода в функциональном интерфейсе.
interface DouЬleNumericArrayFunc { double func(double[] n) throws EmptyArrayException; } class EmptyArrayException extends Exception { EmptyArrayException() { super("Maccив пуст"); // System.out.println("Maccив пуст"); } } public class LambdaExceptionDemo { public static void main(String[] args) throws EmptyArrayException { double[] values = { 1.0, 2.0, 3.0, 4.0 }; // В этом лямбда-выражении вычисляется среднее // числовых значений типа douЫe в массиве DoubleNumericArrayFunc average = (n) -> { double sum = 0; if (n.length == 0) throw new EmptyArrayException(); for (int i = 0; i < n.length; i++) sum += n[i]; return sum / n.length; }; System.out.println("Cpeднee равно " + average.func(values)); // Эта строка кода приводит к генерированию исключения System.out.println("Cpeднee равно " + average.func(new double[0])); } }Напомним, что наличие выражения throws в объявлении метода func() обязательно. Без этого программа не будет скомпилирована, поскольку лямбда-выражение перестанет быть совместимым с методом func().
Данный пример демонстрирует еще одну важную особенность лямбда-выражений. Обратите внимание на то, что параметр, указываемый при объявлении метода func() в функциональном интерфейсе DoubleNumericArrayFunc, обозначает массив, тогда как параметр лямбда-выражения просто указан как n, а не n[].
Напомним, что тип параметра лямбда-выражения выводится из целевого контекста. В данном случае целевым контекстом является массив типа double[],поэтому и параметр n относится к типу double[].Следовательно, указывать этот параметр как n[] совсем не обязательно и даже недопустимо. И хотя его можно было бы явно указать как double[] n, это не дало бы в данном случае никаких преимуществ.
Лямбда-выражения и захват переменных
Переменные, определяемые в объемлющей области действия лямбда-выражения, доступны в этом выражении. Например, в лямбда-выражении можно использовать переменную экземпляра или статическую переменную, определяемую в объемлющем его классе. В лямбда-выражении доступен также по ссылке this (явно или неявно) вызывающий экземпляр объемлющего его класса. Таким образом, в лямбдавыражении можно получить или установить значение переменной экземпляра или статической переменной и вызвать метод из объемлющего его класса.
Но если в лямбда-выражении используется локальная переменная из объемлющей его области видимости, то возникает особый случай, называемый захватом переменной. В этом случае в лямбда-выражении можно использовать только те локальные переменные, которые действительно являются конечными.
Действительно конечной считается такая переменная, значение которой не изменяется после ее первого присваивания. Такую переменную совсем не обязательно объявлять как final, хотя это и не считается ошибкой. (Параметр this в объемлющей области видимости автоматически оказывается действительно конечным, а у лямбда-выражений собственный параметр this отсутствует.)
Следует, однако, иметь в виду, что локальная переменная из объемлющей области видимости не может быть видоизменена в лямбда-выражении. Ведь это нарушило бы ее действительно конечное состояние, а следовательно, привело бы к недопустимому ее захвату.
interface MyFunc { int func(int n); } public class VarCapture { public static void main(String[] args) { // Локальная переменная, которая может быть захвачена int num = 10; MyFunc myLambda = (n) -> { // Такое применение переменной num допустимо, // поскольку она не видоизменяется int v = num + n; // Но следующая строка кода недопустима, поскольку // в ней предпринимается попытка видоизменить // значение переменной num // num++; return v; }; //И следующая строка кода приведет к ошибке, поскольку // в ней нарушается действительно конечное состояние //переменной num // num = 9; System.out.println("Результат: " + myLambda.func(6)); } }Как следует из комментариев к данному примеру программы, переменная num является действительно конечной, и поэтому ее можно использовать в лямбдавыражении myLambda. Но если попытаться видоизменить переменную num как в самом лямбда-выражении, так и за его пределами, то она утратит свое действительно конечное состояние. Это привело бы к ошибке, а программа не подлежала бы компиляции.
Ссылки на методы
С лямбда-выражениями связано еще одно очень важное средство, называемое ссылкой на метод. Такая ссылка позволяет обращаться к методу, не вызывая его. Она связана с лямбда-выражениями потому, что ей также требуется контекст целевого типа, состоящий из совместимого функционального интерфейса. Имеются разные виды ссылок на методы. Рассмотрим сначала ссылки на статические методы.
Ссылки на статические методы
Для создания ссылки на статический метод служит следующая общая форма:
имя_класса::имя_метода
Обратите внимание на то, что имя класса в этой форме отделяется от имени метода двоеточием (::). Этот новый разделитель внедрен в версии JDK 8 специально для данной цели. Такой ссылкой на метод можно пользоваться везде, где она совместима со своим целевым типом.
//Продемонстрировать ссылку на статический метод //Функциональный интерфейс для операций //над символьными строками interface StringFunc2 { String func(String n); } //В этом классе определяется статический //метод strReverse() class MyStringOps { // Статический метод, изменяющий порядок // следования символов в строке static String strReverse(String str) { String result = ""; int i; for (i = str.length() - 1; i >= 0; i--) result += str.charAt(i); return result; } } public class MethodRefDemo { // В этом методе функциональный интерфейс // указывается в качестве типа первого его // параметра. Следовательно, ему может быть // передан любой экземпляр данного интерфейса, // включая и ссылку на метод static String stringOp(StringFunc2 sf, String s) { // s = MyStringOps::strReverse(s); // return s; // return sf.strReverse(s); return sf.func(s); } public static void main(String[] args) { String inStr = "Лямбда-выражения повышают " + "эффективность Java"; String outStr; // Здесь ссылка на метод strReverse() передается //методу stringOp() outStr = stringOp(MyStringOps::strReverse, inStr); System.out.println("Иcxoднaя строка: " + inStr); System.out.println("Oбpaщeннaя строка: " + outStr); } }В этой строке кода ссылка на статический метод strReverse(),объявляемый в классе MyStringOps, передается первому аргументу метода stringOp(). И это вполне допустимо, поскольку метод strReversе() совместим с функциональным интерфейсом StringFunс2. Следовательно, в выражении МуStringОрs::strReverse вычисляется ссылка на объект того класса, в котором метод strReverse() предоставляет реализацию метода func() из функционального интерфейса StringFunc2.
Ссылки на методы экземпляра
Для передачи ссылки на метод экземпляра для конкретного объекта служит следующая общая форма:
ссылка_на_объект::имя_метода
Как видите, синтаксис этой формы ссылки на метод экземпляра похож на тот, что используется для ссылки на статический метод, за исключением того, что вместо имени класса в данном случае используется ссылка на объект. Ниже приведен переделанный вариант программы из предыдущего примера, чтобы продемонстрировать применение ссылки на метод экземпляра.
// Продемонстрировать применение ссылки // на метод экземпляра // Функциональный интерфейс для операций // над символьными строками interface StringFunc3 extends StringFunc { // String func(String n); } // Теперь в этом классе определяется // метод экземпляра strReverse() class MyStringOps2 { String strReverse(String str) { String result = ""; int i; for (i = str.length() - 1; i >= 0; i--) result += str.charAt(i); return result; } } public class MethodRefDemo2 { // В этом методе функциональный интерфейс // указывается в качестве типа первого его // параметра. Следовательно, ему может быть // передан любой экземпляр этого интерфейса, // включая и ссылку на метод static String stringOp(StringFunc3 sf, String s) { return sf.func(s); } public static void main(String[] args) { String inStr = "Лямбда-выражения повышают " + "эффективность Java"; String outStr; // создать объект типа MyStringOps MyStringOps2 strOps = new MyStringOps2(); // А теперь ссылка на метод экземпляра strReverse() // передается методу stringOp() outStr = stringOp(strOps::strReverse, inStr); System.out.println("Иcxoднaя строка: " + inStr); System.out.println("Oбpaщeннaя строка: " + outStr); } }Обратите внимание в данном примере программы на то, что метод strReverse() теперь объявляется в классе MyStringOps2 как метод экземпляра (без static). А в теле метода main() создается экземпляр strOps класса MyStringOps2. Этот экземпляр служит для создания ссылки на свой метод strReverse() при вызове метода stringOp().
Cсылки на метод произвольного объекта определённого типа
Возможны и такие случаи, когда требуется указать метод экземпляра, который будет использоваться вместе с любым объектом данного класса, а не только суказанным объектом. В подобных случаях можно создать ссылку на метод экземпляра в следующей общей форме:
имя_класса::имя_метода_экземпляра
// Функциональный интерфейс с методом, // принимающим два ссылочных аргумента и // возвращающим логическое значение interface MyFunc2 { boolean func(T v1, T v2); } // Класс для хранения максимальной температуры за день class HighTemp { private int hTemp; HighTemp(int ht) { hTemp = ht; } // возвратить логическое значение true, если // вызывающий объект типа HighTemp содержит такую // же температуру, как и у объекта ht2 boolean sameTemp(HighTemp ht2) { return this.hTemp == ht2.hTemp; } } static int counter(T[] vals, MyFunc2 f, T v) { int count = 0; for (int i = 0; i < vals.length; i++) if (f.func(vals[i], v)) //vals[i].sameTemp(v) - по факту!!! count++; return count; // (HighTemp ht1, HighTemp ht2) -> {return ht1.sameTemp(ht2)} } // использование ссылки count = counter(weekDayHighs, HighTemp::sameTemp, new HighTemp(89));
Пример "на пальцах"
Тема: "Передача ссылки на метод произвольного объекта определённого типа".
Вот инструкция с вызовом метода, который принимает так называемую "ссылку на метод произвольного объекта определённого типа"
count = counter(weekDayHighs2, HighTemp::sameTemp, new HighTemp(12));Попробуем представить работу Java, при встрече HighTemp::sameTemp так:
1. Ищет метод sameTemp в классе HighTemp.
2. Определяет что он не "static" (если был бы "static", то тут совсем другое поведение);
3. В найденый метод boolean sameTemp(HighTemp ht2) неявно, в начало, добавляет параметр HighTemp x. В результате сигнатура уже выглядит так:
boolean sameTemp(HighTemp x, HighTemp ht2)
4. Теперь, полученную и слегка "подправленную" (уже с ДВУМЯ параметрами) сигнатуру метода sameTemp
boolean sameTemp(HighTemp x, HighTemp ht2)
сравнивает с сигнатурой метода функционального интерфейса MyFunc
boolean func(T v1, T v2)
у которой тоже ДВА параметра одинакового типа, и такое же возвращаемое значение.
С этим всё, надеюсь понятно.
далее, когда Java встречает это варажение (в методе counter):
f.func(vals[i], v)перед тем как его исполнить, происходит
5. аргумент vals[i] - выносится из скобок и подставляется вместо f, становясь тем самым "произвольным объектом определённого типа" (из темы). То есть в пункте 3 этот параметр неявно был добавлен, а теперь он "исчезает" (объект vals[i] "вытянули" и он (параметр) больше не нужен!).
6. func(v) c оставшимся одним параметром, становится sameTemp(v) в итоге получаем, что Java исполняя это:
f.func(vals[i], v)фактически исполняет это:
vals[i].sameTemp(v)Важно понять, что метод sameTemp(HighTemp ht2) может иметь хоть 5 параметров! В любом случае (по приведённой схеме в п.п.3 и далее) в начало списка параметров будет неявно добавлен шестой параметр типа HighTemp, чтобы в дальнейшем получить в этом параметре "произвольный объект", извлечь его, и использовать как тот самый "произвольный объект" метод которого Java и будет вызывать.
Если у метода sameTemp было бы 5 параметров, то тогда у метода функционального интерфейса должно быть их 6. Первый из которых должен быть обязательно определённого типа (под "определённым типом" подразумевается класс, с методом "произвольного экземпляра" которого мы хотим ассоциировать метод интерфейса)
Примерно поняв, как это работает, название самой темы: - "Передача ссылки на метод произвольного обекта определённого типа" должна звучать слегка дружелюбнее).
Ссылки на обобщенные методы
Ссылками на методы можно также пользоваться для обращения к обобщенным классам и/или методам.
// Функциональный интерфейс для обработки массива // значений и возврата целочисленного результата interface MyFunc3 { int func(T[] vals, T v); } //В этом классе определяется метод countMatching(), //возвращающий количество элементов в массиве, //равных указанному значению. Обратите внимание на то, //что метод countMatching() является обобщенным, //тогда как класс MyArrayOps - необобщенным class MyArrayOps { static int countMatching(T[] vals, T v) { int count = 0; for (int i = 0; i < vals.length; i++) if (vals[i] == v) count++; return count; } } public class GenericMethodRefDemo { // В качестве первого параметра этого метода // указывается функциональный интерфейс MyFunc, // а в качестве двух других параметров - массив и // значение, причем оба типа Т static int myOp(MyFunc3 f, T[] vals, T v) { return f.func(vals, v); } public static void main(String[] args) { Integer[] vals = { 1, 2, 3, 4, 2, 3, 4, 4, 5 }; String[] strs = { "Один", "Два", "Три", "Два" }; int count; /* Если ссылка на обобщённый метод, то обобщённый тип указывается * после двоеточия (как в примере). Если ссылка на обобщённый класс * то обобщённый тип указывается до двоеточия * */ count = myOp(MyArrayOps::countMatching, vals, 4); System.out.println("Maccив vals содержит " + count + " числа четыре"); count = myOp(MyArrayOps::countMatching, strs, "Два"); System.out.println("Maccив strs содержит " + count + " числа два"); /*Следует, однако, заметить, что явно указывать аргумент типа в данном (и во многих других) случаях совсем не необязательно, поскольку тип этого аргумента выводится автоматически.*/ count = myOp(MyArrayOps::countMatching, strs, "Два"); System.out.println("Maccив strs содержит " + count + " числа два"); } }Несмотря на то что в предыдущих примерах был продемонстрирован механизм применения ссылок на методы, эти примеры все же не раскрывают в полной мере их преимуществ. Ссылки на методы могут, в частности, оказаться очень полезными в сочетании с каркасом коллекций Collections Fгamework. И ради полноты изложения ниже приведен краткий, но наглядный пример применения ссылки на метод, чтобы определить наибольший элемент в коллекции.
Обнаружить в коллекции наибольший элемент можно, в частности, вызвав метод max(),определенный в классе Collections. При вызове варианта метода max() , применяемого в рассматриваемом здесь примере, нужно передать ссылку на коллекцию и экземпляр объекта, реализующего интерфейс Comparator. В этом интерфейсе определяется порядок сравнения двух объектов. В нем объявляется единственный абстрактный метод compare(), принимающий два аргумента, имеющих типы сравниваемых объектов. Этот метод должен возвратить числовое значение больше нуля, если первый аргумент больше второго; нулевое значение, если оба аргумента равны; и числовое значение меньше нуля, если первый объект меньше второго.
Прежде для вызова метода max() с двумя определяемыми пользователем объектами экземпляр интерфейса Comparator приходилось получать, реализовав сначала этот интерфейс явным образом в отдельном классе, а затем создав экземпляр данного класса. Далее этот экземпляр передавался в качестве компаратора методу max(). Но, начиная с версии JDK 8, появилась возможность просто передать методу max() ссылку на сравнение, поскольку в этом случае компаратор реализуется автоматически. Этот процесс демонстрируется ниже на простом примере создания коллекции типа ArrayList объектов типа MyClass и поиска в ней наибольшего значения, определяемого в методе сравнения.
package ms.learning.lambda; import java.util.*; // Использовать ссылку на метод, чтобы найти // максимальное значение в коллекции class MyClass2 { private int val; MyClass2(int v) { val = v; } int getVal() { return val; } } public class UseMethodRef { // Метод compare(), совместимый с аналогичным методом, // определенным в интерфейсе Comparator static int compareMC(MyClass2 a, MyClass2 b) { return a.getVal() - b.getVal(); } // static int compareMC(T a, T b) { //// return a * b; // return a.getVal() - b.getVal(); // } public static void main(String[] args) { ArrayList al = new ArrayList(); al.add(new MyClass2(1)); al.add(new MyClass2(4)); al.add(new MyClass2(2)); al.add(new MyClass2(9)); al.add(new MyClass2(3)); al.add(new MyClass2(7)); // найти максимальное значение, используя // метод compareMC() MyClass2 maxValObj = Collections.max(al, UseMethodRef::compareMC); // MyClass2 maxValObj = Collections.max(al, UseMethodRef::compareMC); System.out.println("Maкcимaльнoe значение равно: " + maxValObj.getVal()); } }Обратите в данном примере программы внимание на то, что в самом классе MyClass не определяется метод сравнения и не реализуется интерфейс Comparator. Тем не менее максимальное значение в списке объектов типа MyClass может быть получено в результате вызова метода max(), поскольку в классе UseMethodRef определяется статический метод compareMC(), совместимый с методом compare(),определенным в интерфейсе Comparator. Таким образом, отпадает необходимость явным образом реализовывать и создавать экземпляр интерфейса Comparator.
Ссылки на конструкторы
Ссылки на конструкторы можно создавать таким же образом, как и ссылки на методы. Ниже приведена общая форма синтаксиса, которую можно употреблять для создания ссылок на конструкторы.
имя_класса: : new