Что означает оператор "::" (двойное двоеточие) в Java 8?
List<String> list = Arrays.asList("a", "bb", "ccc");
list.stream().filter(s -> s.length() > 1).forEach(s -> System.out.println(s));
Но потом я увидел такой код:
list.stream().filter(s -> s.length() > 1).forEach(System.out::println);
Здесь вместо `s -> System.out.println(s)` написано `System.out::println`. Что это за магия? Это то же самое? Почему нельзя просто использовать лямбду? И как понять, когда можно писать `::`, а когда нельзя? Можете объяснить простыми словами с примерами?
Также видел такие варианты:
list.stream().map(String::toUpperCase)
list.stream().sorted(String::compareToIgnoreCase)
Как это работает? Это какой-то новый тип оператора или синтаксический сахар? Буду благодарен за развернутое объяснение для новичка.
<<Оператор "::" — это ссылка на метод (Method Reference)
Вы всё верно поняли: это синтаксический сахар, который заменяет лямбда-выражение, когда оно просто вызывает один существующий метод. Оператор :: (двойное двоеточие) в Java 8 называется ссылкой на метод. Он делает код короче и читаемее, но работает точно так же, как лямбда.
Базовый принцип
Когда ваша лямбда выглядит как x -> someMethod(x) (просто передаёт аргумент в метод), вы можете заменить её на ClassOrObject::someMethod. Компилятор сам понимает, какой метод вызывать и с какими аргументами.
Ваш пример:
// лямбда
s -> System.out.println(s)
// ссылка на метод
System.out::println
Это одно и то же. System.out — это объект (статическое поле класса System), а println — его метод. Компилятор видит, что forEach ожидает Consumer<? super String>, то есть функцию, принимающую один аргумент. Метод println как раз принимает один аргумент — подходит идеально.
Четыре вида ссылок на методы
- Ссылка на статический метод:
ClassName::staticMethod - Ссылка на метод экземпляра конкретного объекта:
object::instanceMethod - Ссылка на метод экземпляра произвольного объекта:
ClassName::instanceMethod(для первого аргумента как объекта) - Ссылка на конструктор:
ClassName::new
Примеры каждого вида
1. Статический метод
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
// лямбда
numbers.stream().map(n -> Integer.parseInt(n.toString()))
// ссылка на метод
numbers.stream().map(Integer::parseInt) // parseInt — статический метод
2. Метод экземпляра конкретного объекта
List<String> words = Arrays.asList("hello", "world");
StringBuilder sb = new StringBuilder();
// лямбда
words.forEach(w -> sb.append(w));
// ссылка на метод
words.forEach(sb::append); // sb — конкретный объект
3. Метод экземпляра произвольного объекта (самый частый случай)
List<String> names = Arrays.asList("ivan", "petr", "anna");
// лямбда
names.stream().map(s -> s.toUpperCase())
// ссылка на метод
names.stream().map(String::toUpperCase)
// Здесь String — это класс, а toUpperCase вызывается на каждом элементе
В этом случае компилятор понимает: первый аргумент (элемент стрима) станет объектом, на котором вызывается метод toUpperCase. То есть String::toUpperCase эквивалентно s -> s.toUpperCase().
4. Ссылка на конструктор
List<String> strings = Arrays.asList("a", "b", "c");
// лямбда
strings.stream().map(s -> new StringBuilder(s))
// ссылка на конструктор
strings.stream().map(StringBuilder::new) // создаёт новый StringBuilder из строки
Когда использовать ссылки на методы?
- Когда лямбда просто вызывает один метод и ничего больше не делает (нет дополнительной логики, преобразований).
- Когда это улучшает читаемость:
list.forEach(System.out::println)читается как "для каждого элемента вывести в System.out". - Когда метод имеет подходящую сигнатуру (количество и типы параметров совпадают с функциональным интерфейсом).
Когда НЕЛЬЗЯ использовать?
- Если в лямбде есть дополнительная логика:
s -> { System.out.println(s); return s.length(); }— тут два действия, ссылка не подойдёт. - Если нужно передать аргументы в другом порядке или преобразовать их.
- Если метод перегружен и компилятор не может однозначно определить, какой вариант вызывать (редко, но бывает).
Ваш пример с sorted
list.stream().sorted(String::compareToIgnoreCase)
Здесь sorted ожидает Comparator<String> — функцию, принимающую два аргумента (две строки) и возвращающую int. Метод compareToIgnoreCase — это метод экземпляра, который принимает один аргумент. Но в контексте Comparator компилятор интерпретирует это как: первый аргумент — это объект, на котором вызывается метод, второй — передаётся как параметр. То есть (a, b) -> a.compareToIgnoreCase(b).
Итог: оператор :: — это просто удобная сокращённая запись для лямбд, которые делегируют работу существующему методу. Никакой магии, только синтаксический сахар.
Дополнение: ссылки на методы и отладка
Первый ответ отлично объяснил синтаксис. Хочу добавить пару практических замечаний, которые помогут избежать ошибок.
Ограничения ссылок на методы
Ссылки на методы не работают, если:
- Метод выбрасывает проверяемое исключение (checked exception), которое не объявлено в функциональном интерфейсе. Лямбда может перехватить исключение внутри себя, а ссылка — нет.
- Нужно передать аргументы в другом порядке. Например,
(a, b) -> b.compareTo(a)— ссылкой не заменить.
Производительность
Ссылки на методы и лямбды компилируются практически в один и тот же байт-код (через invokedynamic). Разницы в производительности нет. Выбирайте то, что читается лучше.
Совет для новичков
На начальном этапе всегда пишите лямбды полностью — это безопаснее и понятнее. Когда привыкнете к функциональным интерфейсам (Consumer, Function, Predicate), начнёте автоматически замечать, где можно применить ::. Не пытайтесь использовать ссылки везде — иногда лямбда с явными именами параметров читается лучше.
Например, это:
list.stream().filter(s -> s.startsWith("A")).map(s -> s.trim()).forEach(s -> System.out.println(s));
Понятнее, чем:
list.stream().filter(s -> s.startsWith("A")).map(String::trim).forEach(System.out::println);
Хотя второй вариант короче, новичку сложнее понять, что именно происходит с каждым элементом.
Ответов пока нет.
Войдите чтобы оставить ответ