- PVSM.RU - https://www.pvsm.ru -
К большой моей радости, мне наконец выдалась возможность поработать с популярным языком Kotlin — конвертировать простенькое приложение из Java при помощи инструмента Convert Java File to Kotlin [1] из Android Studio. Я опробовал язык и хотел бы рассказать о своем опыте.
Я быстро убедился, что этот инструмент конвертирует большую часть классов в Java практически безукоризненно. Но кое-где пришлось подчистить за ним код, и в процессе я выучил несколько новых ключевых слов!
Ниже я поделюсь своими наблюдениями. Прежде, чем мы начнем, замечу: если вы в какой-то момент захотите взглянуть, что происходит «под капотом», Android Studio позволяет отслеживать все процессы; просто перейдите в панели по следующему пути: Tools → Kotlin → Show Kotlin Bytecode.
Первое изменение я сперва даже не заметил — настолько оно было незначительным. Волшебным образом конвертер заменил константу long в одном из классов на int и преобразовывал ее обратно в long при каждом обращении. Брр!
companion object {
private val TIMER_DELAY = 3000
}
//...
handler.postDelayed({
//...
}, TIMER_DELAY.toLong())
Хорошая новость: Константа все равно распознавалась благодаря ключевому слову val.
Плохая новость: многие процессы сопровождались ненужными преобразованиями. Я ожидал, что безопасность типов в Kotlin будет на более высоком уровне, что там все будет реализовано лучше. Может быть, я переоценил, насколько умен этот конвертер?
Решение оказалось простым: нужно было просто добавить «L» в конце объявления переменой (примерно как в Java).
companion object {
private val TIMER_DELAY = 3000L
}
//...
handler.postDelayed({
//...
}, TIMER_DELAY)
Одно из главных преимуществ Kotlin — безопасность null [2], которая устраняет угрозу возникновения нулевых ссылок. Осуществляется это при помощи системы типов, которая различает ссылки, допускающие и не допускающие значение null. В большинстве случаев для вас предпочтительнее ссылки, не допускающие значения null, с которыми нет риска столкнуться с NPE (Null Pointer Exceptions). Однако в некоторых ситуациях нулевые ссылки могут быть полезны, например, при инициализации из события onClick(), такого как AsyncTask.
Существует несколько способов наладить работу с нулевыми ссылками:
Определить, на каком именно паттерне остановиться, чтобы обеспечить безопасность null — непростая задача, поэтому конвертер по умолчанию выбирает самое простое решение (третье), позволяя разработчику справляться с проблемой оптимальным для его кейса образом.
Я понимал, что разрешить коду Kotlin выбрасывать null pointer exception — это как-то идет вразрез с преимуществами, которые дает данный язык, и стал копать глубже в надежде найти решение, которое оказалось бы лучше уже имеющихся.
Так я обнаружил мощное ключевое слово lateinit. При помощи lateinit в Kotlin можно инициализировать ненулевые свойства [4] после вызова конструктора, что дает возможность вообще отойти от нулевых свойств.
Это значит, что я получаю все плюсы второго подхода без необходимости прописывать дополнительные «?.». Я просто обращаюсь с методами так, будто они в принципе не бывают нулевыми, не тратя время на шаблонные проверки и используя тот синтаксис, к которому привык.
Использование lateinit — это простой способ убрать операторы!!! из кода на Kotlin. Если вам интересны другие советы о том, как от них избавиться и сделать код аккуратнее, рекомендую пост David Vávra [5].
Так как конвертацию я проводил от класса к классу, мне стало интересно, как уже конвертированные классы будут взаимодействовать с теми, которые пока еще остаются на Java. Я читал, что Kotlin идеально совместим с Java [6], так что, по логике вещей, все должно было бы работать без видимых изменений.
У меня был публичный метод в одном фрагменте, который конвертировался в функцию internal [7] в Kotlin. На Java у него не было никаких модификаторов доступа [8], и, соответственно, он был package private.
public class ErrorFragment extends Fragment {
void setErrorContent() {
//...
}
}
Конвертер заметил отсутствие модификаторов доступа и решил, что метод должен быть видимым только в пределах модуля / пакета, применив ключевое слово internal, чтобы задать параметры видимости [9].
class ErrorFragment : Fragment() {
internal fun setErrorContent() {
//...
}
}
Что означает это новое ключевое слово? Заглянув в декомпилированный биткод мы тут же увидим, что название метода из setErrorContent() превратилось в setErrorContent$production_sources_for_module_app().
public final void setErrorContent$production_sources_for_module_app() {
//...
}
Хорошая новость: в других классах Kotlin достаточно знать исходное название метода.
mErrorFragment.setErrorContent()
Kotlin сам переведет его в сгенерированное имя. Если снова взглянуть на декомпилированный код, можно увидеть, как осуществлялся перевод.
// Accesses the ErrorFragment instance and invokes the actual method
ErrorActivity.access$getMErrorFragment$p(ErrorActivity.this)
.setErrorContent$production_sources_for_module_app();
Таким образом, Kotlin разбирается с изменениями в названиях своими силами. А как насчет остальных классов на Java?
Из Java класса вызвать метод errorFragment.setErrorContent() нельзя — ведь этого «внутреннего» метода на самом деле не существует (так как изменилось название).
Метод setErrorContent() теперь невидим для классов на Java, как можно увидеть и в API, и в окошке Intellisense в Android Studio. Так что придется использовать сгенерированное (и очень громоздкое) название метода.
Несмотря на то, что Java и Kotlin обычно взаимодействуют без проблем, при вызове классов Kotlin из классов Java могут возникнуть непредвиденные сложности с ключевым словом internal. Если вы планируете переходить на Kotlin поэтапно, имейте это в виду.
Kotlin не допускает публичных статических переменных и методов [10], которые столь типичны для Java. Вместо этого не предлагает такой концепт, как объект-компаньон [11], который отвечает за поведение статических объектов и интерфейсов в Java.
Если вы создаете константу в классе на Java [12], а затем конвертируете его в Kotlin, то конвертер не распознает, что переменная static final должна применяться как константа, что может привести к помехам в совместимости Java и Kotlin.
Когда вам нужна константа в классе Java, вы создаете переменную static final:
public class DetailsActivity extends Activity {
public static final String SHARED_ELEMENT_NAME = "hero";
public static final String MOVIE = "Movie";
//...
}
Как видите, после конвертирования, все они оказались в классе компаньона:
class DetailsActivity : Activity() {
companion object {
val SHARED_ELEMENT_NAME = "hero"
val MOVIE = "Movie"
}
//...
}
Когда их используют другие классы Kotlin, все происходит так, как и следовало ожидать:
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra(DetailsActivity.MOVIE, item)
Однако, так как Kotlin, конвертируя константу, помещает ее в собственный класс компаньона, доступ к таким константам из Java класса не интуитивен.
intent.putExtra(DetailsActivity.Companion.getMOVIE(), item)
Декомпилируя класс в Kotlin, мы можем заметить, что константы стали приватными и раскрываются через класс-оболочку компаньона.
public final class DetailsActivity extends Activity {
@NotNull
private static final String SHARED_ELEMENT_NAME = "hero";
@NotNull
private static final String MOVIE = "Movie";
public static final DetailsActivity.Companion Companion = new DetailsActivity.Companion((DefaultConstructorMarker)null);
//...
public static final class Companion {
@NotNull
public final String getSHARED_ELEMENT_NAME() {
return DetailsActivity.SHARED_ELEMENT_NAME;
}
@NotNull
public final String getMOVIE() {
return DetailsActivity.MOVIE;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
В результате код получается куда сложнее, чем хотелось бы.
Хорошая новость: частично исправить положение и добиться желаемого поведения мы можем, введя ключевое слово const [13] в класс компаньона.
class DetailsActivity : Activity() {
companion object {
const val SHARED_ELEMENT_NAME = "hero"
const val MOVIE = "Movie"
}
//...
}
Теперь, если взглянуть на декомпилированный код, мы увидим наши константы! Но увы, в конечном счете мы все равно создаем пустой класс компаньона.
public final class DetailsActivity extends Activity {
@NotNull
public static final String SHARED_ELEMENT_NAME = "hero";
@NotNull
public static final String MOVIE = "Movie";
public static final DetailsActivity.Companion Companion = new DetailsActivity.Companion((DefaultConstructorMarker)null);
//...
public static final class Companion {
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
Зато доступ из Java классов происходит по обычной схеме!
Прошу заметить, что это метод работает только для примитивов и строк [13]. Чтобы узнать больше о не-примитивах, почитайте JvmField [14] и статью Kotlin’s hidden costs [15].
По умолчанию Kotlin конвертирует циклы в диапазоне с границами 0..N-1, чем затрудняет сопровождение кода, увеличивая вероятность возникновения ошибок на единицу [16].
В моем коде, к примеру, был вложенный цикл for, чтобы добавлять карты в каждый ряд — самый обычный пример цикла for на Android.
for (int i = 0; i < NUM_ROWS; i++) {
//...
for (int j = 0; j < NUM_COLS; j++) {
//...
}
//...
}
Конвертирование прошло без особых ухищрений.
for (i in 0..NUM_ROWS - 1) {
//...
for (j in 0..NUM_COLS - 1) {
//...
}
//...
}
Код, который получается в итоге, может показаться Java разработчикам непривычным — будто его писали на Ruby или Python.
Как пишет в своем блоге Dan Lew [17], у Kotlin функция диапазона инклюзивна по умолчанию. Однако, ознакомившись с характеристиками диапазона [18] у Kotlin, я нашел их очень хорошо проработанными и гибкими. Мы можем упростить код и сделать его читабельнее, воспользовавшись возможностями, которые они предлагают.
for (i in 0 until NUM_ROWS) {
//...
for (j in 0 until NUM_COLS) {
//...
}
//...
}
Функция until делает циклы неинклюзивными и более простыми для чтения. Можно наконец выкинуть все эти нелепые -1 из головы!
Для ленивых
Иногда бывает полезно лениво загрузить переменную member. Представьте, что у вас класс типа singleton, который управляет списком данных. Каждый раз создавать этот список заново нет необходимости, так что мы зачастую обращаемся к ленивому геттеру [19]. Паттерн получается в таком духе:
public static List<Movie> getList() {
if (list == null) {
list = createMovies();
}
return list;
}
Если конвертер попытается конвертировать этот паттерн, код не скомпилируется, так как list прописан как неизменяемый, при том что у createMovies() изменяемый тип возвращаемого значения. Компилятор не позволит вернуть изменяемый объект, если сигнатура метода задает неизменяемый.
Это очень мощный паттерн для делегирования загрузки объекта, поэтому Kotlin подключает особую функцию, lazy [20], чтобы упростить загрузку ленивым способом. С ее помощью код компилируется.
val list: List<Movie> by lazy {
createMovies()
}
Так как последняя строка — это возвращаемый объект, теперь мы можем создавать объект, который требует меньше кода, чтобы лениво его загрузить!
Деструктуризация
Если вам приходилось деструктурировать массивы или объекты на javascript [21], то объявления по деструктуризации [22] покажутся вам знакомыми.
На Java мы постоянно создаем и перемещаем объекты. Однако в некоторых случаях нам нужно буквально несколько свойств объекта, и бывает жаль времени на то, чтобы извлекать их в переменные. Если же речь идет о большом количестве свойств, проще получить к ним доступ через геттер. Например, так:
final Movie movie = (Movie) getActivity()
.getIntent().getSerializableExtra(DetailsActivity.MOVIE);
// Access properties from getters
mMediaPlayerGlue.setTitle(movie.getTitle());
mMediaPlayerGlue.setArtist(movie.getDescription());
mMediaPlayerGlue.setVideoUrl(movie.getVideoUrl());
Kotlin, однако, предлагает мощное объявление деструктора, которое упрощает процесс извлечения свойств объекта, сокращая объем кода, необходимый, чтобы закрепить за каждым свойством отдельную переменную.
val (_, title, description, _, _, videoUrl) = activity
.intent.getSerializableExtra(DetailsActivity.MOVIE) as Movie
// Access properties via variables
mMediaPlayerGlue.setTitle(title)
mMediaPlayerGlue.setArtist(description)
mMediaPlayerGlue.setVideoUrl(videoUrl)
Не приходится удивляться, что в декомпилированном коде методы у нас ссылаются на геттеры в классах данных.
Serializable var10000 = this.getActivity().getIntent().getSerializableExtra("Movie");
Movie var5 = (Movie)var10000;
String title = var5.component2();
String description = var5.component3();
String videoUrl = var5.component6();
Конвертер оказался достаточно умным, чтобы упростить код путем деструктуризации объекта. Тем не менее, я бы посоветовал почитать про лямбды и деструктуризацию [23]. В Java 8 существует распространенная практика заключать параметры лямбда-функции в скобки, если их больше одного, но в Kotlin это может быть интерпретировано как деструктуризация.
Использование инструмента для конвертирования в Android Studio стало для меня отличным первым шагом в освоении Kotlin. Но, проглядев некоторые участки полученного кода, я был вынужден начать глубже вникать в этот язык, чтобы найти более эффективные способы писать на нем.
Хорошо, что меня предупредили: после конвертации код нужно обязательно вычитать. Иначе на Kotlin у меня получилось бы нечто маловразумительное! Хотя, честно говоря, у меня и на Java с этим не лучше.
Если вы хотите узнать другую полезную информацию о Kotlin для начинающих, советую прочитать этот пост [24] и посмотреть видео [25].
Автор: nanton
Источник [26]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/259912
Ссылки в тексте:
[1] Convert Java File to Kotlin: https://developer.android.com/kotlin/get-started.html#convert-to-kotlin-code
[2] безопасность null: https://kotlinlang.org/docs/reference/null-safety.html
[3] Safe Call Operator: https://kotlinlang.org/docs/reference/null-safety.html#safe-calls
[4] инициализировать ненулевые свойства: https://kotlinlang.org/docs/reference/properties.html#late-initialized-properties
[5] пост David Vávra: https://android.jlelse.eu/how-to-remove-all-from-your-kotlin-code-87dc2c9767fb?gi=26743d7b8c09
[6] Kotlin идеально совместим с Java: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html
[7] internal: https://kotlinlang.org/docs/reference/visibility-modifiers.html
[8] модификаторов доступа: https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html
[9] параметры видимости: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#visibility
[10] не допускает публичных статических переменных и методов: https://kotlinlang.org/docs/reference/classes.html#companion-objects
[11] объект-компаньон: https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects
[12] в классе на Java: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#static-fields
[13] const: https://kotlinlang.org/docs/reference/properties.html#compile-time-constants
[14] JvmField: https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#instance-fields
[15] Kotlin’s hidden costs: https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
[16] ошибок на единицу: https://en.wikipedia.org/wiki/Off-by-one_error
[17] Dan Lew: http://blog.danlew.net/2017/06/05/musings-on-kotlin-ranges/
[18] характеристиками диапазона: https://kotlinlang.org/docs/reference/ranges.html
[19] ленивому геттеру: https://en.wikipedia.org/wiki/Lazy_initialization#Java
[20] lazy: https://kotlinlang.org/docs/reference/delegated-properties.html#lazy
[21] javascript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
[22] объявления по деструктуризации: https://kotlinlang.org/docs/reference/multi-declarations.html
[23] про лямбды и деструктуризацию: https://kotlinlang.org/docs/reference/multi-declarations.html#destructuring-in-lambdas-since-11
[24] этот пост: https://developer.android.com/kotlin/get-started.html
[25] видео: https://www.youtube.com/watch?v=czKo-jPVweg
[26] Источник: https://habrahabr.ru/post/332598/
Нажмите здесь для печати.