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

Мы впервые столкнулись с этим, когда работали над добавлением поддержки 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
Код
Я использовал 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 objectdup //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 architecturegetstatic 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 thisaload_1 //load the string argcheckcast java/lang/String //make sure it’s a String - we got an Object// call another apply() method in the synthetic classinvokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I//box the resultinvokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integerareturn |
Фактический код для выполнения. Операция length () вложена в этот дополнительный метод apply, который просто возвращает длину строки, как мы и ожидали.
Фуф … это был довольно долгий путь, чтобы добраться сюда!
|
1
2
3
|
aload_1invokevirtual java/lang/String/length()Iireturn |
Для такой простой строки, как мы пишем выше, генерируется довольно много байт-кода — дополнительный класс и куча новых методов. Это, конечно, не означает, что мы не должны использовать Lambdas (мы пишем на Scala, а не на C). Это просто показывает сложность этих конструкций. Только подумайте о количестве кода и сложности, которые входят в составление сложных цепочек лямбда-выражений!
Я вполне ожидал, что Java 8 будет реализовывать это таким же образом, но был весьма удивлен, увидев, что они полностью выбрали другой подход.
Java 8 — новый подход
Байт-код здесь немного короче, но делает что-то довольно удивительное. Он начинается довольно просто с загрузки имен 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() funcinvokeinterface java/util/List.stream:()Ljava/util/stream/Stream;//invokeDynamic magic!invokedynamic #0:apply:()Ljava/util/function/Function;//call the map() funcinvokeinterface 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 Integerlambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer; |
хранятся в записи в отдельной таблице в .class, в который параметр # 0 передается в командные точки. Эта новая таблица фактически изменила структуру спецификации байт-кода впервые после хороших нескольких лет, что потребовало от нас также адаптировать механизм анализа ошибок Takipi .
Лямбда-код
Это код фактического лямбда-выражения. Это очень печенья — просто загрузите параметр String, вызовите length () и запишите результат. Обратите внимание, что он был скомпилирован как статическая функция, чтобы избежать необходимости передавать ему дополнительный объект, как мы видели в Scala.
|
1
2
3
4
|
aload_0invokevirtual java/lang/String.length:()invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;areturn |
Это еще одно преимущество метода invokedynamic , так как он позволяет нам вызывать метод полиморфно с точки зрения функции .map (), но без необходимости выделять объект-обертку или вызывать виртуальный метод переопределения. Довольно круто!
Резюме
Интересно видеть, как Java, самый «строгий» из современных языков, теперь использует динамическое связывание для усиления своих новых лямбда-выражений. Это также эффективный подход, так как не требуется никакой дополнительной загрузки и компиляции классов — метод Lambda — это просто еще один частный метод в нашем классе.
Java 8 действительно проделала очень элегантную работу по использованию новой технологии, представленной в Java 7, для реализации лямбда-выражений очень простым способом. Приятно видеть, что даже «почтенная» леди, такая как Java, может научить нас всем новым трюкам

