Чтобы не отнимать время у тех, кто считает что уже освоился с анонимными функциями, простенькая задачка. Чем отличаются два фрагмента кода ниже:
public class AnonymousClass {
public Runnable getRunnable() {
return new Runnable() {
@Override
public void run() {
System.out.println("I am a Runnable!");
}
};
}
public static void main(String[] args) {
new AnonymousClass().getRunnable().run();
}
}
И второй фрагмент:
public class Lambda {
public Runnable getRunnable() {
return () -> System.out.println("I am a Runnable!");
}
public static void main(String[] args) {
new Lambda().getRunnable().run();
}
}
Если можете сходу ответить — решайте сами, хотите ли читать дальше.
Декомпилируем
Смотрим байт код для обоих вариантов. (Подробная декомпиляция с флажком -verbose — под спойлером.)
С анонимным классом
Compiled from "AnonymousClass.java"
public class AnonymousClass {
public AnonymousClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.Runnable getRunnable();
Code:
0: new #2 // class AnonymousClass$1
3: dup
4: aload_0
5: invokespecial #3 // Method AnonymousClass$1."<init>":(LAnonymousClass;)V
8: areturn
public static void main(java.lang.String[]);
Code:
0: new #4 // class AnonymousClass
3: dup
4: invokespecial #5 // Method "<init>":()V
7: invokevirtual #6 // Method getRunnable:()Ljava/lang/Runnable;
10: invokeinterface #7, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return
}
Classfile /E:/.../src/main/java/AnonymousClass.class
Last modified 17.10.2016; size 518 bytes
MD5 checksum cf61f38da50d7062537edefea71995dc
Compiled from "AnonymousClass.java"
public class AnonymousClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // AnonymousClass$1
#3 = Methodref #2.#22 // AnonymousClass$1."<init>":(LAnonymousClass;)V
#4 = Class #23 // AnonymousClass
#5 = Methodref #4.#20 // AnonymousClass."<init>":()V
#6 = Methodref #4.#24 // AnonymousClass.getRunnable:()Ljava/lang/Runnable;
#7 = InterfaceMethodref #25.#26 // java/lang/Runnable.run:()V
#8 = Class #27 // java/lang/Object
#9 = Utf8 InnerClasses
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 getRunnable
#15 = Utf8 ()Ljava/lang/Runnable;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 AnonymousClass.java
#20 = NameAndType #10:#11 // "<init>":()V
#21 = Utf8 AnonymousClass$1
#22 = NameAndType #10:#28 // "<init>":(LAnonymousClass;)V
#23 = Utf8 AnonymousClass
#24 = NameAndType #14:#15 // getRunnable:()Ljava/lang/Runnable;
#25 = Class #29 // java/lang/Runnable
#26 = NameAndType #30:#11 // run:()V
#27 = Utf8 java/lang/Object
#28 = Utf8 (LAnonymousClass;)V
#29 = Utf8 java/lang/Runnable
#30 = Utf8 run
{
public AnonymousClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public java.lang.Runnable getRunnable();
descriptor: ()Ljava/lang/Runnable;
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: new #2 // class AnonymousClass$1
3: dup
4: aload_0
5: invokespecial #3 // Method AnonymousClass$1."<init>":(LAnonymousClass;)V
8: areturn
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #4 // class AnonymousClass
3: dup
4: invokespecial #5 // Method "<init>":()V
7: invokevirtual #6 // Method getRunnable:()Ljava/lang/Runnable;
10: invokeinterface #7, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return
LineNumberTable:
line 12: 0
line 13: 15
}
SourceFile: "AnonymousClass.java"
InnerClasses:
#2; //class AnonymousClass$1
С лямбдой
Compiled from "Lambda.java"
public class Lambda {
public Lambda();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.Runnable getRunnable();
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: areturn
public static void main(java.lang.String[]);
Code:
0: new #3 // class Lambda
3: dup
4: invokespecial #4 // Method "<init>":()V
7: invokevirtual #5 // Method getRunnable:()Ljava/lang/Runnable;
10: invokeinterface #6, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return
}
Classfile /E:/.../src/main/java/Lambda.class
Last modified 17.10.2016; size 1095 bytes
MD5 checksum f09061410dfbe358c50880576557b64e
Compiled from "Lambda.java"
public class Lambda
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #10.#22 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#27 // #0:run:()Ljava/lang/Runnable;
#3 = Class #28 // Lambda
#4 = Methodref #3.#22 // Lambda."<init>":()V
#5 = Methodref #3.#29 // Lambda.getRunnable:()Ljava/lang/Runnable;
#6 = InterfaceMethodref #30.#31 // java/lang/Runnable.run:()V
#7 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream;
#8 = String #34 // I am a Runnable!
#9 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = Class #37 // java/lang/Object
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 getRunnable
#16 = Utf8 ()Ljava/lang/Runnable;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 lambda$getRunnable$0
#20 = Utf8 SourceFile
#21 = Utf8 Lambda.java
#22 = NameAndType #11:#12 // "<init>":()V
#23 = Utf8 BootstrapMethods
#24 = MethodHandle #6:#38 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#25 = MethodType #12 // ()V
#26 = MethodHandle #6:#39 // invokestatic Lambda.lambda$getRunnable$0:()V
#27 = NameAndType #40:#16 // run:()Ljava/lang/Runnable;
#28 = Utf8 Lambda
#29 = NameAndType #15:#16 // getRunnable:()Ljava/lang/Runnable;
#30 = Class #41 // java/lang/Runnable
#31 = NameAndType #40:#12 // run:()V
#32 = Class #42 // java/lang/System
#33 = NameAndType #43:#44 // out:Ljava/io/PrintStream;
#34 = Utf8 I am a Runnable!
#35 = Class #45 // java/io/PrintStream
#36 = NameAndType #46:#47 // println:(Ljava/lang/String;)V
#37 = Utf8 java/lang/Object
#38 = Methodref #48.#49 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#39 = Methodref #3.#50 // Lambda.lambda$getRunnable$0:()V
#40 = Utf8 run
#41 = Utf8 java/lang/Runnable
#42 = Utf8 java/lang/System
#43 = Utf8 out
#44 = Utf8 Ljava/io/PrintStream;
#45 = Utf8 java/io/PrintStream
#46 = Utf8 println
#47 = Utf8 (Ljava/lang/String;)V
#48 = Class #51 // java/lang/invoke/LambdaMetafactory
#49 = NameAndType #52:#56 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#50 = NameAndType #19:#12 // lambda$getRunnable$0:()V
#51 = Utf8 java/lang/invoke/LambdaMetafactory
#52 = Utf8 metafactory
#53 = Class #58 // java/lang/invoke/MethodHandles$Lookup
#54 = Utf8 Lookup
#55 = Utf8 InnerClasses
#56 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#57 = Class #59 // java/lang/invoke/MethodHandles
#58 = Utf8 java/lang/invoke/MethodHandles$Lookup
#59 = Utf8 java/lang/invoke/MethodHandles
{
public Lambda();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public java.lang.Runnable getRunnable();
descriptor: ()Ljava/lang/Runnable;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: areturn
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #3 // class Lambda
3: dup
4: invokespecial #4 // Method "<init>":()V
7: invokevirtual #5 // Method getRunnable:()Ljava/lang/Runnable;
10: invokeinterface #6, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return
LineNumberTable:
line 7: 0
line 8: 15
}
SourceFile: "Lambda.java"
InnerClasses:
public static final #54= #53 of #57; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#25 ()V
#26 invokestatic Lambda.lambda$getRunnable$0:()V
#25 ()V
Анализируем
Что-нибудь бросилось в глаза? Та-та-та-дам…
Анонимный класс:
5: invokespecial #3 // Method AnonymousClass$1."<init>":(LAnonymousClass;)V
Лямбда:
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
Кажется анонимный класс захватил при создании ссылку на порождающий его экземпляр:
AnonymousClass$1."<init>":(LAnonymousClass;)V
и будет держать её, пока всесильный Сборщик Мусора™ не пометит его как недостижимый и не освободит от этого бремени. Хотя никак эта ссылка внутри не используется, но вот такой он анонимный жадина.
А если серьёзно, то здесь потенциальная утечка памяти, если вы отдаёте экземпляр анонимного класса во внешний мир. С лямбдами это произойдёт только в том случае, если вы явно или неявно ссылаетесь на this в теле анонимной функции. В противном случае, как в этом примере, лямбда ссылки на вызывающий её экземпляр не держит.
Делаем своими руками. Предлагаю всем читателям провести эксперимент и посмотреть что будет в каждом из случаев, если к строке добавить вызов .toString() у порождающего экземляра.
Как в ногу-то попасть? Обещал рассказать!
Самый простой способ напороться на потенциальную утечку памяти — это использовать внутри лямбды нестатические методы внешнего класса, если вам в реальности неинтересно его внутреннее состояние:
public class LambdaCallsNonStatic {
public Runnable getRunnable() {
return () -> {
nonStaticMethod();
};
}
public void nonStaticMethod() {
System.out.println("I am a Runnable!");
}
public static void main(String[] args) {
new LambdaCallsNonStatic().getRunnable().run();
}
}
Лямбда получит ссылку на экземпляр класса её вызывающий (хотя будет создана один раз, но об этом ниже):
1: invokedynamic #2, 0 // InvokeDynamic #0:run:(LLambdaCallsNonStatic;)...
Compiled from "LambdaCallsNonStatic.java"
public class LambdaCallsNonStatic {
public LambdaCallsNonStatic();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.Runnable getRunnable();
Code:
0: aload_0
1: invokedynamic #2, 0 // InvokeDynamic #0:run:(LLambdaCallsNonStatic;)Ljava/lang/Runnable;
6: areturn
public void nonStaticMethod();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String I am a Runnable!
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void main(java.lang.String[]);
Code:
0: new #6 // class LambdaCallsNonStatic
3: dup
4: invokespecial #7 // Method "<init>":()V
7: invokevirtual #8 // Method getRunnable:()Ljava/lang/Runnable;
10: invokeinterface #9, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return
}
Решение: объявить используемый метод статическим или вынести его в отдельный утильный класс.
И всё?
Нет, есть ещё одна замечательная плюшка у лямбд по сравнению с анонимными классами. Если вы когда-нибудь работали в застенках кроваво-энтерпрайзной конторы и не дай боже? писали такое:
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return -Integer.compare(o1, o2);
}
});
То подходил к вам о мудрейший тимлид и говорил:
Не экономно ты,
Ведь новый экземпляр компаратора будет создаваться каждый раз при работе этого фрагмента кода. В результате получалась такая портянка:
public class CorporateComparators {
public static Comparator<Integer> integerReverseComparator() {
return IntegerReverseComparator.INSTANCE;
}
private enum IntegerReverseComparator implements Comparator<Integer> {
INSTANCE;
@Override
public int compare(Integer o1, Integer o2) {
return -Integer.compare(o1, o2);
}
}
}
...
Collections.sort(list, CorporateComparators.integerReverseComparator());
Удобнее же стало, всё в своём файлике теперь лежит и переиспользовать можно. С последним соглашусь, но удобнее стало разве что если у вас DDR4 вместо серого вещества в голове. Читабельность такого кода не просто падает, а летит в тартарары со сверхзвуковой.
С лямбдами можно держать логику ближе к месту непосредственного использования и не платить за это сверху:
Collections.sort(list, (i1, i2) -> -Integer.compare(i1, i2));
Анонимная функция, не захватывающая значений из внешнего контекста, будет лёгкой и создаваться один раз. Хотя спецификация не обязывает конкретную реализацию виртуальной машины к такому поведению (15.27.4. Run-Time Evaluation of Lambda Expressions), но в Java HotSpot VM наблюдается именно это.
Версия Явы
Эксперименты проводились на:
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)
javac 1.8.0_92
javap 1.8.0_92
В заключение
Статья не претендует на сверхстрогость, академичность и полноту, но мне кажется (такой я самонадеянный, сейчас получу в комментариях по первое число) в достаточной мере раскрывает две киллер-фичи, заставляющих ещё больше проникнуться лямбдами. Критика в комментариях, конструктивная и не очень, категорически приветствуется.
Комментарии (40)
Regis
17.10.2016 16:48+2На самом деле в примере с
Collections.sort(list, new Comparator<Integer>() {...});
на JDK 7+ на самом деле никакого пересоздания объекта происходить не должно (гуглить allocation elimination и/или scalar replacement). Так что лямбды тут перед анонимными классами не дают преимущества, а замечание «мудрейшего тимлида» — просто устаревший приём.
apangin
17.10.2016 20:02+6Не стоит преувеличивать возможности Allocation Elimination. Обычно это работает только в простых случаях. Как минимум, метод анонимного класса для этого должен оказаться заинлайнен. Что очень маловероятно в реальных приложениях для мегаморфных callsite-ов вроде Collections.sort.
lany
18.10.2016 07:33+12Идиоматично писать не
-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. Не нужны тут ни лямбды, ни анонимные классы.
mayorovp
Кто-нибудь объяснит мне, зачем рантайму вообще знать про лямбды и почему компилятор не может заменить их на вложенные классы в процессе компиляции?
kdenisk
Насколько я понимаю — чтобы дать виртуальной машине пространство для манёвра (оптимизаций времени выполнения). То что вы предлагаете лишит виртуальную машину такой гибкости.
mayorovp
… а заодно — чтобы сделать такие оптимизации обязательными. Понятно.
Googolplex
Вложенные классы для этого неэффективны. Лямбды дают много возможностей, и их легковесность поощряет их частое использование, особенно совместно с такими вещами как библиотека стримов или какие-нибудь новые фреймворки, сделанные с учётом лямбд. Количество вложенных классов будет возрастать стремительно. Например, в скале, где анонимные функции используются буквально везде, переход на бэкенд, генерирующий лямбды аналогично javac восьмой версии, позволил уменьшить количество классов в несколько раз.
Кроме того, у лямбд другая семантика, они не просто синтаксический сахар. Это видно, в частности, из данной статьи. У них другая семантика в отношении вывода типов и в отношении захвата this. Вот здесь есть небольшое описание в первом ответе. Сделать аналогично поверх классов сложно.
mayorovp
this
— это просто элемент синтаксиса языка. Разумеется, у лямбд и классов разный синтаксис, для того лямбды и вводили чтобы можно было меньше писать. Но это не значит что в байт-коде нельзя взять и захватитьthis
, сохранив его как поле вложенного класса.То же самое про вывод типов. Он делается компилятором. Вся "другая семантика" лямбд заканчивается на этапе компиляции.
Чем плохо возрастание вложенных классов? Только выбранным форматом хранения байт-кода (1 класс — 1 файл), или чем-то еще?
avost
У вас странный вопрос. Компилятор, конечно, может скомпилировать лямбду в анонимный класс. Но может сделать и более эффективный код. Почему компилятор генерит более эффективный код, а не менее эффективный? Гм…
Количество классов, вообще говоря, ограничено. И, например, в андроиде ограничено не очень большой цифрой. Достичь предела совсем не сложно.
mayorovp
Так ведь в этом вопрос и заключается!
Почему то, что там генерируется сейчас, более эффективно чем анонимный класс? За счет чего ускорение?
Maccimo
В конце концов там так же генерируется класс, только происходит это полностью в runtime.
Здесь описан способ посмотреть, какой именно: https://bugs.openjdk.java.net/browse/JDK-8023524
avost
Надо призывать товарища Шипилёва. Или изучать его труды — он же рассказывал и что сначала лямбды были реализованы через анонимный класс и так далее. А я боюсь соврать — подзабыл теорию.
lany
Я не Шипилёв, но скажу. Анонимный класс в смысле языка Java — это совсем не то, что анонимный класс в смысле JVM (говорю о HotSpot). Джавовый анонимный класс для JVM ничем не отличается от обычного. Но в JVM есть именно анонимные классы (создаются через Unsafe.defineAnonymousClass), которые действительно легковеснее обычных. К примеру, они не привязаны к класс-лоадеру. И лямбды (в отличие от анонимных классов Java) материализуются как раз через defineAnonymousClass.
ibessonov
Разрешите уточнить насчёт Unsafe#defineAnonymousClass. Чем они легковеснее?
Запускаю я вот такой код:
и получаю sun.misc.Launcher$AppClassLoader@1b6d3586 — вполне себе ClassLoader. Или дело тут в чём-то другом?
lany
Почему вы решили, что код работает одинаково, когда
getClass()
вызывается и когда не вызывается? Запустите такой код:Выводит? Выводит. Значит,
x
— это реальный объект с идентичностью, верно? Верно. Значит, сколько мыInteger
в коде объявим, столько объектов в куче и будет, верно? А вот и нет, вызов identityHashCode всё меняет. Без него объект мог бы скаляризоваться. Это как в квантовой физике: когда вы пытаетесь измерить систему, вы на неё своим измерением влияете, и система от этого изменяет квантовое состояние.ibessonov
Спасибо!
Получается трюк в том, что пока мы какие-нибудь свойства класса не попросим, его как бы и нет? Для меня это действительно неожиданное свойство.
Что же касается лямбд — если верить коду InnerClassLambdaMetafactory, то всегда будет вызван либо
innerClass.getDeclaredConstructors()
, либоUNSAFE.ensureClassInitialized(innerClass)
, так что пример в моём вопросе ещё более непоказателен, чем казалось.Sirikid
Посмотрите доклад Никиты Липского на JBreak 2016, он как раз рассказывал там о некоторых хитростях хотспота.
lany
Я тут немного нафилософствовал, на самом деле отличие немного в другом. К созданному анонимному классу привязан класс-лоадер — это класс-лоадер внешнего класса. Но по факту класс ему не принадлежит, это просто для удобства сделано. В частности, к класс-лоадеру класс не привязан. То есть класс-лоадер не ссылается на эту лямбду (например, она может быть собрана сборщиком мусора независимо от класс-лоадера). И протекшн-домена у анонимного класса нет. Разницу легко прощупать на следующем примере:
Обычный джавовый анонимный класс вы легко можете загрузить через класс-лоадер по имени. Анонимный класс JVM по имени никогда не загружается (замените анонимный класс на лямбду и получите ClassNotFoundException.
KeLsTaR
Как раз за счет инструкции invokedynamic.
Отличие тут в том, что класс для лямбды будет создан лениво, в рантайме, а не на уровне компиляции (как с анонимными классами).
Более того, объект для лямбды будет создан 1 раз и закэширован навсегда, а не будет создаваться каждый раз новый, как в случае с анонимным классом.
Работает это примерно так: тело лямбды помещается в приватный статический метод в том же классе, где она объявлена, со всеми необходимыми лямбде аргументами, при первом вызове invokedynamic посмотрит, что такого объекта еще нет и начнет создавать объект-обертку над этим статическим методом, который бы заодно реализовывал Comparator, за это отвечает LambdaMetafactory, она создает инстанс компаратора, ссылаясь на статический метод лямбды с помощью MethodHandles. Так как статический метод принимает в себя в качестве аргументов все, что лямбде нужно, мы можем использовать потом этот же объект для любого вызова этой лямбды в системе, что и происходит — при любом последующем вызове работать будет все тот же один объект, ничего нового создаваться не будет.
lany
Только в случае, если лямбда ничего не захватывает.
KeLsTaR
Даже если лямбда что-либо захватывает, это будет передано ей в качестве аргумента статического метода.
Именно поэтому и существует ограничение на захват только effectively final переменных — так как в джаве аргументы передаются только по значению (то есть копируются), то если бы мы смогли внутри лямбды переприсвоить значение этой переменной, на самом деле поменялась бы только её копия, а исходная переменная осталась бы такой же, что контринтуитивно и поэтому запрещено.
И именно поэтому говорят что в джаве нет настоящих замыканий — лямбды захватывают не сами переменные, а только значения этих переменных.
lany
Effectively final не мешает одной и той же лямбде при разных вызовах захватить новое значение.
Здесь лямбда в коде ровно одна и рантайм-представление под неё одно сгенерируется. Но объекта будет два (s1 != s2), потому что где-то же надо хранить эти
"a"
и"b"
(как раз в синтетическом поле разных экземпляров рантайм-представления).Настоящих замыканий нет вовсе не поэтому, а из-за модели памяти. И это прекрасно, что их нет.
kdenisk
>> Effectively final не мешает одной и той же лямбде при разных вызовах захватить новое значение.
Здесь просто явная путаница у людей, что есть лямбда как класс, который отвечает за форму и создаётся через defineAnonymousClass.
И есть конкретный экземпляр лямбды, который в синтетических полях хранит захваченные значения и выполнение тела лямбды происходит уже на нём.
Поскольку у лямбды без состояния, которая ничего не захватывает, и хранить ничего не надо, то можно создать синглтон и его переиспользовать потом везде.
kdenisk
>> Даже если лямбда что-либо захватывает, это будет передано ей в качестве аргумента статического метода.
Если можно приведите пруф этой информации. Т.е. комментарий в 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) закешируется только экземпляр лямбды без состояния. Для экземпляров лямбд с состоянием этого не происходит.
lany
JLS говорит, что объект необязательно будет создаваться, то есть это на усмотрение реализации. Когда будет, а когда нет — вам не гарантируется.
А если говорить о конкретной реализации в OpenJDK, то лучший источник информации — исходники. Если параметров нет, то создаётся коллсайт на константный методхэндл, который замкнут на единственный экземпляр:
А если параметры есть, то коллсайт — это фабричный метод, который создаёт новый экземпляр:
KeLsTaR
Да, здесь в общем вы правы, я ошибся, мой случай действительно применим только тогда, когда захвата у лямбды не происходит. Иначе ей надо где-то хранить захваченное.
mayorovp
Как вы себе представляете технически использование одного и того же объекта?
Допустим, есть такой метод:
вызываем его:
Вы утверждаете:
По-вашему, теперь должно получиться вот так?
Вам не кажется, что это невозможно?
MacIn
А в Java лямбы могут захватывать внешние переменные?
mayorovp
ЕМНИП, они могут захватывать неизменяемые (
final
) переменные, так же как это делают анонимные классыgrossws
Достаточно effectively final (однократное присваивание значения переменной), жесткого
final
не требуется.za-box
Если быть точным, то переменная должна быть как минимум effectively final. То есть:
kdenisk
Да, могут. Но переменные должны быть либо объявлены final или быть финальными по существу (effectively final), т.е. после инициализации их значение не меняется.
Также есть особый случай: переменная цикла for-each также считается финальной по существу:
Такой код скомпилируется без ошибок.
Как и многое другое в Java, сделано чтобы защитить разработчиков от себя самих и не создавать шарад в коде, особенно многопоточном.
Sirikid
Могут, но только по значению, как и всё остальное в Java. Поэтому внутри лямбды нельзя изменять значение переменных на стеке (локальных переменных, параметров метода), но можно, например, инкрементировать захваченный AtomicLong, ссылку мы не трогаем а сам объект находится в хипе.
lany
Маленькое уточнение, чтобы не поняли неправильно без контекста: переменные, объявленные в самой лямбде, изменять, разумеется, можно.
Sirikid
AFAIK Hotspot не создает класс для лямбды пока не будет вызван Object#getClass. И не забывайте что анонимных классов на уровне VM не существует.
CC: kdenisk avost Maccimo
mayorovp
Что заставило вас подумать, что я об этом забыл?
lany
Что заставило вас думать, что их не существует? :D
Sirikid
Конечно я знаю про Unsafe.defineAnonymousClass, эта часть комментария относится к обороту «Компилятор, конечно, может скомпилировать лямбду в анонимный класс.» товарища avost.