Pull to refresh

Comments 40

Хотя спецификация не обязывает конкретную реализацию виртуальной машины к такому поведению (...), но в Java HotSpot VM наблюдается именно это.

Кто-нибудь объяснит мне, зачем рантайму вообще знать про лямбды и почему компилятор не может заменить их на вложенные классы в процессе компиляции?

Насколько я понимаю — чтобы дать виртуальной машине пространство для манёвра (оптимизаций времени выполнения). То что вы предлагаете лишит виртуальную машину такой гибкости.

… а заодно — чтобы сделать такие оптимизации обязательными. Понятно.

Вложенные классы для этого неэффективны. Лямбды дают много возможностей, и их легковесность поощряет их частое использование, особенно совместно с такими вещами как библиотека стримов или какие-нибудь новые фреймворки, сделанные с учётом лямбд. Количество вложенных классов будет возрастать стремительно. Например, в скале, где анонимные функции используются буквально везде, переход на бэкенд, генерирующий лямбды аналогично javac восьмой версии, позволил уменьшить количество классов в несколько раз.


Кроме того, у лямбд другая семантика, они не просто синтаксический сахар. Это видно, в частности, из данной статьи. У них другая семантика в отношении вывода типов и в отношении захвата this. Вот здесь есть небольшое описание в первом ответе. Сделать аналогично поверх классов сложно.

this — это просто элемент синтаксиса языка. Разумеется, у лямбд и классов разный синтаксис, для того лямбды и вводили чтобы можно было меньше писать. Но это не значит что в байт-коде нельзя взять и захватить this, сохранив его как поле вложенного класса.


То же самое про вывод типов. Он делается компилятором. Вся "другая семантика" лямбд заканчивается на этапе компиляции.


Лямбды дают много возможностей, и их легковесность поощряет их частое использование, особенно совместно с такими вещами как библиотека стримов или какие-нибудь новые фреймворки, сделанные с учётом лямбд. Количество вложенных классов будет возрастать стремительно.

Чем плохо возрастание вложенных классов? Только выбранным форматом хранения байт-кода (1 класс — 1 файл), или чем-то еще?

У вас странный вопрос. Компилятор, конечно, может скомпилировать лямбду в анонимный класс. Но может сделать и более эффективный код. Почему компилятор генерит более эффективный код, а не менее эффективный? Гм…
Количество классов, вообще говоря, ограничено. И, например, в андроиде ограничено не очень большой цифрой. Достичь предела совсем не сложно.

Так ведь в этом вопрос и заключается!


Почему то, что там генерируется сейчас, более эффективно чем анонимный класс? За счет чего ускорение?

В конце концов там так же генерируется класс, только происходит это полностью в runtime.
Здесь описан способ посмотреть, какой именно: 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), так что пример в моём вопросе ещё более непоказателен, чем казалось.

UFO just landed and posted this here

Я тут немного нафилософствовал, на самом деле отличие немного в другом. К созданному анонимному классу привязан класс-лоадер — это класс-лоадер внешнего класса. Но по факту класс ему не принадлежит, это просто для удобства сделано. В частности, к класс-лоадеру класс не привязан. То есть класс-лоадер не ссылается на эту лямбду (например, она может быть собрана сборщиком мусора независимо от класс-лоадера). И протекшн-домена у анонимного класса нет. Разницу легко прощупать на следующем примере:


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.

Как раз за счет инструкции invokedynamic.
Отличие тут в том, что класс для лямбды будет создан лениво, в рантайме, а не на уровне компиляции (как с анонимными классами).
Более того, объект для лямбды будет создан 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" (как раз в синтетическом поле разных экземпляров рантайм-представления).


Настоящих замыканий нет вовсе не поэтому, а из-за модели памяти. И это прекрасно, что их нет.

>> Effectively final не мешает одной и той же лямбде при разных вызовах захватить новое значение.

Здесь просто явная путаница у людей, что есть лямбда как класс, который отвечает за форму и создаётся через 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));

Вам не кажется, что это невозможно?

А в Java лямбы могут захватывать внешние переменные?

ЕМНИП, они могут захватывать неизменяемые (final) переменные, так же как это делают анонимные классы

Достаточно effectively final (однократное присваивание значения переменной), жесткого final не требуется.

Если быть точным, то переменная должна быть как минимум effectively final. То есть:
        int number = 42;
        Runnable correct =  () -> System.out.println(number);

        Runnable incorrect = () -> number = 56; //неверно
Да, могут. Но переменные должны быть либо объявлены final или быть финальными по существу (effectively final), т.е. после инициализации их значение не меняется.

Также есть особый случай: переменная цикла for-each также считается финальной по существу:

for (String s : Arrays.asList("a", "b", "c")) {
    runLambda(() -> System.out.println(s));
}

Такой код скомпилируется без ошибок.

Как и многое другое в Java, сделано чтобы защитить разработчиков от себя самих и не создавать шарад в коде, особенно многопоточном.
UFO just landed and posted this here

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

UFO just landed and posted this here
И не забывайте что анонимных классов на уровне VM не существует.

Что заставило вас подумать, что я об этом забыл?

И не забывайте что анонимных классов на уровне VM не существует.

Что заставило вас думать, что их не существует? :D

UFO just landed and posted this here
На самом деле в примере с
Collections.sort(list, new Comparator<Integer>() {...});
на JDK 7+ на самом деле никакого пересоздания объекта происходить не должно (гуглить allocation elimination и/или scalar replacement). Так что лямбды тут перед анонимными классами не дают преимущества, а замечание «мудрейшего тимлида» — просто устаревший приём.
Не стоит преувеличивать возможности Allocation Elimination. Обычно это работает только в простых случаях. Как минимум, метод анонимного класса для этого должен оказаться заинлайнен. Что очень маловероятно в реальных приложениях для мегаморфных callsite-ов вроде Collections.sort.
Да, вы правы. В случае с Collections.sort это не сработает.

Идиоматично писать не -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. Не нужны тут ни лямбды, ни анонимные классы.

Это только ради примера. Я уже потом подумал, что есть готовый компаратор в Collections и чешутся руки пример упростить с его использованием.

to lany: спасибо за неизменно интересный анализ краевых случаев в комментариях!
Sign up to leave a comment.

Articles