Lambda ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-08

Ссылки на методы (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;

Ссылки на методы бывают четырёх видов:

Типы ссылок на методы в Java
Тип ссылки Синтаксис Пример Эквивалентная лямбда
Ссылка на статический метод 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, поэтому производительность практически одинакова. Ссылка на метод иногда позволяет избежать создания дополнительного синтетического метода в классе, но на реальных приложениях это не даёт заметного выигрыша. Выбирайте по читаемости, а не по скорости.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

milkina Jul 5, 2024
ccskrb yf vtnjl какие бывают java

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.