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

 

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

Автор: admin от 17-10-2016, 15:45, посмотрело: 224

Иногда можно услышать такие разговоры: никаких принципиальных изменений в 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."":()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."":(LAnonymousClass;)V
8: areturn

public static void main(java.lang.String[]);
Code:
0: new #4 // class AnonymousClass
3: dup
4: invokespecial #5 // Method "":()V
7: invokevirtual #6 // Method getRunnable:()Ljava/lang/Runnable;
10: invokeinterface #7, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return
}


С лямбдой


Compiled from "Lambda.java"
public class Lambda {
public Lambda();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()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 "":()V
7: invokevirtual #5 // Method getRunnable:()Ljava/lang/Runnable;
10: invokeinterface #6, 1 // InterfaceMethod java/lang/Runnable.run:()V
15: return
}


Анализируем

Что-нибудь бросилось в глаза? Та-та-та-дам…

Анонимный класс:


5: invokespecial #3 // Method AnonymousClass$1."":(LAnonymousClass;)V

Лямбда:


0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;

Кажется анонимный класс захватил при создании ссылку на порождающий его экземпляр:


AnonymousClass$1."":(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;)...


Решение: объявить используемый метод статическим или вынести его в отдельный утильный класс.

И всё?

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

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

В заключение

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

Источник: Хабрахабр

Категория: Программирование

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

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *