W3docs

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

Используйте методы как лямбды через оператор :: в Java: статические, экземплярные, связанные и ссылки на конструкторы.

Ссылка на метод — это сокращённый синтаксис для лямбда-выражения, тело которого делает только одно: вызывает существующий метод. Если лямбда выглядит ровно как x -> SomeClass.foo(x) или (a, b) -> a.bar(b), оператор :: позволяет записать её как SomeClass::foo или Some::bar. Компилятор в обоих случаях создаёт одно и то же значение — экземпляр подходящего функционального интерфейса, — поэтому везде, где подходит лямбда, подойдёт и соответствующая ссылка на метод.

Function<String, Integer> len1 = s -> s.length();
Function<String, Integer> len2 = String::length;            // identical at runtime

List<String> names = List.of("Bob", "Alice");
names.forEach(s -> System.out.println(s));                   // lambda
names.forEach(System.out::println);                          // method reference

Четыре формы, описанные ниже, охватывают все ссылки на методы, с которыми вам придётся работать. Главное — научиться распознавать, какая форма подходит для конкретного вызова.

Форма 1: ссылка на статический метод — ClassName::staticMethod

Метод является static-методом класса. Ссылка превращается в лямбду, параметры которой соответствуют параметрам статического метода:

Function<String, Integer> parse  = Integer::parseInt;        // s -> Integer.parseInt(s)
BinaryOperator<Integer>   max    = Math::max;                 // (a, b) -> Math.max(a, b)
Function<Object, String>  toStr  = String::valueOf;            // o -> String.valueOf(o)

Эта форма часто встречается в коде со стримами, например nums.stream().reduce(0, Integer::sum)Integer.sum(int, int) является статическим, поэтому Integer::sum — это BinaryOperator<Integer> (он же BiFunction<Integer, Integer, Integer>).

Форма 2: связанная ссылка на метод экземпляра — instance::method

Метод является методом экземпляра конкретного, именованного объекта. Ссылка превращается в лямбду, параметры которой соответствуют параметрам метода (экземпляр захватывается):

PrintStream out = System.out;
Consumer<String> print = out::println;                  // s -> out.println(s)

String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;        // name -> prefix.concat(name)

List<String> log = new ArrayList<>();
Consumer<String> record = log::add;                     // msg -> log.add(msg)

Связанный получатель заполняет слот без аргументов: поскольку prefix уже захвачен, greet требует только аргумент для concat, поэтому это Function<String, String>, а не BiFunction. Аналогично log::add фиксирует log и открывает лишь добавляемый элемент, давая Consumer<String>.

Захваченный instance хранится в результирующем объекте — аналогично тому, как лямбда захватывает effectively final-переменные. Связанные ссылки — это способ сказать «используй метод этого конкретного объекта как колбэк»: Logger::info для конкретного логгера, event::handle для конкретного обработчика.

Форма 3: несвязанная ссылка на метод экземпляра — ClassName::method

Метод является методом экземпляра, но вы ссылаетесь на него через класс, а не через конкретный объект. Ссылка превращается в лямбду, первым параметром которой является получатель, а остальные — собственные параметры метода:

Function<String, Integer>   len    = String::length;             // s -> s.length()        — first param is the receiver
Function<String, String>    upper  = String::toUpperCase;         // s -> s.toUpperCase()
BiPredicate<String, String> starts = String::startsWith;          // (s, prefix) -> s.startsWith(prefix)

Это форма, которая вызывает затруднение. String::length выглядит так, будто означает «метод length у класса String», — но такого статического метода нет. На самом деле это значит «для любой строки String вызови её метод экземпляра length() — получатель является первым параметром лямбды». Поэтому String::length является Function<String, Integer> (один вход, один выход), а String::startsWithBiPredicate<String, String> (второй вход — это префикс, который проверяется у получателя).

Эта форма лежит в основе практически каждого стримового конвейера:

people.stream()
    .map(Person::name)               // unbound: p -> p.name()
    .filter(s -> s.startsWith("A"))
    .map(String::toUpperCase)        // unbound: s -> s.toUpperCase()
    .forEach(System.out::println);   // bound: s -> out.println(s)

Форма 4: ссылка на конструктор — ClassName::new

Ссылается на конструктор как на функцию. Результирующая лямбда принимает параметры конструктора и возвращает новый экземпляр:

Supplier<List<String>>           listOf  = ArrayList::new;                  // () -> new ArrayList<>()
Function<Integer, ArrayList<?>>  sized   = ArrayList::new;                   // n -> new ArrayList<>(n)
Function<String, BigDecimal>     toBig   = BigDecimal::new;                  // s -> new BigDecimal(s)
BiFunction<String, Integer, AbstractMap.SimpleEntry<String, Integer>> entry =
    AbstractMap.SimpleEntry::new;

Ссылки на конструкторы — именно так Collectors.toCollection(TreeSet::new) позволяет выбрать тип коллекции-назначения, а Stream.generate(Random::new) создаёт независимые объекты Random при каждом вызове get().

Для массивов существует специальная форма: String[]::new — это IntFunction<String[]>, то есть n -> new String[n]. Именно это используется в stream.toArray(String[]::new).

Ссылка на метод или лямбда — когда что применять

Ссылка на метод — правильный выбор, когда тело лямбды представляет собой ровно один вызов метода с параметрами, переданными в том же порядке:

ЛямбдаСсылка на метод
s -> s.length()String::length
s -> System.out.println(s)System.out::println
(a, b) -> a.compareTo(b)String::compareTo
() -> new ArrayList<>()ArrayList::new

Лямбда — правильный выбор, когда тело делает что-либо иное:

  • Вызывает более одного метода: s -> s.trim().toUpperCase() (для цепочки ссылки нет).
  • Преобразует аргументы: s -> System.out.println("[" + s + "]").
  • Содержит управляющий поток: n -> n < 0 ? 0 : n.
  • Меняет порядок или дублирует аргументы: (a, b) -> b.compareTo(a) (обратный компаратор).

Оптимизация здесь — не в скорости выполнения: и то и другое компилируется в один и тот же bootstrap-вызов invokedynamic. Речь идёт о читаемости. Person::name сразу воспринимается как «поле name», тогда как p -> p.name() требует прочитать три токена. Если ссылка подходит — используйте её; если нет — не стоит искусственно подгонять под неё код.

Подводный камень ссылки на конструктор: неоднозначные перегрузки

ClassName::new отлично работает, когда есть ровно один конструктор, подходящий целевому интерфейсу. Если конструкторов несколько, компилятор выбирает по числу и типам параметров целевого типа. В большинстве случаев это работает; иногда нет, и тогда нужно устранить неоднозначность, явно указав тип переменной или вернувшись к лямбде:

// ArrayList has constructors: (), (int), (Collection)
Supplier<ArrayList<String>>           a = ArrayList::new;     // picks the no-arg
Function<Integer, ArrayList<String>>  b = ArrayList::new;     // picks the (int) one
Function<List<String>, ArrayList<String>> c = ArrayList::new; // picks the (Collection) one

// var inference can't disambiguate — this would not compile:
// var ambiguous = ArrayList::new;

Решение — явно указывать целевой тип, как в a, b, c выше.

Пример: все четыре формы в одной программе

Программа ниже создаёт и использует по одной ссылке на метод каждой формы, демонстрирует, как String::length (несвязанная) становится Function<String, Integer>, и показывает трюк с ссылкой на конструктор, лежащий в основе stream().toArray(T[]::new).

java— editable, runs on the server

Что следует вынести из запуска:

  • Все четыре формы компилируются в экземпляры обычных функциональных интерфейсов — parse является Function<String, Integer> независимо от того, записано ли s -> Integer.parseInt(s) или Integer::parseInt. Сокращение чисто синтаксическое.
  • Несвязанные String::length и String::toUpperCase имеют получателя в качестве первого параметра. Именно поэтому String::length — это Function<String, Integer>, а String::startsWithBiPredicate<String, String>: получатель занимает один слот, явный параметр — другой.
  • Ссылка на конструктор String[]::new создала IntFunction<String[]> — именно ту форму, которую ожидает stream().toArray(...). Ссылки на конструкторы — это способ сообщить стриму «вот тип назначения».
  • Компаратор с обратным порядком по длине не мог быть записан как ссылка на метод: получатель и параметр меняются местами, а ссылки на методы не могут переупорядочивать аргументы. Это именно тот случай, когда лямбда по-прежнему является правильным выбором.

Что дальше

Теперь вы можете писать стримовый конвейер почти полностью из ссылок на методы, оставляя несколько небольших лямбд там, где преобразования действительно требуют формирования. Этот стиль — естественное введение к центральной теме раздела: стримам. Следующая глава, Введение в Java Streams, знакомит с API Stream<T> — что это такое, как выглядит стримовый конвейер, почему он ленивый, почему его можно использовать только один раз и как он сочетается с лямбдами, функциональными интерфейсами и ссылками на методы, которые вы только что изучили.

Практика

Практика
`String::length` — какой вид ссылки на метод и какому функциональному интерфейсу она соответствует?
`String::length` — какой вид ссылки на метод и какому функциональному интерфейсу она соответствует?
Was this page helpful?