Статьи

Компиляция лямбда-выражений: Scala против Java 8

В последние несколько лет лямбда-выражения покорили мир программирования. Большинство современных языков приняли их в качестве фундаментальной части функционального программирования. Языки на основе JVM, такие как Scala, Groovy и Clojure, интегрировали их в качестве ключевой части языка. И теперь, Java 8 (наконец) присоединяется к веселью.

лямбда Что интересно в лямбда-выражениях, так это то, что с точки зрения JVM они совершенно невидимы. В нем нет понятия, что такое анонимная функция или лямбда-выражение. Он знает только байт-код, который является строгой спецификацией ОО. Создатели языка и его компилятор должны работать в рамках этих ограничений, чтобы создавать более новые, более сложные языковые элементы.

Мы впервые столкнулись с этим, когда работали над добавлением поддержки Scala в Takipi, и нам пришлось углубиться в компилятор Scala. С Java 8 прямо за углом я подумал, что было бы интересно посмотреть, как компиляторы Scala и Java реализуют лямбда-выражения. Результаты были довольно удивительными.

Чтобы все пошло как по маслу, я взял простое лямбда-выражение, которое преобразует список строк в список их длин.

В Java —

1
2
List names = Arrays.asList("1", "2", "3");
Stream lengths = names.stream().map(name -> name.length());

В Скале —

1
2
val names = List("1", "2", "3")
val lengths = names.map(name => name.length)

Не обманывайте его простоту — за кулисами происходят некоторые сложные вещи.

Давайте начнем со Scala

SCalaLam-1

Код

Я использовал javap для просмотра содержимого байт-кода .class, созданного компилятором Scala. Давайте посмотрим на байт-код результата (это то, что JVM фактически выполнит).

1
2
3
4
5
6
// this loads the names var into the stack (the JVM thinks
// of it as variable #2).
// It’s going to stay there for a while till it gets used
// by the <em>.map</em> function.
 
aload_2

Далее все становится интереснее — создается и инициализируется новый экземпляр синтетического класса, сгенерированный компилятором. Это объект, который с точки зрения JVM содержит метод Lambda. Забавно, что хотя лямбда определяется как неотъемлемая часть нашего метода, в действительности она живет совершенно за пределами нашего класса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
new myLambdas/Lambda1$$anonfun$1 //instantiate the Lambda object
dup //put it into the stack again
 
// finally, invoke the c’tor. Remember - it’s just a plain object
// from the JVM’s perspective.
invokespecial myLambdas/Lambda1$$anonfun$1/()V
 
// these two (long) lines loads the immutable.List CanBuildFrom factory
// which will create the new list. This factory pattern is a part of
// Scala’s collections architecture
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;
 
// Now we have on the stack the Lambda object and the factory.
// The next phase is to call the .<em>map</em>() function.
// If you remember, we loaded the <em>names</em> var onto
// the stack in the beginning. Now it’ll gets used as the
// this for the .<em>map</em>() call, which will also
// accept the Lambda object and the factory to produce the
// new list of lengths.
 
invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;

Но держись — что происходит внутри этого лямбда-объекта?

Лямбда-объект

Класс Lambda является производным от scala.runtime.AbstractFunction1 . Благодаря этому функция map () может полиморфно вызывать переопределенную apply (), чей код ниже —

01
02
03
04
05
06
07
08
09
10
11
12
13
// this code loads this and the target object on which to act,
// checks that it’s a String, and then calls another apply overload
// to do the actual work and boxes its return value.
aload_0 //load this
aload_1 //load the string arg
checkcast java/lang/String //make sure it’s a String - we got an Object
 
// call another apply() method in the synthetic class
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I
 
//box the result
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn

Фактический код для выполнения. Операция length () вложена в этот дополнительный метод apply, который просто возвращает длину строки, как мы и ожидали.

Фуф … это был довольно долгий путь, чтобы добраться сюда!

1
2
3
aload_1
invokevirtual java/lang/String/length()I
ireturn

Для такой простой строки, как мы пишем выше, генерируется довольно много байт-кода — дополнительный класс и куча новых методов. Это, конечно, не означает, что мы не должны использовать Lambdas (мы пишем на Scala, а не на C). Это просто показывает сложность этих конструкций. Только подумайте о количестве кода и сложности, которые входят в составление сложных цепочек лямбда-выражений!

Я вполне ожидал, что Java 8 будет реализовывать это таким же образом, но был весьма удивлен, увидев, что они полностью выбрали другой подход.

Java 8 — новый подход

JavaLam-1

Байт-код здесь немного короче, но делает что-то довольно удивительное. Он начинается довольно просто с загрузки имен var и вызывает его. Метод stream (), но тогда он делает что-то довольно элегантное. Вместо создания нового объекта, который обернет функцию Lambda, он использует новую инструкцию invokeDynamic, которая была добавлена ​​в Java 7, чтобы динамически связать этот сайт вызова с реальной функцией Lambda.

01
02
03
04
05
06
07
08
09
10
11
aload_1 //load the names var
 
// call its stream() func
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;
 
//invokeDynamic magic!
invokedynamic #0:apply:()Ljava/util/function/Function;
 
//call the map() func
invokeinterface java/util/stream/Stream.map:
(Ljava/util/function/Function;)Ljava/util/stream/Stream;

InvokeDynamic магия . Эта инструкция JVM была добавлена ​​в Java 7, чтобы сделать JVM менее строгой, и позволяет динамическим языкам связывать символы во время выполнения по сравнению со статическим выполнением всех связей, когда код компилируется JVM.

Динамическое связывание . Если вы посмотрите на фактическую инструкцию invokedynamic, то увидите, что нет ссылки на реальную лямбда-функцию (называемую lambda $ 0). Ответ заключается в том, как устроен invokedynamic (который сам по себе заслуживает полного поста), но краткий ответ — это имя и подпись лямбды, которая в нашем случае —

1
2
// a function named lamda$0 that gets a String and returns an Integer
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

хранятся в записи в отдельной таблице в .class, в который параметр # 0 передается в командные точки. Эта новая таблица фактически изменила структуру спецификации байт-кода впервые после хороших нескольких лет, что потребовало от нас также адаптировать механизм анализа ошибок Takipi .

Лямбда-код

Это код фактического лямбда-выражения. Это очень печенья — просто загрузите параметр String, вызовите length () и запишите результат. Обратите внимание, что он был скомпилирован как статическая функция, чтобы избежать необходимости передавать ему дополнительный объект, как мы видели в Scala.

1
2
3
4
aload_0
invokevirtual java/lang/String.length:()
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
areturn

Это еще одно преимущество метода invokedynamic , так как он позволяет нам вызывать метод полиморфно с точки зрения функции .map (), но без необходимости выделять объект-обертку или вызывать виртуальный метод переопределения. Довольно круто!

Резюме

Интересно видеть, как Java, самый «строгий» из современных языков, теперь использует динамическое связывание для усиления своих новых лямбда-выражений. Это также эффективный подход, так как не требуется никакой дополнительной загрузки и компиляции классов — метод Lambda — это просто еще один частный метод в нашем классе.

Java 8 действительно проделала очень элегантную работу по использованию новой технологии, представленной в Java 7, для реализации лямбда-выражений очень простым способом. Приятно видеть, что даже «почтенная» леди, такая как Java, может научить нас всем новым трюкам

Ссылка: Компиляция лямбда-выражений: Scala против Java 8 от нашего партнера по JCG Тала Вейса из блога Takipi .