UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

Kotlinを3ヶ月書いて感じたJavaとの違い

はじめに

  • SPEEDA PDT歴3ヶ月の相川です
  • 今回はJavaを2年くらい書いていた私が、3ヶ月間で感じたKotlinの特徴を列挙していこうと思います

Kotlinの特徴

  • 型推論
  • データクラス
  • nullable
  • 検査例外の話
  • returnを明示的に書かなくても良い
  • 拡張関数
  • リスト操作の評価について
  • kotlin corutine

型推論について

  • Kotlinでは変数を定義する際に、varもしくはvalを使います
  • その際に、型推論が採用されているおかげで、Javaのように型を宣言しなくても良くなります
  • ただ、Java10から型推論は使えるようになってるので、あまりKotlinだけに言える話ではなくなっているかと思います
// Javaで書く場合
String name = "太郎";

// Kotlinで書く場合
var name = "太郎";

データクラスについて

  • Kotlinのデータクラスは非常に綺麗です
  • Javaですと、constructorやgetterを定義する必要があり、どうしてもデータクラスの記述量が増えがちです。(もちろんlombokを使えば、アノテーションで少しは減らすことはできると思います)
  • Kotlinでは、下記のように定義するだけでインスタンスを生成できますし、Getterを定義しなくてもプロパティ名からアクセスすることができます。
//  Kotlinのデータクラスはこれだけ
data class Sample(val name: String, val num: Int)

nullable

  • Kotlinを書くようになってから圧倒的にNull Pointer Exceptionにで会う回数が減りました。(嬉しいような...寂しいような)
  • というのも、Kotlinでは何も気にせず変数にnullを代入しようとすると怒られるからです。
  • なので、nullをどうしても使いたい場合は変数名の後ろに?をつける必要があるのですが、このおかげでnullが入りうる箇所を限定的にできるということがNull Pointer Exceptionを防ぐことに繋がっているのです(余計な防御的プログラミングを減らせるというのも、メリットだと思っています)
// コンパイルエラー
val hoge: String = null

// これは大丈夫
val hoge?: String = null

検査例外の話

  • Javaでは検査例外(RuntimeExceptionとそのサブクラスを除く例外たちのこと)を投げるメソッドを呼び出す際に、try-catchするなどハンドリングしてあげる必要がありました。
  • この検査例外という仕組みは現代では嫌われる傾向があり、Java以降はほとんどの言語で採用されてないようです
  • kotlinもその流れを汲んで検査例外というものを導入していないので、Javaではtry-catchを書いていた箇所も、kotlinでは書かなくてもコンパイルエラーにはなりません。

returnを明示的に書かなくても良い

  • Kotlinでは一つの式からなる関数はreturnを省略することができます
  • Javaでは返り値のある関数は全てreturnしていたかと思いますが、=で書けるようになります
  • また、返り値の型も省略できるので、簡単な関数を書く際などには=で書き、返り値の型も省略してしまった方が個人的には読みやすいと感じました。
data class Sample(val name: String, val num: Int)

fun hoge()  = Sample("hoge", 1);

fun piyo() : String = hoge().name

拡張関数と拡張プロパティ

  • Kotlinには継承をせずとも、既存のクラスを拡張できる機能が実装されています(拡張関数と拡張プロパティ)
  • これらを使うことで、既存のクラス(継承禁止クラスを含む)に、新たに関数やプロパティを追加することができます。

拡張関数

  • まず、拡張関数を見てみます。
  • やるべきことは、拡張するクラス or インターフェースの名前の後に、追加する関数名を定義します。
  • あとは、呼び出し側で関数を呼び出してあげるだけでokです
fun main() {
 // 拡張関数の呼び出し
    println("hoge".secondChar().toUpperCase())
 
}

// 拡張関数を定義する
fun String.secondChar() = this.get(1)

拡張プロパティ

  • 続いて拡張プロパティを使用してみます。
  • やるべきことは、拡張するクラス or インターフェースの名前の後に、追加するプロパティ名を定義します。
  • 拡張プロパティの場合、デフォルトのgetterが用意されないので、常に定義してあげる必要があるので、定義します。
  • あとは、呼び出し側からプロパティ名でアクセスできるようになります。
  • また必要に応じて、setterの定義もできます(その場合は、変数宣言をvarでする必要があります)
fun main() {
 // 拡張プロパティの呼び出し
    println("fuga".thirdChar)
}

// 拡張プロパティを定義する
val String.thirdChar : Char 
    get() = get(2)
  • これらの機能が追加されてる背景としては、Kotlinのメインテーマの1つとして、既存のコードとスムーズな統合というものがあります。
  • KotlinをJavaプロジェクトに統合するときに、編集対象外の既存のコードを修正せずにKotlinの長所を利用できるようにしたかったというのがこの拡張関数・拡張プロパティが追加された動機のようです。 参考

リスト操作の評価について

  • 説明に先立ってサンプルコードから書いてしまいます
  • 今までJavaを書いていた人間からすると、リスト操作をするとなると下記のような実装になると思います
fun main() {
 val list = listOf(1, 2, 3, 4, 5)
 list.filter{ ... }.map{ ... }
}
  • この場合、KotlinのIterableに対する操作とみなされ、先行評価型となります。
  • なので、下記のような実験的なコードを実行してみると・・・
fun eagerEvaluationMethods() {
    val list = listOf(1, 2, 3, 4, 5)
    var result = list.filter {
        println("iterable.filter : $it")
        it % 2 == 0
    }.map {
        println("iterable.map : $it")
        it * 2
    }.first()
    println(result)
  • コンソールを確認すると下記のような評価結果になります
iterable.filter : 1
iterable.filter : 2
iterable.filter : 3
iterable.filter : 4
iterable.filter : 5
iterable.map : 2
iterable.map : 4
4
  • 前から順番に評価されているのがわかります
  • 先行評価の場合、まず全件に対してfilterし、次に残ったものに対してmapして、最後にfirstしてるという具合です
  • これを遅延評価に変更したい場合はiterableをsequenceにします
fun lazyEvaluationMethods() {
    val list = listOf(1, 2, 3, 4, 5)
    var result = list.asSequence().filter {
        println("sequence.filter : $it")
        it % 2 == 0
    }.map {
        println("sequence.map : $it")
        it * 2
    }.first()
    println(result)
}
  • 変更後の結果はこんな感じです
sequence.filter : 1
sequence.filter : 2
sequence.map : 2
4
  • 遅延評価になっているのがわかります
  • 1件ずつ、順番にfilter→map→firstしたら終了といった具合に評価されています
  • Javaを書いていた人にとっては、今まで遅延評価されてきたものが急に先行評価に変わるので、予期せぬ結果を招くという話になるかもしれないです
  • ですので、遅延評価なのか先行評価なのかきちんと理解してCollection操作をしてあげる必要がありそうですね 参考

coroutineの話

  • Javaでは並列実行するように実装をしようとなった場合、CompletableFutureとかParallelStreamとか他にもいくつか標準APIの選択肢があると思います
  • 一方で、Kotlinには 1.3系から正式に使えるようになったcoroutineというものがあります。その話を最後にしようと思ったのですが、2回前に原田さんがブログでまとめてくださってるのでそちらを見てもらえればと・・・ KotlinのCoroutineを用いた,外部API呼び出しの並列数を指定できるライブラリを作成した話

  • あと、詳しい説明は 公式サイトを見て頂ければと思います

まとめ

  • 今回は簡単にKotlinの特徴をまとめてしまいましたが、他にもKotlinとJavaの違いはまだまだあります。
  • この記事を読んで、少しでもKotlinに興味を持ってもらえたら嬉しいです。
  • また補足ですが、Kotlinは同一プロジェクト内でJavaファイルと共存できるので(コンパイルかけると全部classファイルになるので)、今Javaしか書いてない人は少しずつ書いてみるのも手段ですし、IntelliJ IDEAというIDEではJavaのソースコードをKotlinに変換する機能もついているので、変換してみたらどうなるのか見るだけでもイメージが湧くのかなと思います。