Приветствую, Хабр! Предлагаю вашему вниманию небольшую пятничную статью про Java, Scala, ненормальных программистов и нарушенные обещания. Простые наблюдения иногда приводят к не очень простым вопросам. Вот, к примеру, простой и внешне, пожалуй, даже тривиальный факт, гласящий, что в Java можно расширять любой не-final класс и любой интерфейс в области видимости. И другой, тоже достаточно простой, гласящий, что Scala-код, скомпилированный для JVM, может использоваться из Java-кода. Сочетание этих двух фактов, однако, заставило меня задаться вопросом: а как поведёт себя с точки зрения Java какой-нибудь класс, который в Scala является sealed, т.е. не может быть расширен внешним относительно его собственного файла кодом? Декомпилированный Scala-класс в представлении художника. Источник: https://specmahina.ru/wp-content/uploads/2018/08/razobrannaya-benzopila.jpg В качестве подопытного кролика я взял стандартный класс Option. Скормив его декомпилятору, встроенному в IntelliJ Idea, получаем примерно следующее: Декомпилированный код, правда, не будет являться валидным Java-кодом — проблема, аналогичная описанной здесь, к примеру, вот с таким методом: где свойство MODULE$ соответствует константе, объявленной в package object. Тем не менее, это проблема декомпилятора, использовать соответствующую скомпилированную библиотеку из Java мы сможем спокойно, верно? В качестве эксперимента создадим отдельный проект на Java (со сборкой через Maven), который будет использовать Scala как provided-библиотеку — то есть, строго говоря, вообще не использовать, а только ссылаться на экспортируемые из неё типы: И попробуем создать класс, унаследованный от scala.Option. Idea, разумеется, знает про Scala и сразу скажет, что нам не стоит наследоваться от sealed-класса, но, тем не менее, послушно сгенерирует все необходимые методы: Последние три метода нам нужны из-за того, что Option реализует Product. Собственно, мы даже не будем тут ничего менять — всё и так неплохо получилось. Запускаем mvn package — получаем крохотный, на три килобайта, jar-ник, и это значит, что Java, как и следовало ожидать, проглотила наш код, даже не заметив потенциальной подставы. Посмотрим теперь, что с таким инструментом можно сделать в Scala. Самый простой способ использовать подобную зависимость из Scala (вернее, из SBT) — положить её в папку lib внутри проекта, что мы, собственно, и сделали; build.sbt остаётся таким, каким его сгенерировала Idea. Начинаем писать основной (и единственный) класс нашего приложения: Здесь я использовал implicit var как способ избежать повторяющегося кода при вызове будущих экспериментов. tryDo — небольшая вспомогательная функция, чьё назначение достаточно прямолинейно: вывести в консоль либо значение переданного выражения, либо ошибку, возникшую при его вычислении. За счёт синтаксиса call-by-name мы можем передавать в tryDo не лямбду, а само выражение, чем и воспользуемся ниже. Для начала, попробуем просто сделать match — самую простую операцию, какую только можно сделать с sealed class-ом (мы же знаем, что у нас есть объект sealed-класса, верно?) Результат: Вполне ожидаемый исход, но обратите внимание: так как Option — это sealed class, если бы мы опустили один из исходов (оставили только case Some или только case None), компилятор бы честно сгенерировал предупреждение: Здесь же он нас ни о чём предупредить не смог, и код развалился в рантайме. Окей, давайте теперь посмотрим на какой-нибудь случай с применением стандартных методов Option: Результат: Суть происходящего становится понятной, если посмотреть на реализацию метода map: То есть, логика действий такая: Неслабо так для языка, в котором в нормальных условиях для получения NPE надо либо специально постараться, либо использовать значения из Java без проверки? (Впрочем, строго говоря, сейчас именно этот последний факт и имел место, да...) Ну и напоследок добавим ещё один пример: В языке с более серьёзной системой типов (точнее, с системой типов, более серьёзно относящейся к null) такой код мог бы просто не скомпилироваться, если бы метод contains требовал передавать в него значение не-Nullable типа. В данном случае код компилируется, но, очевидно, с обычным Option он бы выдал false — содержащееся в нём значение никогда не равняется null. Что же в нашем случае? Результат: Что, в свете сказанного выше, вполне понятно, поскольку реализация метода contains абсолютно аналогична map: !isEmpty && this.get == elem. Разумеется, в реальных условиях подобное можно сотворить только специально. Я ни в коем случае не призываю быть параноидальными, проверять на null всё, что получили из Option (в конце концов, этот класс для того и создан, чтобы таких проверок было поменьше) и вставлять ветки else во все подряд блоки match. По сути, всё, для чего нужна была эта статья, — небольшой эксперимент по раскрытию одного нюанса взаимодействия разных языков на одной JVM. Нюанса, при небольшом размышлении, очевидного, но — на мой вкус, всё-таки интересного. Разработчик, экспериментатор