kotlinの良いところとモヤッとするところ

More than 1 year has passed since last update.

趣味でAndroid WearのWatch Faceを作っているのですが、せっかくなのでJavaではなくkotlinで書いてみました。以下のようなすごくシンプルなWatch Face(N88-BASICを模倣)なのですが、Javaではなく、あえてkotlinで書いたときに「お?これは!」と思った点を書いていこうと思います。

kotlin_1.png

以下は主にAndroid開発者向けです。Java8は比較対象外です。


とにかくJavaのクラスが普通に使える

文法はkotlinで書いていくのですが、既存のJavaのクラスを全く苦労することなく使えました。AndroidのクラスライブラリにあるPointクラスを使いたい場合は、

val point = Point(100, 150)

val x = point.x
val y = point.y

というように「普通に」使えます。本当に何も気にすることなかったです。

・・・と思ってましたが、特にメソッドのオーバーライドをする際に気になることがありました。後述します。


getter, setterがプロパティに見える

例えば、java.util.Calendarを使いたい場合、Javaだと以下のようなコードを書くことになります。

Calendar cal = Calendar.getInstance();

cal.setTimeZone(TimeZone.getDefault());
cal.setTimeInMillis(System.currentTimeMillis());

2つのsetterを呼んでますが、これはkotlinでは以下のように書けました。

val cal = Calendar.getInstance()

cal.timeZone = TimeZone.getDefault()
cal.timeInMillis = Systtem.currentTimeMilles()

staticなメソッドの呼び出しは、さすがにプロパティ形式で書くことはできません。


コンストラクタを簡潔に書ける

大抵のクラスはコンストラクタで値を受け取って、それをインスタンスフィールドに格納する、という処理を持ちます。Javaだと以下のような感じです。

class Foo {

int v;
Foo(int v) {
this.v = v;
}
}

kotlinだと以下のように書けました。短いですね!

class Foo(v: int)

これはプライマリコンストラクタと呼ばれています。


文字列を回せる

Javaで文字列を一文字ずつ取得していく方法は、charAt()メソッドを使って取り出していくことです。

String source = " 11 11 ";

for (int i = 0; i < source.length(); i++) {
char it = source.charAt(i);
// Do something for 'it'
}

kotlinでは、文字列をリストのように扱えました。以下のような感じです。

val source = " 11 11 "

source.forEach {
// Do something for 'it'
}

forEach()の他にも、map()も使えます。例えば、" 11 11 "という文字列から、1はtrue、他はfalseという論理値のリストに変換したいケースを考えてみます。Javaだと面倒ですね。以下のような感じだと思います。

String source = " 11 11 ";

List<Boolean> list = new ArrayList<>(source.length());
for (int i = 0; i < source.length(); i++) {
char it = source.charAt(i);
list.add(it == '1');
}

これがkotlinだと、こうなります。

val source = " 11 11 "

val list = source.map { it == '1' }

衝撃的ですよね!?


デフォがpublic、final

Javaの場合、スコープ識別子を省略すると「Package Private」が適用されますが、これに疑問を持った人は少なくないと思います。そう、kotlinではスコープ識別子を省略すると「public」の意味になります。そして、何も指定しなければ、final扱いです。

public final class Foo {

public final void bar() {
}
}

これが、kotlinだとこうなります。

class Foo {

fun bar() {
}
}

つまり、スコープについては「何も考えなければ公開」であり、オーバーライド可能かとか親クラスになれるかどうかについては「何も考えなければできない」になります。これらの初期動作に対して、いろいろな識別子を付けていくことで「制約」していくという発想になります。

Javaと同じ感覚で作っているとかなりおかしなことになりますが、kotlinの方が僕は自然に感じています。


ifが値を返せる

Javaと違い、kotlinでは"if文"ではなく"if式"です。つまり、値を返します。そう、RubyやScalaのように。3項演算子に相当するものって感じで考えても可ですね。つまり、Javaであれば、

boolean foo = ...;

drawString(canvas, foo ? "_" : " ", 0, 5, clientBounds, dotSize)

と書くところを、kotlinだと以下のように書けちゃいます。

val foo = ...

drawString(canvas, if (foo) "_" else " ", 0, 5, clientBounds, dotSize)

これは極端な例ですが、もっとシンプルに言うと、以下のような感じですね。

boolean foo = ...

val bar = if (foo) "_" else " "

if文と3項演算子を使い分けなくても大丈夫、という点では、考えることが減ったかな、と。


シングルトンオブジェクトを簡単に作れる

Javaだとシングルトンオブジェクトを作るのは結構大変(厳密なシングルトンオブジェクトを作るための方法を知ってるJavaエンジニアは少ない)なのですが、kotlinでは言語レベルでのサポートがありました。objectです。

object Fonts {

fun foo() = ...
}

使うときは、Javaでいうstaticメソッド呼び出しの感覚ですね。

Fonts.foo()

もちろん、状態を持つことも可能です。


複数値の返却と受け取り

もうこれなくしてKotlinは語れないでしょう。ある関数から複数の値を返して、それらをそれぞれ別の変数に代入することは、Javaを書いたことがある人なら夢にまで見ることだと思います。Javaだと、配列使うとか、Map使うとか、それ用のクラスを作るとか、何か別の工夫が必要で、それらの入れ物から取り出すのも自分でやる必要があります。律儀にやると、クラス数の爆増になりますね。

public class Pair<T1, T2> {

private T1 v1;
private T2 v2;
Pair(T1 v1, T2 v2) {
this.v1 = v1;
this.v2 = v2;
}
public T1 getValue1() { return v1; }
public T2 getValue2() { return v2; }
}

Pair<String, Integer> getPerson() {
return new Pair<String, Integer>("Yoichiro", 40);
}

Pair<String, Integer> result = getPerson();
String name = result.getValue1();
int age = result.getValue2();

kotlinでは、上記で自作したPairクラスが標準で提供されます。また、代入文の左辺には複数の変数を並べることが可能でした。

fun getPerson() = Pair("Yoichiro", 40)

val (name, age) = getPerson()

上記は以下のように書くことも可能です。

fun getPerson() = "Yoichiro".to(40)

val (name, age) = getPerson()

3つの値を持つTripleクラスもあります。

ちなみに、上記のように関数の定義を等号使って式として書くと、宣言的で良いですね!


Javaとの絡みでモヤモヤする点

良いことをずっと書いてきましたが、一つだけ「うーん」という点を一つあげておきます。

kotlinでは、null値に対して「実行時にではなくコンパイル時にチェックをする」という思想があります。そのため、変数や引数など何らか値を持つものについては「null値を持てるかどうか」を宣言時に必ず指定することが求められます。具体的には、"?"を付けるかどうか、です。

var foo: String // null値を持てない

var foo: String? // null値を持てる

// fooにはnull値を渡せない
fun foo(foo: String, bar: String?) = ...

つまり、上記の1行目はコンパイルエラーです。null値を持てないので、宣言時に何か値を持たせなければいけません。そして、"?"がついたもの、つまりnull値を持つことができるものを扱うときには、



  • !!.を使って、null値だった時は実行時に例外が出るようにする。


  • ?.を使って、もしnull値だった時は評価しないようにする(実行されない)。

  • if式などで事前にnullかどうかをチェックする(valで宣言されたもののみ可能)

という対処が必要になります。

kotlinの世界だけであれば、宣言時もそれを利用する時も上記のルールが適用されて厳格になるのですが、ここにJavaの世界が入ってくると話は厄介になります。特に今回、Javaのクラスを継承したkotlinのクラスを作成し、Java側のメソッドをオーバーロードした関数をkotlin側で定義する、ということをした時に、モヤッとしました。それは、引数にnullが渡ってくる可能性をどうするのか?ということです。

例えば、以下のようなJavaのクラスがあった時、

abstract class Foo {

abstract void bar(String s);
}

これをkotlin側で継承したクラスを作りたいとします。この際、bar関数の引数である"s"について、"?"を付けるかどうか、という問題が出てきます。上記の場合、Java側ではsにnullが渡ってくることを許容するかどうかは何の言及もされていないので「許容する」ことになります。そのため、kotlin側では以下のように"?"を付けることが自然だと思われます。Android Studio上でも、オーバーライドする関数を自動生成させると、"?"が付いてます。

class Foo2 : Foo() {

override fun bar(s: String?) {
...
}
}

しかし!実は以下のように"?"を付けなくても、Android Studio + kotlin Plug-inの組み合わせで「コンパイルが通ります」。警告すら出ません。ちゃんと実行も可能です。

class Foo2 : Foo() {

override fun bar(s: String) {
...
}
}

ただし、ではいざbar関数にnullが渡されてきた場合は、実行時例外が発生するようです。

コンパイルが通る、という状況は、実は好ましいとおもう状況もあります。例えば、Android向けの何かコードを書いていた場合、「明らかにnullは渡ってこない」とわかりきってるメソッドも多いかと思います。nullが渡ってきたら、それはAndroid OS側のバグだ、くらい言い切っても良いことがあります。これらに対して"?"を必ず付けないといけないとなると、!!.?.をいちいち使って値を使わないといけないっていう面倒な話になります。でも、元のJavaコードだけを見ればnull値が渡ってこない保証は何もないわけで、いくらAndroidの挙動がnullを渡さないとしても、kotlin側で必ず"?"を付けるべきって考えもできるわけです。

"?"を付けても付けなくてもコンパイルが通るのは、JetBrains社が悩んだ末の折衷案なのかな?、と勝手に思いました。

これを根本的に解決するための方法としては、KAnnotatorというPlug-inを入れて、Java側の各メソッドの各引数について、別途XMLファイルでnull値を許すかどうかを定義しておいて、kotlinのコンパイル時にそれを見て"?"が必須かどうかチェックする、という方法があるっぽいです。まだ試していませんが。。。


まとめ

長々と書いてきましたが、kotlinのコードを書くのに慣れてしまえば、Javaのうざったさ(?)からかなり解放されます。とにかくコードを書くのが「また」楽しくなると思います。ぜひチャレンジしてみてください。

(自分のブログポストをQiitaにも掲載してみました)