Как в Java выстрелить себе в ногу из лямбды и не промахнуться

    Иногда можно услышать такие разговоры: никаких принципиальных изменений в Java 8 не произошло и лямбды это старые добрые анонимные классы щедро посыпанные синтаксическим сахаром. Как бы не так! Предлагаю сегодня поговорить, в чём отличие лямбд от анонимных классов. И почему попасть себе в ногу стало всё-таки сложнее.

    Чтобы не отнимать время у тех, кто считает что уже освоился с анонимными функциями, простенькая задачка. Чем отличаются два фрагмента кода ниже:

    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
    }
    

    RunnableAnonymousClassExperiment.class (подробная декомпиляция)
    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
    }
    

    Lambda.class (подробная декомпиляция)
    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;)...
    

    Декомпиляция LambdaCallsNonStatic.class
    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

    В заключение


    Статья не претендует на сверхстрогость, академичность и полноту, но мне кажется (такой я самонадеянный, сейчас получу в комментариях по первое число) в достаточной мере раскрывает две киллер-фичи, заставляющих ещё больше проникнуться лямбдами. Критика в комментариях, конструктивная и не очень, категорически приветствуется.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 40

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

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

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

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

          +3

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


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

            0

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


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


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

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

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

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


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

                  0
                  В конце концов там так же генерируется класс, только происходит это полностью в runtime.
                  Здесь описан способ посмотреть, какой именно: https://bugs.openjdk.java.net/browse/JDK-8023524
                    0
                    Надо призывать товарища Шипилёва. Или изучать его труды — он же рассказывал и что сначала лямбды были реализованы через анонимный класс и так далее. А я боюсь соврать — подзабыл теорию.
                      +5

                      Я не Шипилёв, но скажу. Анонимный класс в смысле языка Java — это совсем не то, что анонимный класс в смысле JVM (говорю о HotSpot). Джавовый анонимный класс для JVM ничем не отличается от обычного. Но в JVM есть именно анонимные классы (создаются через Unsafe.defineAnonymousClass), которые действительно легковеснее обычных. К примеру, они не привязаны к класс-лоадеру. И лямбды (в отличие от анонимных классов Java) материализуются как раз через defineAnonymousClass.

                        +1

                        Разрешите уточнить насчёт Unsafe#defineAnonymousClass. Чем они легковеснее?
                        Запускаю я вот такой код:


                        Supplier<Integer> f = () -> 42;
                        System.out.println(f.getClass().getClassLoader());

                        и получаю sun.misc.Launcher$AppClassLoader@1b6d3586 — вполне себе ClassLoader. Или дело тут в чём-то другом?

                          +3

                          Почему вы решили, что код работает одинаково, когда getClass() вызывается и когда не вызывается? Запустите такой код:


                          Integer x = 4242;
                          System.out.println(System.identityHashCode(x));

                          Выводит? Выводит. Значит, x — это реальный объект с идентичностью, верно? Верно. Значит, сколько мы Integer в коде объявим, столько объектов в куче и будет, верно? А вот и нет, вызов identityHashCode всё меняет. Без него объект мог бы скаляризоваться. Это как в квантовой физике: когда вы пытаетесь измерить систему, вы на неё своим измерением влияете, и система от этого изменяет квантовое состояние.

                            0

                            Спасибо!
                            Получается трюк в том, что пока мы какие-нибудь свойства класса не попросим, его как бы и нет? Для меня это действительно неожиданное свойство.
                            Что же касается лямбд — если верить коду InnerClassLambdaMetafactory, то всегда будет вызван либо innerClass.getDeclaredConstructors(), либо UNSAFE.ensureClassInitialized(innerClass), так что пример в моём вопросе ещё более непоказателен, чем казалось.

                            • UFO just landed and posted this here
                            +4

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


                            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.
                        Отличие тут в том, что класс для лямбды будет создан лениво, в рантайме, а не на уровне компиляции (как с анонимными классами).
                        Более того, объект для лямбды будет создан 1 раз и закэширован навсегда, а не будет создаваться каждый раз новый, как в случае с анонимным классом.
                        Работает это примерно так: тело лямбды помещается в приватный статический метод в том же классе, где она объявлена, со всеми необходимыми лямбде аргументами, при первом вызове invokedynamic посмотрит, что такого объекта еще нет и начнет создавать объект-обертку над этим статическим методом, который бы заодно реализовывал Comparator, за это отвечает LambdaMetafactory, она создает инстанс компаратора, ссылаясь на статический метод лямбды с помощью MethodHandles. Так как статический метод принимает в себя в качестве аргументов все, что лямбде нужно, мы можем использовать потом этот же объект для любого вызова этой лямбды в системе, что и происходит — при любом последующем вызове работать будет все тот же один объект, ничего нового создаваться не будет.
                          +4
                          Более того, объект для лямбды будет создан 1 раз и закэширован навсегда, а не будет создаваться каждый раз новый, как в случае с анонимным классом.

                          Только в случае, если лямбда ничего не захватывает.

                            –2
                            Так как статический метод принимает в себя в качестве аргументов все, что лямбде нужно, мы можем использовать потом этот же объект для любого вызова этой лямбды в системе

                            Даже если лямбда что-либо захватывает, это будет передано ей в качестве аргумента статического метода.
                            Именно поэтому и существует ограничение на захват только effectively final переменных — так как в джаве аргументы передаются только по значению (то есть копируются), то если бы мы смогли внутри лямбды переприсвоить значение этой переменной, на самом деле поменялась бы только её копия, а исходная переменная осталась бы такой же, что контринтуитивно и поэтому запрещено.
                            И именно поэтому говорят что в джаве нет настоящих замыканий — лямбды захватывают не сами переменные, а только значения этих переменных.
                              +3

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


                              Supplier<String> get(String x) { return () -> x; }
                              
                              Supplier<String> s1 = get("a");
                              Supplier<String> s2 = get("b");

                              Здесь лямбда в коде ровно одна и рантайм-представление под неё одно сгенерируется. Но объекта будет два (s1 != s2), потому что где-то же надо хранить эти "a" и "b" (как раз в синтетическом поле разных экземпляров рантайм-представления).


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

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

                                Здесь просто явная путаница у людей, что есть лямбда как класс, который отвечает за форму и создаётся через defineAnonymousClass.

                                И есть конкретный экземпляр лямбды, который в синтетических полях хранит захваченные значения и выполнение тела лямбды происходит уже на нём.

                                Поскольку у лямбды без состояния, которая ничего не захватывает, и хранить ничего не надо, то можно создать синглтон и его переиспользовать потом везде.
                                +1
                                >> Даже если лямбда что-либо захватывает, это будет передано ей в качестве аргумента статического метода.

                                Если можно приведите пруф этой информации. Т.е. комментарий в 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) закешируется только экземпляр лямбды без состояния. Для экземпляров лямбд с состоянием этого не происходит.
                                  +3

                                  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));
                                    0
                                    Да, здесь в общем вы правы, я ошибся, мой случай действительно применим только тогда, когда захвата у лямбды не происходит. Иначе ей надо где-то хранить захваченное.
                                  0

                                  Как вы себе представляете технически использование одного и того же объекта?


                                  Допустим, есть такой метод:


                                  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));

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

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

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

                          +1

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

                            +2
                            Если быть точным, то переменная должна быть как минимум effectively final. То есть:
                                    int number = 42;
                                    Runnable correct =  () -> System.out.println(number);
                            
                                    Runnable incorrect = () -> number = 56; //неверно
                            
                            +2
                            Да, могут. Но переменные должны быть либо объявлены 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
                                +3

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

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

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

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

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

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

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

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

                                  Only users with full accounts can post comments. Log in, please.