Ссылка на метод

Ссылка на метод


Вместе с лямбда-выражениями, рассмотренными в рамках предыдущего урока, 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, тип остальных параметров не имеет значения:

s -> Integer.parseInt(s)          равносильно             Integer::parseInt

(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 умеет с ним работать.

Полагаю, некоторые из вас уже догадались, что может возникнуть двусмысленность в случае, если класс содержит статический и нестатический методы с одинаковым названием и соответствующим набором параметров – скажем:

public class SthClass {
  public static void doSth(SthClass sthClass, String sthParam) {
    //do sth
  }

  public void doSth(String sthParam) {
    //do sth
  }

В таком случае, использование method reference будет недоступно – компилятор не сможет определить, какой из методов вы имели ввиду, ведь оба лямбда-выражения

(sthClass, s) -> SthClass.doSth(sthClass, s)

и

(sthClass, s) -> sthClass.doSth(s)

В виде method reference будут выглядеть как:

SthClass::doSth

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

Впрочем, если ваши классы имеют статические и нестатические методы, да еще и выполняющие все описанные выше условия (одинаковое название, схожий набор параметров), боюсь, невозможность использования method reference – последнее, что должно вас заботить.

Касательно применения абстрактных классов и методов в качестве ClassName для данной реализации: вы можете использовать и то, и другое, если соответствующий абстрактный класс/интерфейс имеет хотя бы объявление нужного метода. Но реализация всегда будет использована в соответствии с реальным типом объекта, у которого вызывается метод. Впрочем, та же логика действует и за пределами method reference.


varName::methodName

Сначала разберемся, что может скрываться под varName.

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

Рассмотрим ряд примеров.

Пример 1

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

Отдельно рассмотрим ситуацию, когда необходима ссылка на не статический метод в рамках того же класса:

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

По сути, мы сделали ссылку на метод ptintln() у статического поля класса Systemout. Это работало бы, будь поле и не статическим. Другой вопрос, что не статическое публичное поле – большая редкость.

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

sthField1.sthField2.sthField3.sthField4::doSth

 

Теперь о том, что будет работать, но чего делать не стоит:

Пример 4

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<String> list = List.of(sthVar);
    SthFunctionalInterface f = (s1, s2) -> 
                     list.get(0).formatted(s1, s2);
    //some logic
  }
}

Здесь

(s1, s2) -> getStr().formatted(s1, s2) равносильно getStr()::formatted

(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 (пример ниже):

String str = "sthString";
SthFunctionalInterface  f = (s1, s2) -> s1.formatted(str, s2);


В целом, это вполне нормально, если ваше лямбда-выражение не сворачивается до method reference. Причин, почему это так – множество и далеко не все из них свидетельствуют о низком качестве кода. Но, все же, рекомендую завести привычку использовать данный механизм там, где это возможно. Такой подход улучшает читабельность кода.

 

Полезные ссылки

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

Качество подачи в видеоматериале, на мой взгляд, не очень, но кому-то, возможно, зайдет. В любом случае, обратите внимание на тезисы и примеры под видео. Также не поленитесь пройти коротенький тест, расположенный ниже примеров применения method referece:

https://www.examclouds.com/ru/java/java-core-russian/method-references-russian

 

Более расширенные и подробные примеры использования method reference. Примеры сложнее базовых, поскольку пытаются решить практические задачи, хоть и учебные, зато дают немного больше контекста для области применения лямбда-выражений, за пределами Stream API и других, пока нам не знакомых, но классических областей применения лямбда-выражений:

https://www.bestprog.net/ru/2020/12/22/java-types-of-method-references-reference-to-methods-ru/

 

Вместо итога

Я не могу назвать эту тему обязательной, или безусловно необходимой, особенно для junior-специалистов. Писать код, даже использовать лямбда-выражения, можно не используя конструкцию method reference или используя вслепую – когда IDEA предложит.

Тем не менее, это удобный механизм, который может сделать ваш код более понятным и лаконичным. К тому же он избавит вас от придумывания имен параметров в лямбда-выражении – это иногда бывает нетривиальной задачей:)

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

 

С теорией на сегодня все!


Переходим к практике:

Задача 1

Создайте произвольный список элементов. Выведите каждый из элементов в консоль. Параметр forEach() опишите как method reference.

 

Задача 2

Реализуйте Задачу 1, обернув метод выведения записи в консоль (System.out.println()) в собственный статический метод.

 

Задача 3

Реализуйте Задачу 3 из урока 46, описав все реализуемые фильтры через method reference’ы. Рекомендую вынести функциональность формирования фильтров в отдельный сервис, если это не было сделано ранее.


Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)

Канал: https://t.me/+relA0-qlUYAxZjI6

Мой тг: https://t.me/ironicMotherfucker

 

Дорогу осилит идущий!

Report Page