Ссылки на методы (method reference) в Java
1. Что такое ссылки на методы
Этот код падает с NullPointerException на второй строке — ещё до того, как кто-либо вызовет supplier.get():
String s = null;
Supplier<Integer> supplier = s::length; // NullPointerException прямо здесь
System.out.println(supplier.get()); // до этой строки дело не дойдёт Замените ссылку на метод эквивалентной лямбдой () -> s.length() — и исключение переедет на третью строку. Разница в том, когда вычисляется объект слева от ::. С этой детали и начинаются почти все вопросы по теме на собеседованиях.
Ссылка на метод (method reference) — это сокращённая запись лямбда-выражения, которое не делает ничего, кроме вызова одного уже существующего метода. Синтаксис использует оператор :: (двойное двоеточие): слева указывается класс или объект, справа — имя метода без скобок и без аргументов. Ссылки на методы появились в Java 8 вместе с лямбда-выражениями и, как и они, могут присваиваться только функциональному интерфейсу.
Правило простое: если лямбда-выражение вызывает ровно один существующий метод и просто пробрасывает в него свои параметры, лучше сослаться на этот метод по имени. Например:
Consumer<String> consumer = str -> System.out.println(str); можно переписать с помощью method reference:
Consumer<String> consumer = System.out::println; Ссылки на методы бывают четырёх видов:
| Тип ссылки | Синтаксис | Пример | Эквивалентная лямбда |
|---|---|---|---|
| Ссылка на статический метод | ContainingClass::staticMethodName | Integer::parseInt | s -> Integer.parseInt(s) |
| Ссылка на нестатический метод конкретного объекта (bound receiver) | containingObject::instanceMethodName | System.out::println | x -> System.out.println(x) |
| Ссылка на нестатический метод любого объекта конкретного типа (unbound receiver) | ContainingType::methodName | String::toLowerCase | s -> s.toLowerCase() |
| Ссылка на конструктор (constructor reference) | ClassName::new | ArrayList::new | () -> new ArrayList<>() |
Подсвеченная строка — тот самый вид, который чаще всего сбивает с толку: метод нестатический, а слева от :: стоит имя класса. Разбор в разделе 4.
2. Ссылка на статический метод (ContainingClass::staticMethodName)
Самый прямолинейный вид: слева от :: — имя класса, справа — имя статического метода. Все параметры лямбда-выражения один в один становятся аргументами метода.
Синтаксис:
ContainingClass::staticMethodName Например:
Function<String, Boolean> function = e -> Boolean.valueOf(e);
System.out.println(function.apply("TRUE")); // true Перепишем с помощью ссылки на статический метод:
Function<String, Boolean> function = Boolean::valueOf;
System.out.println(function.apply("TRUE")); // true Количество параметров значения не имеет — лишь бы сигнатура метода подходила функциональному интерфейсу:
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
UnaryOperator<Integer> abs = Math::abs; // x -> Math.abs(x)
BiFunction<Integer, Integer, Integer> max = Math::max; // (a, b) -> Math.max(a, b)
System.out.println(parse.apply("42")); // 42
System.out.println(abs.apply(-7)); // 7
System.out.println(max.apply(3, 9)); // 9 Типичное применение — в Stream API:
List<String> numbers = List.of("10", "20", "30");
int sum = numbers.stream()
.mapToInt(Integer::parseInt)
.sum();
System.out.println(sum); // 60 3. Ссылка на нестатический метод конкретного объекта
Синтаксис:
containingObject::instanceMethodName Этот тип (в англоязычной литературе — bound receiver, «связанный получатель») используется, когда лямбда-выражение вызывает метод внешнего уже существующего объекта. Объект-получатель зафиксирован в самой ссылке, а параметры функционального интерфейса становятся аргументами метода.
Например:
Consumer<String> consumer = e -> System.out.println(e);
consumer.accept("OCPJP 8"); Перепишем, используя ссылку:
Consumer<String> consumer = System.out::println;
consumer.accept("OCPJP 8"); Ещё один пример:
Integer integer = 5;
Supplier<String> supplier = () -> integer.toString();
System.out.println(supplier.get()); // 5 Перепишем:
Integer integer = 5;
Supplier<String> supplier = integer::toString;
System.out.println(supplier.get()); // 5 Получателем может быть и текущий объект — тогда используются this:: и super:::
class Printer {
void print(String text) {
System.out.println("> " + text);
}
Consumer<String> asConsumer() {
return this::print; // ссылка на метод текущего объекта
}
} Важно
Выражение слева от :: вычисляется один раз — в момент создания ссылки, а не при каждом вызове. Поэтому s::length при s == null бросает NullPointerException сразу, а System.out::println навсегда запоминает тот поток вывода, который был установлен на момент создания ссылки: последующий System.setOut(...) на неё уже не повлияет. У лямбда-выражения x -> System.out.println(x) поведение другое — поле читается при каждом вызове.
4. Ссылка на нестатический метод любого объекта конкретного типа
Синтаксис:
ContainingType::methodName Здесь слева стоит имя класса, хотя метод нестатический (unbound receiver, «несвязанный получатель»). Работает это так: первый параметр функционального интерфейса становится объектом, на котором вызывается метод, а остальные параметры — его аргументами.
Например:
Function<String, String> function = s -> s.toLowerCase();
System.out.println(function.apply("OCPJP 8")); // ocpjp 8 Перепишем:
Function<String, String> function = String::toLowerCase;
System.out.println(function.apply("OCPJP 8")); // ocpjp 8 Если у метода есть собственные аргументы, они «сдвигаются» на следующие параметры интерфейса:
BiFunction<String, String, Boolean> equals = String::equalsIgnoreCase;
// эквивалентно: (a, b) -> a.equalsIgnoreCase(b)
System.out.println(equals.apply("Java", "JAVA")); // true
Comparator<String> comparator = String::compareToIgnoreCase;
// эквивалентно: (a, b) -> a.compareToIgnoreCase(b) На практике этот вид ссылок встречается чаще всего — именно он делает код стримов читаемым:
List<String> names = List.of("bob", "alice", "eve");
names.stream()
.map(String::toUpperCase) // s -> s.toUpperCase()
.sorted()
.forEach(System.out::println);
List<Person> people = getPeople();
people.sort(Comparator.comparing(Person::getName)); // p -> p.getName() Как отличить вид 3 от вида 4
Смотрите, что стоит слева от ::. Переменная или выражение (system.out, integer, this) — получатель зафиксирован, все параметры интерфейса уходят в аргументы метода. Имя класса (String, Person) — получателем станет первый параметр интерфейса. Отсюда и разное количество параметров: Function<String, String> подходит для String::toLowerCase, но не для str::toLowerCase.
5. Ссылка на конструктор
Синтаксис:
ClassName::new ClassName не может быть абстрактным классом или интерфейсом — объект такого типа создать нельзя. Компилятор сам подбирает конструктор, подходящий по числу и типам параметров функционального интерфейса.
Например:
Supplier<List<String>> listSupplier = () -> new ArrayList<>();
List<String> list = listSupplier.get(); Перепишем:
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get(); С конструктором, принимающим аргументы:
class Person {
private final String name;
Person(String name) {
this.name = name;
}
String getName() {
return name;
}
}
Function<String, Person> creator = Person::new; // name -> new Person(name)
Person person = creator.apply("Alice");
System.out.println(person.getName()); // Alice Отдельная форма ссылки на конструктор — создание массива: Type[]::new. Она нужна, например, в Stream.toArray():
IntFunction<String[]> arrayCreator = String[]::new; // size -> new String[size]
String[] empty = arrayCreator.apply(3);
String[] result = Stream.of("a", "b", "c")
.toArray(String[]::new);
System.out.println(result.length); // 3 Ссылки на конструкторы часто используются как «фабрики» в коллекторах:
Set<String> set = Stream.of("a", "b", "a")
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(set); // [a, b] Не используйте Integer::new и подобное
В старых примерах часто встречается Function<String, Integer> f = Integer::new;. Конструкторы классов-обёрток объявлены устаревшими начиная с Java 9, а с Java 16 помечены как deprecated for removal: они всегда создают новый объект в обход кэша. Используйте фабричные методы — Integer::valueOf или Integer::parseInt (последний возвращает примитив int).
6. Ссылка на метод или лямбда-выражение: что выбрать
Ссылка на метод не быстрее и не «мощнее» лямбда-выражения — это синтаксический сахар. На уровне байт-кода оба варианта компилируются в invokedynamic с использованием LambdaMetafactory. Разница только в читаемости и в паре нюансов поведения.
| Критерий | Лямбда-выражение | Ссылка на метод |
|---|---|---|
| Тело | Любой код: несколько вызовов, условия, арифметика | Только один вызов существующего метода или конструктора |
| Параметры | Указываются явно | Не указываются вообще — выводятся из интерфейса |
| Изменение порядка аргументов | Возможно: (a, b) -> f(b, a) | Невозможно |
| Частичное применение (константа в аргументе) | Возможно: s -> s.substring(1) | Невозможно |
| Момент вычисления получателя | При каждом вызове | Один раз, при создании ссылки |
| Читаемость | Хуже при простом пробросе параметров | Лучше: String::trim вместо s -> s.trim() |
Практическое правило: пишите ссылку на метод, если лямбда-выражение выглядит как x -> что-то(x) или x -> x.что-то(). Как только внутри появляется хоть что-то ещё — оставайтесь на лямбде.
7. На чём чаще всего спотыкаются
Получатель вычисляется заранее
Уже разобрано выше, но это ошибка номер один. obj::method вычисляет obj немедленно — и падает с NullPointerException в строке объявления, если объект равен null.
Неоднозначная ссылка: reference to ... is ambiguous
Если в классе есть и статический, и нестатический метод с одинаковым именем, и оба подходят по сигнатуре, код не скомпилируется:
class Foo {
static String bar(Foo f) { return "static"; }
String bar() { return "instance"; }
}
Function<Foo, String> f = Foo::bar; // ошибка компиляции: reference to bar is ambiguous Компилятор не может выбрать между «статический метод с аргументом Foo» и «нестатический метод, вызванный на объекте Foo». Лечится переходом на лямбду с явным вызовом.
Нельзя подставить свой аргумент
Метод-ссылка передаёт параметры интерфейса как есть. Всё, где нужна константа или изменённый порядок, ссылкой не записывается:
Function<String, String> ok = String::trim; // корректно
Function<String, String> fail = String::substring; // не скомпилируется: не хватает аргумента
Function<String, String> okLambda = s -> s.substring(1); // так и оставляем Перегруженные методы разрешаются по целевому типу
String::valueOf сам по себе неоднозначен — перегрузок больше десятка. Компилятор выбирает нужную по типу переменной, которой присваивается ссылка:
Function<Integer, String> fromInt = String::valueOf; // valueOf(int)
Function<char[], String> fromChars = String::valueOf; // valueOf(char[]) Если целевой тип не позволяет выбрать единственный вариант, будет ошибка компиляции — помогает явное приведение или лямбда.
Ссылка на метод не «замораживает» значение переменной сама по себе
Как и лямбда-выражение, ссылка на метод может захватывать только effectively final переменные. Попытка изменить локальную переменную после создания ссылки на её метод не скомпилируется.
Часто задаваемые вопросы
Почему String::toLowerCase работает как s -> s.toLowerCase(), ведь метод не статический?
Это ссылка на нестатический метод произвольного объекта типа (unbound receiver). Компилятор берёт первый параметр функционального интерфейса и использует его как объект, на котором вызывается метод, а остальные параметры передаёт как аргументы. Поэтому Function<String, String> с одним параметром подходит для метода без аргументов, а BiFunction<String, String, Boolean> — для String::equalsIgnoreCase, у которого один аргумент.
Можно ли ссылаться на перегруженный метод?
Да, если по целевому функциональному интерфейсу компилятор однозначно определяет нужную перегрузку. Function<Integer, String> f = String::valueOf; выберет valueOf(int), а Function<char[], String> — valueOf(char[]). Если подходят сразу несколько вариантов, компиляция завершится ошибкой «reference is ambiguous»; в этом случае используйте лямбда-выражение с явным вызовом.
Можно ли написать this::method или super::method?
Да. Оба варианта допустимы внутри нестатического контекста и относятся к ссылкам на метод конкретного объекта. this::print ссылается на метод текущего объекта, super::print — на реализацию из суперкласса, что удобно при переопределении метода. В статическом методе this:: использовать нельзя.
Как создать массив через ссылку на конструктор?
Используйте форму Type[]::new, которая эквивалентна лямбде size -> new Type[size]. Чаще всего она встречается в методе Stream.toArray: stream.toArray(String[]::new) вернёт массив String[], тогда как toArray() без аргумента вернёт Object[]. Это частная форма ссылки на конструктор, а не отдельный пятый вид ссылок.
Ссылки на методы быстрее лямбда-выражений?
Нет. Оба варианта компилируются в инструкцию invokedynamic и связываются через LambdaMetafactory, поэтому производительность практически одинакова. Ссылка на метод иногда позволяет избежать создания дополнительного синтетического метода в классе, но на реальных приложениях это не даёт заметного выигрыша. Выбирайте по читаемости, а не по скорости.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии