Иногда можно услышать такие разговоры: никаких принципиальных изменений в 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

В заключение


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

Комментарии (40)


  1. mayorovp
    17.10.2016 16:17

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

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


    1. kdenisk
      17.10.2016 16:25

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


      1. mayorovp
        17.10.2016 16:29

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


    1. Googolplex
      17.10.2016 16:42
      +3

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


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


      1. mayorovp
        17.10.2016 16:58

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


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


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

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


        1. avost
          17.10.2016 19:30
          +1

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


          1. mayorovp
            17.10.2016 20:22
            +1

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


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


            1. Maccimo
              17.10.2016 21:14

              В конце концов там так же генерируется класс, только происходит это полностью в runtime.
              Здесь описан способ посмотреть, какой именно: https://bugs.openjdk.java.net/browse/JDK-8023524


            1. avost
              18.10.2016 09:38

              Надо призывать товарища Шипилёва. Или изучать его труды — он же рассказывал и что сначала лямбды были реализованы через анонимный класс и так далее. А я боюсь соврать — подзабыл теорию.


              1. lany
                18.10.2016 12:27
                +5

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


                1. ibessonov
                  18.10.2016 13:43
                  +1

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


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

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


                  1. lany
                    18.10.2016 13:48
                    +3

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


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

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


                    1. ibessonov
                      18.10.2016 14:13

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


                      1. Sirikid
                        18.10.2016 14:14
                        +3

                        Посмотрите доклад Никиты Липского на JBreak 2016, он как раз рассказывал там о некоторых хитростях хотспота.


                  1. lany
                    19.10.2016 13:31
                    +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. KeLsTaR
              18.10.2016 12:31
              +1

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


              1. lany
                18.10.2016 12:59
                +4

                Более того, объект для лямбды будет создан 1 раз и закэширован навсегда, а не будет создаваться каждый раз новый, как в случае с анонимным классом.

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


                1. KeLsTaR
                  18.10.2016 13:12
                  -2

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

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


                  1. lany
                    18.10.2016 13:26
                    +3

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


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

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


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


                    1. kdenisk
                      18.10.2016 13:47

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

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

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

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


                  1. kdenisk
                    18.10.2016 13:33
                    +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) закешируется только экземпляр лямбды без состояния. Для экземпляров лямбд с состоянием этого не происходит.


                    1. lany
                      18.10.2016 13:43
                      +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));


                      1. KeLsTaR
                        18.10.2016 16:41

                        Да, здесь в общем вы правы, я ошибся, мой случай действительно применим только тогда, когда захвата у лямбды не происходит. Иначе ей надо где-то хранить захваченное.


                  1. mayorovp
                    18.10.2016 13:43

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


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


                    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. MacIn
      17.10.2016 18:44
      -1

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


      1. mayorovp
        17.10.2016 18:46

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


        1. grossws
          17.10.2016 21:12
          +1

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


        1. za-box
          17.10.2016 23:26
          +2

          Если быть точным, то переменная должна быть как минимум effectively final. То есть:

                  int number = 42;
                  Runnable correct =  () -> System.out.println(number);
          
                  Runnable incorrect = () -> number = 56; //неверно
          


      1. kdenisk
        18.10.2016 00:05
        +2

        Да, могут. Но переменные должны быть либо объявлены final или быть финальными по существу (effectively final), т.е. после инициализации их значение не меняется.

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

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

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

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


      1. Sirikid
        18.10.2016 10:57
        +2

        Могут, но только по значению, как и всё остальное в Java. Поэтому внутри лямбды нельзя изменять значение переменных на стеке (локальных переменных, параметров метода), но можно, например, инкрементировать захваченный AtomicLong, ссылку мы не трогаем а сам объект находится в хипе.


        1. lany
          18.10.2016 12:58
          +3

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


    1. Sirikid
      18.10.2016 11:06
      +1

      AFAIK Hotspot не создает класс для лямбды пока не будет вызван Object#getClass. И не забывайте что анонимных классов на уровне VM не существует.
      CC: kdenisk avost Maccimo


      1. mayorovp
        18.10.2016 11:35

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

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


      1. lany
        18.10.2016 12:57
        +1

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

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


        1. Sirikid
          18.10.2016 13:07
          +1

          Конечно я знаю про Unsafe.defineAnonymousClass, эта часть комментария относится к обороту «Компилятор, конечно, может скомпилировать лямбду в анонимный класс.» товарища avost.


  1. Regis
    17.10.2016 16:48
    +2

    На самом деле в примере с

    Collections.sort(list, new Comparator<Integer>() {...});
    на JDK 7+ на самом деле никакого пересоздания объекта происходить не должно (гуглить allocation elimination и/или scalar replacement). Так что лямбды тут перед анонимными классами не дают преимущества, а замечание «мудрейшего тимлида» — просто устаревший приём.


    1. apangin
      17.10.2016 20:02
      +6

      Не стоит преувеличивать возможности Allocation Elimination. Обычно это работает только в простых случаях. Как минимум, метод анонимного класса для этого должен оказаться заинлайнен. Что очень маловероятно в реальных приложениях для мегаморфных callsite-ов вроде Collections.sort.


      1. Regis
        18.10.2016 19:01

        Да, вы правы. В случае с Collections.sort это не сработает.


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


    1. kdenisk
      18.10.2016 12:08
      +3

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

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