Android

Java to Kotlin のコンバート

Kotlin を使う上での色々なノウハウやマトメが投稿されているので、ひたすら失敗談について書きます。

はじめての Kotlin

言語仕様などを読んで楽しそうだと思った反面、何かを始めるには安定よりも思い切りが必要なのは新しいチャレンジにはついて回るもの。勿論、業務のプロダクションに導入するのは難しいので自分が一人でやっているアプリから初めてみよう思ったのが6月の半ばぐらい。

どうやって導入する?

導入の仕方はいくつもあり、大別すると...

  • 安全なところから初める
    • テストから始める
    • シンプルな構造のクラスだけ(RuntimeException を継承した独自の例外クラスなど)
    • 新規の機能もしくは影響範囲を限定できる独立したクラス
  • 実際の一部を書き換えてみる
    • ダイアログやポップアップなどのロジックが他の機能と独立している
    • 画面やアプリのライフサイクルの影響を受けにくい箇所
      • Pure Java で記述できる箇所が多い

とは言え「プログラミング言語なんて何とかなるだろう」と思うのと、退路を断って絶対にやり遂げるぐらいの気持ちがないと、短期間で経験値を溜めたいという考えプロジェクトの一式を丸ごとコンバートしてみることにしました。

終わりと始まり

コンバートには勿論 Convert Java File to Kotlin File を選ぶだけ。
app を選択してconvert すると迷いも後腐れもなく一気に変換されます。

スクリーンショット 2017-12-17 14.12.59.png

が、これが中々成功しない。
だいたいプロジェクトの何かが Kotlin へ移行するのを強く引き止めてきました。

ようやく本編ですが、大きく足かせになった実装や構造について触れたいと思います。
の前に、この Converter を何度も使ってみて思ったのですが Converter のロジックは1ファイルずつ単体で処理を行っており、前後のスクープ/ブロックやファイル(Class や Interface など) を横断するような参照や実装などについては、あまり見ていないようです。
コンパイルとしてリンクしてみたいな流れで言えばコンパイルに近い仕事(コードの構文を逐次解釈して一定のスコープ毎に Java コードを Kotlin コードに置き換えていく)をしているように感じます。

1. AIDL / Android インターフェース定義言語

Android では Service の実装を行う際に実装することがあります。
Music Player のようなバックグラウンドに常駐して再生処理の担うようなケースなどでは利用されると思いますが、AIDL は Kotlin にはなりません。当然です。

スクリーンショット 2017-12-17 14.28.15.png

Android SDK ツールが .aidl ファイルに基づいて Java プログラミング言語でインターフェースを生成します。

もちろん、aidl は定義だけなので java でも問題ないのですが Kotlin と Java の往復する箇所が残るのはちょっとモヤモヤが残りました。

2. synchronized

Kotlin Converter は Java の synchronized Block に出くわすとは突然の脱力からのスルーを決めてきます。初めてみたときはちょっと驚きましたが、構文解釈をするにしても前後関係の文脈を正確に理解して構造を置き換える必要があり単体のファイルだけでは判断できないことも多々あるので仕方ないのかもしれません。Converterの処理としてもsynchronized blockは特に何もせずsynchronizedのままでコードを残されてしまうので、当然コンパイルエラーになります。

Java code
スクリーンショット 2017-12-17 14.44.07.png

Kotlin code
スクリーンショット 2017-12-17 14.45.42.png

3. AsyncTask

大昔に書いたまま放置していた(存在自体をほとんど忘れていたロジック)を発掘されてしまい、その中に AsyncTask があったのですが、メモリリークするかもよ?という指摘を受けました。ちゃんとコードをメンテナンスしないとダメですねごめんなさい。

スクリーンショット 2017-12-17 15.14.03.png

4. スコープ関数への変換は行われない

実際のコードでもあるのですが、下記のようなコードがあると Kotlin 的にはスコープ関数を使うと思うのですが、実直な変換がされてしまうので、こんなコードは...

if (service != null) {
    try {
        return service.getQueueList();
    } catch (final RemoteException ignored) {
    }
}
return null;

こんな感じに。

val queueList: IntArray?
    get() {
        if (service != null) {
            try {
                return service!!.queueList
            } catch (ignored: RemoteException) {
            }
        }
        return null
    }

どうしても Converter を使うと !! を大量に使われるので一見問題のないようなコードに見えますが、もうちょっと何とかなるだろうという気持ちと、やっぱり自分(手動)で書き直せば良かったという気持ちを高めてきます。

4. 型の扱い

今まで自動的にキャストされてきた中でも Kotlin では許されないケースは多々あります。
ちゃんと書いていこう。

スクリーンショット 2017-12-17 15.59.25.png

他に int と long は Java のように自動的に変換されないので、long に int を代入するような処理でコンパイルエラーがでてしまうため初見では、かなり驚くかと思います。ビックリした。

x. エラーが多すぎると、コンパイルを諦めはじめる?

Converter の構文解析は逐次変換なので、単純な構文は正しく変換されるのですが前述のようなエラーが大量に出てくるとコード的に正しい処理だったとしても、突然諦めてしまう傾向があります。

スクリーンショット 2017-12-17 15.45.02.png

これだけでなく、さらに単純な for loop の式でも until でエラーがでる頻度でコンパイルエラーになるのでエラーの数が膨大になってメンタルに大ダメージを受けます。

終わりに

何度か繰り返し使ってみて気がついたのですが、この Converter の良い所はコードの試金石のような印象があります。エラーを修正しようと調査するたびに元の Java code の手抜きさが突きつけられるようで改めてちゃんとコードを書いていこうと思うようになりました。

  • 型や nullable などを正しく定義、処理していれば変なコードに変換される箇所は少なくなる
  • 早期 return は大事(メソッドのパラメータチェックを怠ると optional が乱用される)
  • 型変換は明示的にする
  • 整数型だからといって放置しない
    • if( long != -1 ) などはダメ
  • String.format みたいなケースは諦める
  • synchronized block は本当に必要なのか見直す
    • 本来乱用されるような記法ではないので大量にあるなら別の問題があるのでは?と疑う

Converter がきれいな変換をするためには、元々のコードが整備されているのがやはり大事だなと痛感しました。