diff --git a/lessons/java-core/047/Method reference.md b/lessons/java-core/047/Method reference.md new file mode 100644 index 0000000..f810bb3 --- /dev/null +++ b/lessons/java-core/047/Method reference.md @@ -0,0 +1,354 @@ +# Ссылка на метод + +Вместе с лямбда-выражениями, рассмотренными в рамках предыдущего урока, Java 8 привнесла еще один механизм, позволяющий +писать код лаконично – **method reference (ссылка на метод)**. Отмечу, что почти не встречал в живой коммуникации +русского варианта этого термина. Видимо, не прижился. По крайней мере, в моем окружении. + +Мы совсем поверхностно коснулись существования такой языковой конструкции в прошлом уроке, более детально постараемся +разобраться в рамках текущего. + +## Что такое ссылка на метод? + +**Method reference** – языковая конструкция в Java, позволяющая упрощать запись лямбда-выражений. Таким образом, это +следующий уровень оптимизации кодовой базы при работе с функциональными интерфейсами: + +``` + анонимный класс → лямбда-выражение → method reference +``` + +Заранее отмечу, что реализацию любого функционального интерфейса можно описать в виде лямбда выражения, но далеко не +каждое лямбда-выражение можно представить в виде method reference’а. Ниже мы разберем, при каких условиях подобная +трансформация возможна, а при каких – нет + +> !NB: как и с переходом от анонимного класса к лямбда-выражению, обычно IDEA подсвечивает лямбды, которые можно +> свернуть до method reference. И даже предложит сделать это за вас: +> → курсор в область предупреждения (часть кода, выделенного желтым); +> → alt+Enter для появления выпадающего списка; +> → как правило, первый пункт – _«Replace lambda with method reference»_; +> → Enter. +> Но иногда приведение к ссылке на метод возможно, но требует небольших изменений в коде: изменения порядка параметров +> в методе или что-то еще. В таких случаях IDEA помочь не в силах. + +## Синтаксические особенности + +В общем случае, все формы записи method reference можно свести к нескольким группам: + +1. `ClassName::methodName;` где `ClassName` – имя класса (`String`, `Integer`…), `methodName` – имя вызываемого метода; +2. `varName::methodName;` `varName` – обращение к переменной или полю класса, `methodName` – имя вызываемого метода; +3. `ClassName::new`; `ClassName` – имя класса, `new` – оператор выделения памяти, тот же, что и перед вызовом + конструктора. + +Как видите, все три описанные группы используют новый для нас оператор `«::»` – два двоеточия. Это **method reference +operator – оператор ссылки на метод**. Так же, как `«->»` – оператор, указывающий на лямбда-выражение, `«::»` – +оператор, указывающий на использование method reference. + +Разберем группы, описанные выше, подробнее. + +### ClassName::methodName + +Первое, на что стоит обратить внимание, `ClassName`. Под такой записью может скрываться как обращение к обычному +классу (`String`, `Object`, `SthYourPublicClass`), так и обращение к вложенному. Например, `AbstractMap.SimpleEntry`. +Также может быть указано название абстрактного класса или интерфейса. + +Второй важный нюанс заключается в том, что под данной формой записи может скрываться две реализации. + +Первая из них – вызов статического метода `ClassName`. В таком случае, все параметры лямбда-выражения (если они есть) +будут переданы как параметры вызываемого статического метода. Чтобы использовать подобную форму записи, необходимо, +чтобы метод принимал параметрами те же аргументы, что и ваше лямбда выражение. Порядок следования параметров тоже должен +совпадать. Впрочем, эти правила, с небольшой поправкой, применимы для любой формы method reference. + +_Примеры_. Если `s` является переменной типа `String`, тип остальных параметров не имеет значения: + +1. `s -> Integer.parseInt(s)` равносильно `Integer::parseInt` +2. `(s, o1, o2) -> String.format(s, 01, 02)` равносильно `String::format` + +В данной реализации использование абстрактного класса или интерфейса в качестве `ClassName` не имеет каких-либо +особенностей. + +Вторая реализация говорит о том, что метод вызывается у параметра лямбда-выражения (у первого параметра, если их +несколько). Соответственно, вызываемый метод не является статическим. + +> !NB: в целом, мы можем вызывать статические методы через обращение к объекту класса, хоть это и не рекомендуется: +> `"sthString".format()` равноценно вызову `String.format()`. Но не в method reference. Здесь попытка реализовать +> подобное поведение приведет к ошибке компиляции. + +Такая реализация недоступна для ситуаций, когда лямбда-выражение не принимает параметра на вход (как у функционального +интерфейса `Supplier`, например). + +Рассмотрим примеры. + +Пример 1. Допустим, `d` – переменная типа `Double`: +`d -> d.intValue()` равносильно `Double::intValue` + +Пример 2. Допустим, `s1` и `s2` – переменные типа `String`. Тогда: +`(s1, s2) -> s1.concat(s2)` равносильно `String::concat` + +Пример 3. Допустим, `s` – переменная типа `String`, `o1`, `o2`, `o3` – переменные типа `Object` (или любого другого, +здесь это не будет иметь значения): +`(s, o1, o2, o3) -> s.formatted(o1, o2, o3)` равносильно `String::formatted` + +Последний пример интересен тем, что `formatted()` принимает varargs. Как видите, method reference умеет с ним работать. + +Полагаю, некоторые из вас уже догадались, что может возникнуть двусмысленность в случае, если класс содержит статический +и нестатический методы с одинаковым названием и соответствующим набором параметров – скажем: + +```java + public class SthClass { + public static void doSth(SthClass sthClass, String sthParam) { + //do sth + } + + public void doSth(String sthParam) { + //do sth + } + } +``` + +В таком случае, использование method reference будет недоступно – компилятор не сможет определить, какой из методов вы +имели ввиду, ведь оба лямбда-выражения + +```java + (sthClass, s) -> SthClass.doSth(sthClass, s) +``` + +и + +```java + (sthClass, s) -> sthClass.doSth(s) +``` + +В виде method reference будут выглядеть как: + +```java + SthClass::doSth +``` + +Поэтому при попытке использовать для подобных случаев ссылку на метод произойдет ошибка компиляции. + +> Впрочем, если ваши классы имеют статические и нестатические методы, да еще и выполняющие все описанные выше условия +> (одинаковое название, схожий набор параметров), боюсь, невозможность использования method reference – +> последнее, что должно вас заботить. + +Касательно применения абстрактных классов и методов в качестве `ClassName` для данной реализации: вы можете использовать +и то, и другое, если соответствующий абстрактный класс/интерфейс имеет хотя бы объявление нужного метода. Но реализация +всегда будет использована в соответствии с реальным типом объекта, у которого вызывается метод. Впрочем, та же логика +действует и за пределами method reference. + +### varName::methodName + +Сначала разберемся, что может скрываться под `varName`. + +В общем-то, там может быть что угодно, что приведет к получению ссылки на объект: переменная, параметр метода (в котором +описывается данное лямбда-выражение), поле класса, `this`, статическое поле другого класса, даже метод (или цепочка +методов) результатом которого будет объект. Последнее является извращением, но будет синтаксически верно. + +Рассмотрим ряд примеров. + +Пример 1: + +```java + public class SthClass { + private String sthField; + + public void doSthUsingField() { //1 + //some logic + SthFunctionalInterface f = (s1, s2) -> str.formatted(s1, s2); + //some logic + } + + public void doSthWithoutField() { //2 + //some logic + String strVar = "%s: %s"; + SthFunctionalInterface f = (s1, s2) -> strVar.formatted(s1, s2); + //some logic + } + + public void doSthUsingParam(String strParam) { //3 + //some logic + SthFunctionalInterface f = (s1, s2) -> strParam.formatted(s1, s2); + //some logic + } + } +``` + +Здесь: + +1. `(s1, s2) -> str.formatted(s1, s2)` равносильно `str::formatted` +2. `(s1, s2) -> strVar.formatted(s1, s2)` равносильно `strVar::formatted` +3. `(s1, s2) -> strParam.formatted(s1, s2)` равносильно `strParam::formatted` + +В данных случаях все легко и непринужденно. + +Пример 2: + +Отдельно рассмотрим ситуацию, когда необходима ссылка на не статический метод в рамках того же класса: + +```java + public class SthClass { + public void doSth() { + //some logic + SthFunctionalInterface f = (s1, s2) -> doSthInternal(s1, s2); + //some logic + } + + private void doSthInternal(String sthString1, String sthString2) { + //do sth + } + } +``` + +В данном случае лямбда-выражение `(s1, s2) -> doSthInternal(s1, s2)` +равносильно лямбда-выражению `(s1, s2) -> this.doSthInternal(s1, s2)` +что, в свою очередь будет равносильно `this::doSthInternal` + +Данный пример ненамного сложнее предыдущих, но с использованием `this` в method reference у новичков часто возникают +вопросы. + +Перейдем к следующему примеру. + +Пример 3: + +`s -> System.out.println(s)` равносильно `System.out::println` + +По сути, мы сделали ссылку на метод `println()` у статического поля класса `System` – `out`. Это работало бы, +будь поле и не статическим. Другой вопрос, что не статическое публичное поле – большая редкость. + +Это вполне допустимая форма записи. Только не делайте цепочку обращения к полям длинной, +это будет выглядеть, как минимум, странно: + +``` + sthField1.sthField2.sthField3.sthField4::doSth +``` + +Теперь о том, что будет работать, но чего делать не стоит. + +Пример 4: + +```java + public class SthClass { + public void doSthUsingAnotherMethod() { + //some logic + SthFunctionalInterface f = (s1, s2) -> getStr().formatted(s1, s2); + //some logic + } + + private String getStr() { + //some method returning some String-object + } + + public void doSthUsingVar() { + //some logic + String strVar = "%s: %s"; + List list = List.of(sthVar); + SthFunctionalInterface f = (s1, s2) -> + list.get(0).formatted(s1, s2); + //some logic + } + } +``` + +Здесь: + +1. `(s1, s2) -> getStr().formatted(s1, s2)` равносильно `getStr()::formatted` +2. `(s1, s2) -> list.get(0).formatted(s1, s2)` равносильно `list.get(0)::formatted` + +Такие формы записи будут работать. Но являются примерами очень плохого кода. Справедливости ради, в виде +лямбда-выражения подобное тоже редко бывает допустимым. + +### ClassName::new + +В целом, наиболее интуитивно понятный вид method reference. + +> Да, несмотря на то что это является ссылкой на конструктор, де-факто никто не стал плодить лишние сущности и такая +> запись является лишь частным случаем method reference. + +По сути, данная форма ничем не отличается от ситуации, когда мы ссылаемся на статический метод класса. Все параметры +лямбда-выражения будут переданы как параметры конструктора. + +Пример 1. `cap` – переменная типа `int`, `loadFactor` – переменная типа `float`: + +`(capacity, loadFactor) -> new HashMap(capacity, loadFactor)` равносильно `HashMap::new` + +Пример 2. С вложенными классами тоже работает. Тип `k` и `v` – любой: + +`(k, v) -> new AbstractMap.SimpleEntry(k, v)` равносильно `AbstractMap.SimpleEntry::new` + +Для данного типа method reference недопустимо использование абстрактных классов или интерфейсов в качестве `ClassName` – +ведь объект абстрактного класса или интерфейса создать невозможно. + +На этом разбор синтаксических особенностей method reference можно считать завершенным. + +## Условия использования method reference + +Как уже упоминалось выше, не любое лямбда-выражение можно описать с помощью method reference. Постараемся обобщить, +какие условия должны выполняться, чтобы такая форма записи стала возможной: + +1. Лямбда-выражение должно быть описано в одну строку и не иметь ветвления. Проще говоря, вашу лямбду не удастся + превратить в method reference, если она содержит фигурные скобки, тернарный оператор или switch-case; +2. Все параметры лямбда-выражения должны использоваться строго единожды и в порядке, в котором они передаются в лямбду; +3. В качестве параметра метода не может быть передано что-либо, кроме параметров лямбда-выражения. Проще говоря, если вы + хотите использовать в качестве параметра вызываемого в лямбде метода поле или переменную – что-либо, кроме параметров + самой лямбды – такую лямбду не удастся превратить в method reference (пример ниже): + ```java + String str = "sthString"; + SthFunctionalInterface f = (s1, s2) -> s1.formatted(str, s2); + ``` + +В целом, это вполне нормально, если ваше лямбда-выражение не сворачивается до method reference. Причин, почему это так – +множество и далеко не все из них свидетельствуют о низком качестве кода. Но, все же, рекомендую завести привычку +использовать данный механизм там, где это возможно. Такой подход улучшает читабельность кода. + +## Полезные ссылки + +Вне зависимости от того, насколько понятной была для вас эта статья, рекомендую не проходить мимо следующих двух ссылок. + +1. [Первая](https://www.examclouds.com/ru/java/java-core-russian/method-references-russian). Качество подачи в + видеоматериале, на мой взгляд, не очень, но кому-то, возможно, зайдет. В любом случае, обратите внимание на тезисы и + примеры под видео. Также не поленитесь пройти коротенький тест, расположенный ниже примеров применения + method reference +2. [Вторая](https://www.bestprog.net/ru/2020/12/22/java-types-of-method-references-reference-to-methods-ru/). Более + расширенные и подробные примеры использования method reference. Примеры сложнее базовых, поскольку пытаются решить + практические задачи, хоть и учебные, зато дают немного больше контекста для области применения лямбда-выражений, за + пределами Stream API и других, пока нам не знакомых, но классических областей применения лямбда-выражений + +## Вместо итога + +Я не могу назвать эту тему обязательной, или безусловно необходимой, особенно для junior-специалистов. Писать код, даже +использовать лямбда-выражения, можно не используя конструкцию method reference или используя вслепую – когда IDEA +предложит. + +Тем не менее, это удобный механизм, который может сделать ваш код более понятным и лаконичным. К тому же он избавит вас +от придумывания имен параметров в лямбда-выражении – это иногда бывает нетривиальной задачей:) + +В любом случае, я считаю важным владение базовым инструментарием Java и умение его эффективно использовать. Тем более, +что в данном случае изучение инструмента займет у вас не больше пары часов. Очень маленькая цена за улучшение качества +кода. + +#### С теорией на сегодня все! + +![img.png](../../../commonmedia/defaultFooter.jpg) + +Переходим к практике: + +## Задача 1: + +Создайте произвольный список элементов. Выведите каждый из элементов в консоль. Параметр `forEach()` опишите как +method reference. + +## Задача 2: + +Реализуйте Задачу 1, обернув метод выведения записи в консоль `System.out.println()` в собственный статический метод. + +## Задача 3: + +Реализуйте Задачу 3 из [урока 46](https://telegra.ph/Funkcionalnye-interfejsy-i-lyambda-vyrazheniya-02-10), +описав все реализуемые фильтры через method reference’ы. Рекомендую вынести функциональность формирования фильтров в +отдельный сервис, если это не было сделано ранее. + +> Если что-то непонятно или не получается – welcome в комменты к посту или в лс:) +> +> Канал: https://t.me/ViamSupervadetVadens +> +> Мой тг: https://t.me/ironicMotherfucker +> +> **Дорогу осилит идущий!** \ No newline at end of file