Статьи

Темная сторона лямбда-выражений в Java 8

blog_lambada_2

Этот пост не может сделать меня новыми друзьями. Ну да ладно, я никогда не был действительно популярен в школе. Но давайте перейдем к делу. Самая большая особенность языка Java 8 — это, несомненно, лямбда-выражения. Это была ключевая функция для функциональных языков, таких как Scala и Clojure, в течение нескольких лет, и теперь Java наконец присоединилась.

Вторая самая большая особенность (в зависимости, конечно, от того, кого вы спрашиваете) — это Nashorn — новый движок JVM JavaScript, который должен привести Java в соответствие с другими движками JS, такими как V8 и его контейнер node.js.

Но эти новые функции имеют темную сторону к ним.

Я объясню. Платформа Java состоит из двух основных компонентов. JRE, который JIT компилирует и выполняет байт-код, и JDK, который содержит инструменты dev и компилятор исходного кода javac. Эти два компонента довольно (но не полностью) отделены друг от друга, что позволяет людям писать свои собственные языки JVM, и Scala стала популярной в последние несколько лет. И в этом заключается некоторая проблема.

JVM была разработана, чтобы быть независимой от языка в том смысле, что она может выполнять код, написанный на любом языке, при условии, что он может быть преобразован в байт-код. Сама спецификация байт-кода полностью OO, и была разработана, чтобы близко соответствовать языку Java. Это означает, что байт-код, скомпилированный из исходного кода Java, будет очень похож на него структурно

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

Когда вы смотрите на полностью динамические языки, такие как JavaScript , это расстояние становится огромным.

И теперь с Java 8 это начинает распространяться и на Java.

Так почему я должен заботиться?

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

Чтобы увидеть, как давайте (повторно) посетить пример ниже.

Java 6 & 7

Это традиционный метод, с помощью которого мы будем перебирать список строк, чтобы отобразить их длину.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// simple check against empty strings
public static int check(String s) {
    if (s.equals("")) {
        throw new IllegalArgumentException();
    }
    return s.length();
}
  
//map names to lengths
  
List lengths = new ArrayList();
  
for (String name : Arrays.asList(args)) {
    lengths.add(check(name));
}

Это вызовет исключение, если передается пустая строка. Трассировка стека будет выглядеть так:

1
2
at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)

Здесь мы видим соотношение 1: 1 между трассировкой стека, которую мы видим, и кодом, который мы написали, что делает отладку этого стека вызовов довольно простой. Это то, к чему привыкли большинство разработчиков Java.

Теперь давайте посмотрим на Scala и Java 8.

Scala

Давайте посмотрим на тот же код в Scala. Здесь у нас есть две большие перемены. Первое — это использование лямбда-выражения для отображения длин, а второе — итерация выполняется структурой (т. Е. Внутренняя итерация).

1
val lengths = names.map(name => check(name.length))

Здесь мы действительно начинаем замечать разницу между тем, как выглядит код, который вы написали, и тем, как JVM (и вы) увидят его во время выполнения. Если выдается исключение, стек вызовов на порядок длиннее , и его гораздо сложнее понять.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
at Main$.check(Main.scala:6)
at Main$$anonfun$1.apply(Main.scala:12)
at Main$$anonfun$1.apply(Main.scala:12)
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
at scala.collection.immutable.List.foreach(List.scala:318)
at scala.collection.TraversableLike$class.map(TraversableLike.scala:244)
at scala.collection.AbstractTraversable.map(Traversable.scala:105)
at Main$delayedInit$body.apply(Main.scala:12)
at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.collection.immutable.List.foreach(List.scala:318)
at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32)
at scala.App$class.main(App.scala:71)
at Main$.main(Main.scala:1)
at Main.main(Main.scala)

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

Это долго было проблемой со Scala, и одной из причин, по которой мы создали Scala Stackifier .

А теперь в Java 8

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
Stream lengths = names.stream().map(name -> check(name));
 
at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)

Это становится очень похоже на Scala. Мы платим цену за более короткий и краткий код с более сложной отладкой и более длинными синтетическими стеками вызовов.

Причина в том, что, хотя javac был расширен для поддержки лямбда-функций, JVM по-прежнему не замечает их. Это было дизайнерским решением Java-пользователей, чтобы поддерживать работу JVM на более низком уровне и не вводить новые элементы в его спецификацию.

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

JavaScript в Java 8

Java 8 представляет совершенно новый компилятор JavaScript. Теперь мы можем эффективно и просто интегрировать Java + JS. Однако нигде нет диссонанса между кодом, который мы пишем, и кодом, который мы отлаживаем, больше, чем здесь.

Вот та же самая функция в Nashorn —

1
2
3
4
5
6
7
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
 
String js = "var map = Array.prototype.map \n";
js += "var a = map.call(names, function(name) { return Java.type(\"LmbdaMain\").check(name) }) \n";
js += "print(a)";
engine.eval(js);

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

Вопросы, комментарии? (при условии, что вы можете прокрутить весь путь ниже этого стека вызовов). Дайте мне знать в разделе комментариев.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
LmbdaMain [Java Application]
LmbdaMain at localhost:51287
Thread [main] (Suspended (breakpoint at line 16 in LmbdaMain))
LmbdaMain.wrap(String) line: 16
1525037790.invokeStatic_L_I(Object, Object) line: not available
1150538133.invokeSpecial_LL_I(Object, Object, Object) line: not available
538592647.invoke_LL_I(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
2150540.interpret_I(MethodHandle, Object, Object) line: not available
538592647.invoke_LL_I(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
92150540.interpret_I(MethodHandle, Object, Object) line: not available
38592647.invoke_LL_I(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
731260860.interpret_L(MethodHandle, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LL_L(MethodHandle, Object[]) line: 1108
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
2619171.interpret_L(MethodHandle, Object, Object, Object) line: not available
1597655940.invokeSpecial_LLLL_L(Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLL_L(MethodHandle, Object[]) line: 1118
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
2619171.interpret_L(MethodHandle, Object, Object, Object) line: not available
1353530305.linkToCallSite(Object, Object, Object, Object) line: not available
Script$\^eval\_._L3(ScriptFunction, Object, Object) line: 3
1596000437.invokeStatic_LLL_L(Object, Object, Object, Object) line: not available
1597655940.invokeSpecial_LLLL_L(Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLL_L(MethodHandle, Object[]) line: 1118
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
484673893.interpret_L(MethodHandle, Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLLL_L(MethodHandle, Object[]) line: 1123
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
282496973.interpret_L(MethodHandle, Object, Object, Object, long, Object) line: not available
93508253.invokeSpecial_LLLLJL_L(Object, Object, Object, Object, Object, long, Object) line: not available
1850777594.invoke_LLLLJL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
282496973.interpret_L(MethodHandle, Object, Object, Object, long, Object) line: not available
293508253.invokeSpecial_LLLLJL_L(Object, Object, Object, Object, Object, long, Object) line: not available
1850777594.invoke_LLLLJL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
1840903588.interpret_L(MethodHandle, Object, Object, Object, Object, long, Object) line: not available
2063763486.reinvoke(Object, Object, Object, Object, Object, long, Object) line: not available
850777594.invoke_LLLLJL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
82496973.interpret_L(MethodHandle, Object, Object, Object, long, Object) line: not available
220309324.invokeExact_MT(Object, Object, Object, Object, long, Object, Object) line: not available
NativeArray$10.forEach(Object, long) line: 1304
NativeArray$10(IteratorAction).apply() line: 124
NativeArray.map(Object, Object, Object) line: 1315
1596000437.invokeStatic_LLL_L(Object, Object, Object, Object) line: not available
504858437.invokeExact_MT(Object, Object, Object, Object, Object) line: not available
FinalScriptFunctionData(ScriptFunctionData).invoke(ScriptFunction, Object, Object...) line: 522
ScriptFunctionImpl(ScriptFunction).invoke(Object, Object...) line: 207
ScriptRuntime.apply(ScriptFunction, Object, Object...) line: 378
NativeFunction.call(Object, Object...) line: 161
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
1740189450.invokeSpecial_LLL_L(Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLL_L(MethodHandle, Object[]) line: 1113
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
2619171.interpret_L(MethodHandle, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLL_L(MethodHandle, Object[]) line: 1113
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
323326911.interpret_L(MethodHandle, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLL_L(MethodHandle, Object[]) line: 1118
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
323326911.interpret_L(MethodHandle, Object, Object, Object, Object) line: not available
263793464.invokeSpecial_LLLLL_L(Object, Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLLL_L(MethodHandle, Object[]) line: 1123
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
1484673893.interpret_L(MethodHandle, Object, Object, Object, Object, Object) line: not available
587003819.invokeSpecial_LLLLLL_L(Object, Object, Object, Object, Object, Object, Object) line: not available
811301908.invoke_LLLLLL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
484673893.interpret_L(MethodHandle, Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLLL_L(MethodHandle, Object[]) line: 1123
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object...) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object...) line: 604
323326911.interpret_L(MethodHandle, Object, Object, Object, Object) line: not available
2129144075.linkToCallSite(Object, Object, Object, Object, Object) line: not available
Script$\^eval\_.runScript(ScriptFunction, Object) line: 3
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
1709804316.invokeExact_MT(Object, Object, Object, Object) line: not available
FinalScriptFunctionData(ScriptFunctionData).invoke(ScriptFunction, Object, Object...) line: 498
ScriptFunctionImpl(ScriptFunction).invoke(Object, Object...) line: 207
ScriptRuntime.apply(ScriptFunction, Object, Object...) line: 378
NashornScriptEngine.evalImpl(ScriptFunction, ScriptContext, ScriptObject) line: 544
NashornScriptEngine.evalImpl(ScriptFunction, ScriptContext) line: 526
NashornScriptEngine.evalImpl(Source, ScriptContext) line: 522
NashornScriptEngine.eval(String, ScriptContext) line: 193
NashornScriptEngine(AbstractScriptEngine).eval(String) line: 264
LmbdaMain.main(String[]) line: 44
Ссылка: Темная сторона лямбда-выражений в Java 8 от нашего партнера по JCG Тала Вайса из блога Takipi