Ссылки на методы в 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::startsWith — BiPredicate<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).
Что следует вынести из запуска:
- Все четыре формы компилируются в экземпляры обычных функциональных интерфейсов —
parseявляетсяFunction<String, Integer>независимо от того, записано лиs -> Integer.parseInt(s)илиInteger::parseInt. Сокращение чисто синтаксическое. - Несвязанные
String::lengthиString::toUpperCaseимеют получателя в качестве первого параметра. Именно поэтомуString::length— этоFunction<String, Integer>, аString::startsWith—BiPredicate<String, String>: получатель занимает один слот, явный параметр — другой. - Ссылка на конструктор
String[]::newсоздалаIntFunction<String[]>— именно ту форму, которую ожидаетstream().toArray(...). Ссылки на конструкторы — это способ сообщить стриму «вот тип назначения». - Компаратор с обратным порядком по длине не мог быть записан как ссылка на метод: получатель и параметр меняются местами, а ссылки на методы не могут переупорядочивать аргументы. Это именно тот случай, когда лямбда по-прежнему является правильным выбором.
Что дальше
Теперь вы можете писать стримовый конвейер почти полностью из ссылок на методы, оставляя несколько небольших лямбд там, где преобразования действительно требуют формирования. Этот стиль — естественное введение к центральной теме раздела: стримам. Следующая глава, Введение в Java Streams, знакомит с API Stream<T> — что это такое, как выглядит стримовый конвейер, почему он ленивый, почему его можно использовать только один раз и как он сочетается с лямбдами, функциональными интерфейсами и ссылками на методы, которые вы только что изучили.