Comments 40
Хотя спецификация не обязывает конкретную реализацию виртуальной машины к такому поведению (...), но в Java HotSpot VM наблюдается именно это.
Кто-нибудь объяснит мне, зачем рантайму вообще знать про лямбды и почему компилятор не может заменить их на вложенные классы в процессе компиляции?
Вложенные классы для этого неэффективны. Лямбды дают много возможностей, и их легковесность поощряет их частое использование, особенно совместно с такими вещами как библиотека стримов или какие-нибудь новые фреймворки, сделанные с учётом лямбд. Количество вложенных классов будет возрастать стремительно. Например, в скале, где анонимные функции используются буквально везде, переход на бэкенд, генерирующий лямбды аналогично javac восьмой версии, позволил уменьшить количество классов в несколько раз.
Кроме того, у лямбд другая семантика, они не просто синтаксический сахар. Это видно, в частности, из данной статьи. У них другая семантика в отношении вывода типов и в отношении захвата this. Вот здесь есть небольшое описание в первом ответе. Сделать аналогично поверх классов сложно.
this
— это просто элемент синтаксиса языка. Разумеется, у лямбд и классов разный синтаксис, для того лямбды и вводили чтобы можно было меньше писать. Но это не значит что в байт-коде нельзя взять и захватить this
, сохранив его как поле вложенного класса.
То же самое про вывод типов. Он делается компилятором. Вся "другая семантика" лямбд заканчивается на этапе компиляции.
Лямбды дают много возможностей, и их легковесность поощряет их частое использование, особенно совместно с такими вещами как библиотека стримов или какие-нибудь новые фреймворки, сделанные с учётом лямбд. Количество вложенных классов будет возрастать стремительно.
Чем плохо возрастание вложенных классов? Только выбранным форматом хранения байт-кода (1 класс — 1 файл), или чем-то еще?
Количество классов, вообще говоря, ограничено. И, например, в андроиде ограничено не очень большой цифрой. Достичь предела совсем не сложно.
Так ведь в этом вопрос и заключается!
Почему то, что там генерируется сейчас, более эффективно чем анонимный класс? За счет чего ускорение?
Здесь описан способ посмотреть, какой именно: https://bugs.openjdk.java.net/browse/JDK-8023524
Я не Шипилёв, но скажу. Анонимный класс в смысле языка Java — это совсем не то, что анонимный класс в смысле JVM (говорю о HotSpot). Джавовый анонимный класс для JVM ничем не отличается от обычного. Но в JVM есть именно анонимные классы (создаются через Unsafe.defineAnonymousClass), которые действительно легковеснее обычных. К примеру, они не привязаны к класс-лоадеру. И лямбды (в отличие от анонимных классов Java) материализуются как раз через defineAnonymousClass.
Разрешите уточнить насчёт Unsafe#defineAnonymousClass. Чем они легковеснее?
Запускаю я вот такой код:
Supplier<Integer> f = () -> 42;
System.out.println(f.getClass().getClassLoader());
и получаю sun.misc.Launcher$AppClassLoader@1b6d3586 — вполне себе ClassLoader. Или дело тут в чём-то другом?
Почему вы решили, что код работает одинаково, когда getClass()
вызывается и когда не вызывается? Запустите такой код:
Integer x = 4242;
System.out.println(System.identityHashCode(x));
Выводит? Выводит. Значит, x
— это реальный объект с идентичностью, верно? Верно. Значит, сколько мы Integer
в коде объявим, столько объектов в куче и будет, верно? А вот и нет, вызов identityHashCode всё меняет. Без него объект мог бы скаляризоваться. Это как в квантовой физике: когда вы пытаетесь измерить систему, вы на неё своим измерением влияете, и система от этого изменяет квантовое состояние.
Спасибо!
Получается трюк в том, что пока мы какие-нибудь свойства класса не попросим, его как бы и нет? Для меня это действительно неожиданное свойство.
Что же касается лямбд — если верить коду InnerClassLambdaMetafactory, то всегда будет вызван либо innerClass.getDeclaredConstructors()
, либо UNSAFE.ensureClassInitialized(innerClass)
, так что пример в моём вопросе ещё более непоказателен, чем казалось.
Я тут немного нафилософствовал, на самом деле отличие немного в другом. К созданному анонимному классу привязан класс-лоадер — это класс-лоадер внешнего класса. Но по факту класс ему не принадлежит, это просто для удобства сделано. В частности, к класс-лоадеру класс не привязан. То есть класс-лоадер не ссылается на эту лямбду (например, она может быть собрана сборщиком мусора независимо от класс-лоадера). И протекшн-домена у анонимного класса нет. Разницу легко прощупать на следующем примере:
public class Test {
public static void main(String[] args) throws Exception {
Runnable r = new Runnable() {public void run(){}};//() -> {};
System.out.println(r.getClass().getClassLoader());
System.out.println(r.getClass().getName());
System.out.println(Class.forName(r.getClass().getName(), false, r.getClass().getClassLoader()));
}
}
Обычный джавовый анонимный класс вы легко можете загрузить через класс-лоадер по имени. Анонимный класс JVM по имени никогда не загружается (замените анонимный класс на лямбду и получите ClassNotFoundException.
Отличие тут в том, что класс для лямбды будет создан лениво, в рантайме, а не на уровне компиляции (как с анонимными классами).
Более того, объект для лямбды будет создан 1 раз и закэширован навсегда, а не будет создаваться каждый раз новый, как в случае с анонимным классом.
Работает это примерно так: тело лямбды помещается в приватный статический метод в том же классе, где она объявлена, со всеми необходимыми лямбде аргументами, при первом вызове invokedynamic посмотрит, что такого объекта еще нет и начнет создавать объект-обертку над этим статическим методом, который бы заодно реализовывал Comparator, за это отвечает LambdaMetafactory, она создает инстанс компаратора, ссылаясь на статический метод лямбды с помощью MethodHandles. Так как статический метод принимает в себя в качестве аргументов все, что лямбде нужно, мы можем использовать потом этот же объект для любого вызова этой лямбды в системе, что и происходит — при любом последующем вызове работать будет все тот же один объект, ничего нового создаваться не будет.
Более того, объект для лямбды будет создан 1 раз и закэширован навсегда, а не будет создаваться каждый раз новый, как в случае с анонимным классом.
Только в случае, если лямбда ничего не захватывает.
Так как статический метод принимает в себя в качестве аргументов все, что лямбде нужно, мы можем использовать потом этот же объект для любого вызова этой лямбды в системе
Даже если лямбда что-либо захватывает, это будет передано ей в качестве аргумента статического метода.
Именно поэтому и существует ограничение на захват только effectively final переменных — так как в джаве аргументы передаются только по значению (то есть копируются), то если бы мы смогли внутри лямбды переприсвоить значение этой переменной, на самом деле поменялась бы только её копия, а исходная переменная осталась бы такой же, что контринтуитивно и поэтому запрещено.
И именно поэтому говорят что в джаве нет настоящих замыканий — лямбды захватывают не сами переменные, а только значения этих переменных.
Effectively final не мешает одной и той же лямбде при разных вызовах захватить новое значение.
Supplier<String> get(String x) { return () -> x; }
Supplier<String> s1 = get("a");
Supplier<String> s2 = get("b");
Здесь лямбда в коде ровно одна и рантайм-представление под неё одно сгенерируется. Но объекта будет два (s1 != s2), потому что где-то же надо хранить эти "a"
и "b"
(как раз в синтетическом поле разных экземпляров рантайм-представления).
Настоящих замыканий нет вовсе не поэтому, а из-за модели памяти. И это прекрасно, что их нет.
Здесь просто явная путаница у людей, что есть лямбда как класс, который отвечает за форму и создаётся через defineAnonymousClass.
И есть конкретный экземпляр лямбды, который в синтетических полях хранит захваченные значения и выполнение тела лямбды происходит уже на нём.
Поскольку у лямбды без состояния, которая ничего не захватывает, и хранить ничего не надо, то можно создать синглтон и его переиспользовать потом везде.
Если можно приведите пруф этой информации. Т.е. комментарий в JLS говорит нам, что:
A new object need not be allocated on every evaluation.
Но на Stack Overflow есть такой комментарий (опять же без ссылок):
http://stackoverflow.com/questions/23983832/is-method-reference-caching-a-good-idea-in-java-8/23991339#23991339
в котором говорится, что гарантировано (с поправкой на конкретную реализацию — HotSpot VM) закешируется только экземпляр лямбды без состояния. Для экземпляров лямбд с состоянием этого не происходит.
JLS говорит, что объект необязательно будет создаваться, то есть это на усмотрение реализации. Когда будет, а когда нет — вам не гарантируется.
А если говорить о конкретной реализации в OpenJDK, то лучший источник информации — исходники. Если параметров нет, то создаётся коллсайт на константный методхэндл, который замкнут на единственный экземпляр:
Object inst = ctrs[0].newInstance();
return new ConstantCallSite(MethodHandles.constant(samBase, inst));
А если параметры есть, то коллсайт — это фабричный метод, который создаёт новый экземпляр:
return new ConstantCallSite(
MethodHandles.Lookup.IMPL_LOOKUP.findStatic(innerClass, NAME_FACTORY,
invokedType));
Как вы себе представляете технически использование одного и того же объекта?
Допустим, есть такой метод:
Callable<Integer> const(int v) {
return () -> v;
}
вызываем его:
Callable<Integer> a = const(1), b = const(2);
Вы утверждаете:
при любом последующем вызове работать будет все тот же один объект, ничего нового создаваться не будет
По-вашему, теперь должно получиться вот так?
assert (a == b);
assert (a.call().equals(1));
assert (b.call().equals(2));
Вам не кажется, что это невозможно?
ЕМНИП, они могут захватывать неизменяемые (final
) переменные, так же как это делают анонимные классы
Достаточно effectively final (однократное присваивание значения переменной), жесткого final
не требуется.
int number = 42;
Runnable correct = () -> System.out.println(number);
Runnable incorrect = () -> number = 56; //неверно
Также есть особый случай: переменная цикла for-each также считается финальной по существу:
for (String s : Arrays.asList("a", "b", "c")) {
runLambda(() -> System.out.println(s));
}
Такой код скомпилируется без ошибок.
Как и многое другое в Java, сделано чтобы защитить разработчиков от себя самих и не создавать шарад в коде, особенно многопоточном.
Collections.sort(list, new Comparator<Integer>() {...});на JDK 7+ на самом деле никакого пересоздания объекта происходить не должно (гуглить allocation elimination и/или scalar replacement). Так что лямбды тут перед анонимными классами не дают преимущества, а замечание «мудрейшего тимлида» — просто устаревший приём.
Идиоматично писать не -Integer.compare(o1, o2)
, а Integer.compare(o2, o1)
. Спецификация Integer.compare
позволяет возвращать любое число (необязательно -1), в том числе Integer.MIN_VALUE
. Все знают, чему равно -Integer.MIN_VALUE
?
А ещё более идиоматично писать Collections.sort(list, Collections.reverseOrder())
. Удивительна страсть людей к велосипедам. Даже если компаратор написать просто, неужели не приходило в голову, что он уже мог быть написан в библиотеке? Этот метод существует, я думаю, с Java 1.2. Не нужны тут ни лямбды, ни анонимные классы.
Как в Java выстрелить себе в ногу из лямбды и не промахнуться