Появление Kotlin – это важный бонус для разработчиков. Высокоуровневый язык, бесшовно интегрирующийся с Java, значительно расширяет возможности программистов. Однако в любом языке мы постоянно сталкиваемся с некоторыми неприятностями, которые, напротив, создают ограничения, и Kotlin, конечно, не стал исключением. О них мы и поговорим сегодня.
Kotlin был с воодушевлением воспринят сообществом, так как новый язык программирования значительно упрощает написание кода, заменяя собой Java. Особенно это заметно на Android, где новые версии Java появляются, мягко говоря, «со скрипом». Многие аппараты обновляются с большими задержками, ещё больше – вообще не получают обновления прошивки. В лучшем случае поддержка аппарата заканчивается примерно через 2 года после его выпуска, что сулит одно, максимум — два обновления системы. Однако люди продолжают пользоваться устройствами, а значит разработчикам приходится ориентироваться как на (пока ещё) новейший Android 8, так и на Android 5, а то и более ранние версии, где уж точно нет не только самой последней, но даже актуальной реализации Java. Для справки, сейчас суммарная доля Android 7 и 8 — тех версий, где есть поддержка Java 8 со стороны системных библиотек — составляет 42,9%, всё остальное — Java 7.
Кто-то может говорить, что Java устарела. Но есть также и мнение, что Java – это язык, который хорош «as is», и его не нужно трогать. Это как НОД (наибольший общий делитель) в математике. Он содержит джентльменский набор функций, а для тех, кому нужны дополнительные возможности сегодня доступны специальные языки, работающие уже поверх JVM. Этих языков уже расплодилось довольно много: Scala, Clojure, JRuby, Jython, Groovy и прочие, менее известные. Однако Kotlin отличается рядом плюсов. Этот язык оставляет больше свободы программисту: он позволяет одновременно использовать готовые фрагменты на Java, объединяя старый и новый код.
Но при всех преимуществах нового языка, которые мы ни в коей мере не оспариваем, в процессе разработки в нём обнаружились некоторые минусы. И сегодня интересно было бы услышать мнение коллег о том, мешают ли они работать так же, как и нам.
Скрыть нельзя показать?
Пакеты, как водится, представляют собой довольно распространённый способ организовать классы по пространству имён. Но их плюс не только в этом, в Java они ещё и выступают средством для ограничения видимости классов и их членов.
Напомню, что в Java есть 4 разных категории, которые позволяют разграничить видимость. Для классов их всего два – они видны либо в рамках пакета (package private), либо полностью открыты (public). Но метод или поле уже можно сделать private (он будет недоступен вне класса), видимым только для пакета (package private), можно сделать так, чтобы метод был виден наследникам, как в своём пакете, так и вне пакета (protected), а также можно его сделать видимым для всех (public).
В Java 9 появилась возможность разбивать код на модули, и теперь есть возможность сделать некоторую часть кода видимой везде внутри модуля, но не извне. И это оказывается очень полезно для выстраивания толкового API.
Одним словом, опций в Java — более чем достаточно. А вот в Kotlin почему-то ограничились тем, что ввели публичную и приватную видимость, а также видимость для наследников. Кроме этого, правда, они ввели разграничение по модулям, но разграничить доступ к методам и классам по пакетам стало нельзя. Модуль — это уже не конструкция самого языка, и частенько люди понимают под этим термином довольно-таки разные вещи. В Kotlin официально определяют модуль как «набор скомпилированных вместе Kotlin-файлов».
Загвоздка состоит в том, что модуль, как правило, приводит к дополнительным затратам ресурсов. При этом, если создать какой-то отдельный пакет и поместить в него классы с функциями, которые видны только в этом пакете, проблем не возникает, потому что все пакеты будут компилироваться вместе и никаких дополнительных ресурсов не потребуются.
Особенно ярко это проявляется, если, например, собирать проекты в Gradle, как обычно и собираются приложения на Android. Модули, как правило, делают относительно большими, чтобы они представляли собой завершенную функциональную единицу. В рамках одного модуля метод нельзя сделать видимым одним и не видимым другим классам. И если мы хотим сделать видимость методов более гранулярной, возникает проблема.
Тут сразу хочется вспомнить про пакеты, ведь в Kotlin эта сущность никуда не делась, но на видимость она, увы, не влияет. Конечно, всегда можно наделать больше модулей, но, учитывая особенности Gradle, это просто нерационально: скорость сборки будет снижаться. Да, есть возможность засовывать классы в один файл, но в случае с большим проектом файл станет действительно «тяжёлым». Поэтому хотелось бы получить какой-то другой способ сокрытия методов, например, по образцу Java.
Не обрабатывай это… даже если и надо бы
Второй — довольно спорный (потому что некоторые считают их использование моветоном), но тем не менее минус – отсутствие проверяемых исключений. Эти средства есть в Java, но в Kotlin решили их не реализовывать. В официальной документации есть пример про интерфейс Appendable. К Appendable можно присоединять строки, и поскольку строки могут подсоединяться к объектам, связанным с вводом/выводом, например, при записи в файла или в сеть, при вызове методов интерфейса потенциально можно получить IOException. И такие случаи делают использование проверяемых исключений неудобным.
Объясняют свою аргументацию создатели языка следующим образом: если мы используем StringBuilder, обращаясь к нему через Appendable, нам, оказывается, нужно обязательно обрабатывать IOException, даже если вы уверены, что оно в принципе не может произойти:
Appendable log = new StringBuilder();
try {
log.append(message);
} catch (IOException e) {
// Must be safe
}
Выход из ситуации: исключение «ловят», но ничего с ним не делают, что, разумеется, не есть хорошо. Однако возникает вопрос: если мы заведомо работаем со StringBuilder через интерфейс Appendable – почему не взаимодействовать напрямую со StringBuilder? Тут есть важная разница: при работе с Appendable, если мы не знаем, какая под ним лежит конкретная реализация, исключение ввода/вывода становится действительно возможным, но вот StringBuilder точно его не выдаст, и соответствующие методы в нём так и объявлены, хотя они и реализуют интерфейс. Так что пример получается довольно натянутый…
Особенно интересно, что авторы документации ссылаются на главу 77 в Effective Java, где говорится, что исключения нельзя ловить и игнорировать. А в соседних главах пишется, что проверяемые исключения использовать можно и нужно, если делать это с умом. Так что обилие позиций дает широкий выбор, что цитировать, оправдывая ту или иную точку зрения.
В результате разработчики Kotlin сделали так, что в сущности, все методы потенциально могут выдать какое-то исключение. Но тогда где же средства для обработки ошибок в новом языке? Как теперь понять, где может появиться исключение? На уровне языка, увы, мы не находим помощи в этих вопросах, и даже при наличии исходного кода (который ещё и далеко не всегда есть без учёта разных ухищрений), сложно понять, что делать в каждой конкретной ситуации.
Если обратиться с вопросом к разработчикам, они просто разводят руками и говорят, что в других языках нет проверяемых исключений и ничего, на них тоже создаются большие и надёжные программы. Но они и на откровенно неудачных языках успешно пишутся, это ведь не аргумент. На StackOverflow на подобный вопрос говорят, что «нужно читать документацию» — хороший ответ, очень удобный в подобных ситуациях. Только вот документации может не быть, она может быть неполной или устаревшей — компилятор же её не проверяет. Наконец, в ней можно так и не найти ответ.
Да, это правда, что наличие проверяемых исключений может приводить к созданию неудобных API. Когда проверка на исключения — заведомо излишняя, чтобы компилятор «был доволен», приходится прописывать try...catch и фактически просто создавать мусорный код, ведь при обработке этих исключений не делается ничего. И то, что в функциональном коде неудобно их использовать — тоже правда. Но ведь проверяемые исключения — это лишь инструмент, который точно так же можно использовать грамотно, там, где это нужно.
Непонятно, почему язык забрал эту возможность, если философия Kotlin заключается в доверии большего количества инструментов программисту (по крайней мере, нам так показалось), и если автор кода может грамотно расписать, где какие исключения будут, почему бы не поверить ему? Взять те же перегружаемые операторы, которые убрали из Java, потому что они могут привести к появлению API с неочевидными действиями — это была защита программистов от самих себя. В Kotlin, напротив, есть возможность перегружать операторы и делать многие другие вещи – так почему же нет проверяемых исключений? Уж не сделали ли это создатели из-за того, чтобы просто было не как в Java?
Мы ждём перемен…
Максимум, на что мы можем рассчитывать под Android – это Java 7 или Java 8 (но уже с некоторыми ограничениями и оговорками), в то время как на подходе уже Java 11. С использованием Kotlin программирование на Android становится намного проще, сокращается количество строк текста.
Остаётся лишь надеяться, что разработчики дополнят этот очень полезный язык теми возможностями, которые по неочевидным причинам отсутствуют сегодня. Может быть, в следующих версиях появятся новые инструменты для анализа исключений в IDE, а также новые категории приватности. Но, судя по всему, это дело в лучшем случае отдалённого будущего, так как никаких обещаний разработчики языка пока даже не озвучивали.
Автор: Александр Газаров