elizarov


Блог Романа Елизарова


Previous Entry Share Next Entry
Java 8: Lambda и другие изменения языка
elizarov

В сети появилась презентация "Java 8: Selected Updates". В ней перечислены основные изменения, которые планируется включить в Java 8 и дана весьма подробная информация про Проект Лямбда, включая исчерпывающее объяснение того, как лямбда-выражения будут представлены в скомпилированном байт-коде и почему так. Вообще, Java 8 имеет шанс переплюнуть даже Java 1.5 по общему количеству изменений в языке. Это не удивительно, ибо работа над всеми этими изменениями идет уже очень давно. Я слежу за всеми ими уже много лет и далеко не все первоначальные идеи были всесторонне продуманы. Тем приятней видеть, что дополнительное потраченное на проработку время пошло только на пользу.

Наверное самое мало обсуждаемое, но, тем не менее, очень важное изменения языка это возможность использования аннотаций всюду где идет указание типа. Эта работа в рамках JSR 308 ведется уже очень давно. Она открывает доступ в языке Java к так называемым "подключаемым системам типов", о которых Gilad Bracha писал в своей известной одноименной статье. Конечно, то что получается в Java далеко от описанного автором идеала, когда типы можно и не указывать вовсе. Я не считаю это проблемой. Gilad имеет возможность поэкспериментировать с этой концепцией в её чистом в виде в языке Dart, а в Java все-таки сохранится необходимость указывать тип полей, аргументов и переменных. Однако, в тех приложениях, где система типов в Java не достаточно строга, у программистов появится возможность подключать дополнительные системы типов стандартным образом, а не только полагаясь на инструменты от сторонних разработчиков. Я уверен, что это подстегнет не только исследования, но и практику по многим отрытым проблемам в области написании более надежного и более безопасного программного обеспечения.

Необходимость модуляризации Java (проект Jigsaw) уде давно перезрела. Сейчас JRE для Java 7 занимает более 100 мегабайт, а диаграмма зависимости между стандартными пакетами превратилась в классическую лапшу. Разобрать эту лапшу на аккуратные модули задача не из простых. Куча времени было потрачено на политические баталии с OSGi, который стал воспринимать все инициативы по модуляризации Java как конкурентов, и критиковал многие особенности предлагаемой модульной структуры Java, такие как возможность разрезания пакетов. Да, в разрезании пакетов нет ничего красивого, но это необходимо, чтобы "распилить" на модули некоторые из уже сложившихся в Java пакетов. Более того, по причине отсутствия в языке Java стандартного способа описания модулей (авторы языка неявно предполагали что такими модулями будут пакеты, но реальная практика показа несостоятельность модели "пакет-как-модуль"), появилось множество способов модуляризации не завязанных на сам язык (кладем каждый модуль в отдельный каталог) со всеми вытекающими отсюда проблемами отсутствия соответствиями между проверками выполняемыми на этапе компиляции и проверками при исполнении кода. Так как многие крупные проекты вынуждены были всё это время существовать в тех же условиях, что и сама платформа Java, то те подходы, которые хороши для модуляризации самой Java, будут хороши и для них.

Ну и конечно Проект Лямбда. Он больше всего выиграл от полученной форы во времени. На первые потуги в этом направлении было больно смотреть — начиная от предлагаемого синтаксиса до стратегии реализации. Я рад, что разум восторжествовал и вроде уже финальный синтаксис не вызывает никакого отторжения. Он позволяет компактней и понятней записывать уже устоявшиеся шаблоны кода в Java. Да что там говорить. Разум восторжествовал даже в синтаксисе методов расширений, и авторам новых библиотек во многих случаях не придется больше для каждого интерфейса Foo создавать парный ему класс AbstractFoo, а порой еще и класс Foos с дополнительными методами-помощниками.

Будь Проект Лямбда реализован раньше, то он бы с трудом полетел. Единственная доступная ранее стратегия реализации предполагала создание внутренних классов для каждого замыкания, что привело бы просто к экспоненциальному взрыву размера типичных Java-библиотек и, соответственного, к очень осторожному отношения к самой идеи замыкания среди тех программистов, кто привык думать о производительности. Тот факт, что ссылки на методы (MethodHandle) и динамические вызовы (invokedynamic по JSR 292) уже являются частью Java 7, дало возможность отладить функциональность и производительность этого механизма в боевых условиях.


  • 1
Роман, а как вы относитесь к новому языку Kotlin?

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

Однако, в языке Кotlin, как мне кажется, нет единой когерентной концепции, вокруг которой крутился бы его функционал. Ведь самое сложное в дизайне языка, это не решить что туда добавить, а решить что оттуда убрать. Сама по-себе идея "давайте устраним основные проблемы, с которыми сталкивается программист" звучит хорошо и работает отлично... для среды разработки, но не для языка программирования. По сравнению со сменой среды разработки, смена языка программирования, особенно в команде, это намного более трудный барьер. Много ли команд смогут его взять? Во сколько раз язык должен быть "лучше" чтобы это имело смысл?

Безусловно, есть пример языка С#, который (как и Delphi до него), развивается именно по такой модели, и, в общем, весьма успешно. Но в чем именно причина успеха языка C#? В самом языке или в поддержке Microsoft, которая за ним стоит?

А самое главное в Jigsaw, за перформанс которого я отвечаю головой -- это возможность делать install-time оптимизации (от банальной преверификации байткода до сохранения скомпилированного кода и хитрых интерпроцедурных титбитов).

И когда же будет сделано хранение скомпилированного кода? В .NET это уже сильно давно есть.

Когда перформансный бенефит от перелопачивания тонны инициализационного и JIT-ового кода перевалит за накладные расходы на такую деятельность. Поэтому либо ждать, когда появится конкретный юз-кейс, на котором это очень поможет, либо появится удобная инфраструктура, которая эту задачу облегчит (типа Jigsaw).

Мои оценки показывали, что особого бенефита от сохранения кода на больших клиентских приложениях нет. Пока, конечно, вы пишете для людей, а не для бездушных трейдерских ботов.

Всё так. Да и на меленьких консольных приложениях время в основном тоже не на компиляцию кода тратится, а на инициализации структур JVM (хотя использование прекомпилированного кода JVM и помогает). Тут тоже от Jigsaw можно ожидать косвенного выигрыша, за счет устранения лишних зависимостей в стандартных библиотеках в процессе их модуляризации.

Сама по себе модуляризация класслиба мало что даёт на стартапе. Ну загружается там не 750 классов, а 735 (не помню точные данные, но порядок такой), и весь выигрыш покрывается догрузкой собственно jigsaw'шных классов (модульные класслоадеры, то да сё). То есть, один и тот же hello world на распиленной jdk и "обычной" особенно не отличается.

Кроме того, я не открою большого секрета если скажу, что в класслоадере ещё есть чего пилить. И придётся допилить, ибо рантайм Jigsaw тоже занимает немало по времени (в текущем билде порядка 200-300 мс), а по требованиям к релизу надо вложиться в no regression при переходе Java 7 -> Java 8.

Эээ то есть в итоге нельзя будет от JVM отпилить swing/awt/апплеты/xml/corba/rmi и ускорить запуск в сто газиллионов раз? Это жалкие 750 классов так долго засасываются?

Зачем тогда модуляризация эта нужна, кроме красоты?

У меня ощущение, будто вы всерьёз полагаете, что на простом консольном "Hello, World" загружается весь класслиб. А между тем загружаются только зависимые классы, и их набирается пачка. Распиливание на модули потенциально сужает дерево зависимостей и т.о. уменьшает количество загружаемых классов... но а) на маргинальную величину; б) это покрывается самим рантаймом.

А модуляризация нужна в первую очередь для того, чтобы можно было собирать пострипанные JRE для ограниченных сценариев. В том числе, например, выбросив GUI, корбу и иже с ними. Но на стартап это не влияет вообще, максимум на скорость деплоя новой JDK и её дисковый футпринт.

Ну а приятный бонус -- это install time optimizations, позволяющие сделать какие-нибудь клёвые штуки до того как JVM попросят подняться.

Ясно, спасибо.

> У меня ощущение, будто вы всерьёз полагаете, что на простом консольном "Hello, World" загружается весь класслиб

Я на это искренне надеялся. Жаль, значит.

А не надо надеяться, надо взять и проверить ;)

$ java -verbose:class Hello | wc -l
347

$ java -verbose:class Hello | grep awt | wc -l
0

$ java -verbose:class Hello | grep swing | wc -l
0

Ага. А вот наглядная демонстрация увеличения зависимоcтей от версии к версии:

jdk1.5.0_12$ java -verbose:class Hello | wc -l
295

jdk1.6.0_20$ java -verbose:class Hello | wc -l
312

jdk1.7.0_02$ java -verbose:class Hello | wc -l
348


Поучительно и собственно изучение списка загруженных классов (а также отличий в разных версиях). Вроде как вывести "Hello, world!" на экран в любой версии не сложно, но общее усложнение платформы сказывается и на таком простом примере. Это, в общем, закон динамики любого ПО.

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

Average for jdk1.5.0_12 key over 16 runs

Elapsed Time:     0:00:00.120
Process Time:     0:00:00.059
System Calls:     2539
Context Switches: 476
Page Faults:      3501
Bytes Read:       1942303
Bytes Written:    41041
Bytes Other:      16137


Average for jdk1.6.0_20 key over 16 runs

Elapsed Time:     0:00:00.123
Process Time:     0:00:00.055
System Calls:     3367
Context Switches: 561
Page Faults:      4121
Bytes Read:       1849705
Bytes Written:    42527
Bytes Other:      17730


Average for jdk1.7.0_02 key over 16 runs

Elapsed Time:     0:00:00.069
Process Time:     0:00:00.058
System Calls:     3705
Context Switches: 473
Page Faults:      4579
Bytes Read:       2067643
Bytes Written:    49458
Bytes Other:      16854

А вот интересно, почему в 1.5 и 1.6 elapsed time был много больше process time. Что они там ожидали при загрузке? В любом случае, молодцы, что исправили это в java7.

А интересно, будет ли все-таки новый бинарный формат для модулей? Такой, чтобы все классы модуля использовали общий набор констант?

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

Общий констант-пул не то что для модуля, а для всей библиотеки сразу я исследую, ага. Здесь очень играет на руку то, что юзеры работают сразу с библиотекой, а отдельные модули устанавливают/удаляют через JDK-шного библиотекаря. On-wire формат самих модулей -- это отдельная песня, и она отличается от on-disk формата. Может статься, что будет иметь смысл иметь эксклюзивные констант-пулы для каждого из модулей, чтобы не засорять общий, но это как тесты покажут.

В качестве wire формата pack200 можно использовать, но у нас победил консерватизм, после того, как когда-то давно из-за pack200 мы запороли один из релизов (после распаковки один из классов получался битым)... осадочек остался, в общем, и тех пор гоняем jar-ы и jar-diff-ы.


Ну собственно pack200+gzip/lzma и собираемся: http://cr.openjdk.java.net/~mr/jigsaw/notes/module-file-format/

...хотя on-wire формат меня волнует в последнюю очередь. Наша работа начинается уже с инсталлированной jdk =)

Никак не могу понять, как могут помочь MethodHandle's в лямбдах, если результат должен иметь тип интерфейса?

Презентация, на которую я дал ссылку, это подробно объясняет. Если же нужно более краткое объяснение, то c помощью метода MethodHandleProxies.asInterfaceInstance, предварительно привязав необходимый контекст с помощью вызовов MethodHandle.bindTo.

Ключевое удобство в том, что для получения собственно ссылки на метод не нужно использовать рефлексию. Они размещаются прямо в пуле констант. Это радикально отличает такой подход от подхода в java.beans.EventHandler и аналогичных ему. java.beans.EventHandler был доступен уже начиная с 1.4 и в, общем, как-бы должен был помогать справиться с засилием внутренних классов для обработки событий.

Я читал презентацию, но не смог понять. С твоим объяснением все стало на много понятнее.

На счет пула констант я пока не понял. Это пул в *.class файле или в JVM? Если в *.class файле, то значит ли, что они расширили их формат, позволив размещать ссылки на методы? Я с внутренним строением .class файлов пока еще не работал.

Да. В класс-файле. В Java 7 формат был расширен, чтобы можно было размещать ссылки на методы прямо в класс-файле и загружать их на стек инструкцией ldc. Подробнее об этом в разделе 4.4.8 спецификации JVM.


  • 1
?

Log in

No account? Create an account