Создает ли лямбда-выражение объект в куче при каждом выполнении?

187

Когда я перебираю коллекцию, используя новый синтаксический сахар Java 8, такой как

myStream.forEach(item -> {
  // do something useful
});

Разве это не эквивалентно приведенному ниже фрагменту "старого синтаксиса"?

myStream.forEach(new Consumer<Item>() {
  @Override
  public void accept(Item item) {
    // do something useful
  }
});

Означает ли это, что новый анонимный Consumerобъект создается в куче каждый раз, когда я перебираю коллекцию? Сколько места в куче для этого требуется? Какие последствия для производительности это имеет? Означает ли это, что при итерации по большим многоуровневым структурам данных мне лучше использовать старый стиль циклов?

Бастиан Фойгт
источник
48
Короткий ответ: нет. Для лямбда-выражений без сохранения состояния (тех, которые ничего не захватывают из своего лексического контекста), только один экземпляр будет когда-либо создан (лениво) и кэширован на сайте захвата. (Вот как работает реализация; спецификация была тщательно написана, чтобы разрешить, но не обязать такой подход.)
Брайан Гетц

Ответы:

162

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

Поведение точно не указано. JVM предоставляется большая свобода в том, как ее реализовать. В настоящее время JVM Oracle создает (по крайней мере) один экземпляр для каждого лямбда-выражения (т.е. не разделяет экземпляр между разными идентичными выражениями), но создает одиночные экземпляры для всех выражений, которые не захватывают значения.

Вы можете прочитать этот ответ для получения более подробной информации. Там я не только дал более подробное описание, но и протестировал код для наблюдения за текущим поведением.


Это описано в Спецификации языка Java®, глава « 15.27.4. Оценка лямбда-выражений во время выполнения »

Вкратце:

Эти правила предназначены для обеспечения гибкости реализации языка программирования Java в том смысле, что:

  • Нет необходимости выделять новый объект при каждой оценке.

  • Объекты, созданные разными лямбда-выражениями, не обязательно должны принадлежать к разным классам (например, если тела идентичны).

  • Каждый объект, созданный при оценке, не обязательно должен принадлежать к одному и тому же классу (например, захваченные локальные переменные могут быть встроены).

  • Если «существующий экземпляр» доступен, он не обязательно должен быть создан при предыдущей лямбда-оценке (например, он мог быть выделен во время инициализации включающего класса).

Хольгер
источник
25

Создание экземпляра, представляющего лямбду, сильно зависит от точного содержимого тела лямбды. А именно, ключевым фактором является то, что лямбда захватывает из лексической среды. Если он не фиксирует какое-либо состояние, которое изменяется от создания к созданию, то экземпляр не будет создаваться каждый раз при входе в цикл for-each. Вместо этого синтетический метод будет сгенерирован во время компиляции, и сайт использования лямбда просто получит одноэлементный объект, который делегирует этому методу.

Также обратите внимание, что этот аспект зависит от реализации, и вы можете ожидать будущих улучшений и улучшений HotSpot для повышения эффективности. Есть общие планы, например, сделать легкий объект без полного соответствующего класса, который имеет достаточно информации для передачи в единственный метод.

Вот хорошая и доступная подробная статья по этой теме:

http://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood

Марко Топольник
источник
0

Вы передаете forEachметоду новый экземпляр . Каждый раз, когда вы это делаете, вы создаете новый объект, но не по одному для каждой итерации цикла. Итерация выполняется внутри forEachметода с использованием того же экземпляра объекта «обратного вызова», пока она не будет выполнена в цикле.

Таким образом, память, используемая циклом, не зависит от размера коллекции.

Разве это не эквивалент фрагмента "старого синтаксиса"?

Да. У него есть небольшие отличия на очень низком уровне, но я не думаю, что вам следует о них беспокоиться. В выражениях Lamba вместо анонимных классов используется функция invokedynamic.

Аалку
источник
У вас есть документация, подтверждающая это? Довольно интересная оптимизация.
А. Рама
Спасибо, но что, если у меня есть набор коллекций коллекций, например, при выполнении поиска в глубину в структуре данных дерева?
Bastian Voigt
2
@ A.Rama Простите, оптимизацию не вижу. То же самое с лямбдами или без них и с циклом forEach или без него.
aalku
1
На самом деле это не одно и то же, но все же в любой момент времени потребуется не более одного объекта на уровень вложенности, что незначительно. Каждая новая итерация внутреннего цикла будет создавать новый объект, скорее всего, захватывая текущий элемент из внешнего цикла. Это создает некоторую нагрузку на сборщик мусора, но по-прежнему не о чем беспокоиться.
Марко Топольник
4
@aalku: « Каждый раз, когда вы это делаете, вы создаете новый объект »: не в соответствии с этим ответом Хольгера и этим комментарием Брайана Гетца .
Lii