traP Member's Blog

javaからkotlinに乗り換えよう

takashi_trap
このエントリーをはてなブックマークに追加

traP AdventCalendar 2016の10日目の記事です。

みなさん初めまして、takashiです。私はサークルの方でtiteQuestの制作チームに所属していて、そこで開発中のゲームのステージエディタを作りました。その際に、チームメンバーのPC環境がMacとWinと混在しており、マルチプラットフォームなエディタを作ることが要求されたので初めはJavaで開発してたのですが、もともとC#をやっていた人なのでJavaのgetter/setter関数をウザがったり、そもそも言語として古いから云々など言っていたため、生産性があまりよろしくありませんでした。そこで、Javaに代わるマルチプラットフォームで動く言語を探してたところ、kotlinという言語に出会い、javaでの開発からkotlinでの開発に移行しました。今ではすっかりkotlinに魅了されてしまい、最近はもうkotlinしかやってません。他の言語忘れました。

ということで今回はこのkotlinという言語について紹介していきたいと思います。

なおこういう技術系の記事を書くのは初めてなので拙いところがあると思いますがご了承ください。

kotlinとは

kotlinはJetBrains社が開発したJVM(Java仮想マシン)上で動くいわゆるAltJavaなプログラミング言語です。kotlinの安定版言語バージョン1.0がリリースされたのはなんと今年(2016)の2月であり、既存のプログラミング言語にある様々な記法や機能をJavaに付け加えて改良し、Javaよりも安全で簡潔なコードを書くことができます。また、kotlinはJetBrains社の開発している高機能なIDEであるIntelliJ IDEAと強力に連携しており、さらにコーディング環境を向上させています。先に書いた通りJVM上で動くので†マルチプラットフォーム†な言語で、Android開発にも使うことができます。(自分はAndroid端末持ってないのでしたことはないですが)

と書きましたが、具体的にJavaとどこが違って良いのってことで以下ではJavaとの比較も交えながらkotlinの特徴を紹介していきます。

目次

kotlinにはjavaには冗長性を無くしたり、javaにはない記法や機能が数多くありますが、今回は自分が特に紹介したいものだけに絞ります。残りはkotlinのリファレンスとかを読んでね。

  1. 簡潔な記法
  2. Null安全
  3. data classと分解宣言で複数の返り値を持つ関数
  4. オブジェクト宣言(シングルトン宣言)
  5. 高階関数とラムダ
  6. インライン関数・スコープ関数
  7. 拡張関数
  8. Java言語との運用

お詫び:このサイトはkotlin言語に対応していないので、シンタックスハイライトは全く機能してません。

1.簡潔な記述

kotlinはjavaにある冗長的な記法をなるべく排除した記法を持っているので簡潔に書けます。

//ローカル変数の宣言
//kotlinは型推論が効きます。
var a = 1 //aはInt型の変数
var b = "kotlinことりん" //bはString型の変数
val c: Int = 114515 //明示的に記述することももちろん可能。cはInt型の定数。

//関数の宣言
fun kaketetasu(a: Int, b: Int, c: Int): Int { //アクセス修飾子がついてない場合ものは全てpublic
    return a * b + c
}
//式が1つだけならばもっと簡潔に書ける。
fun kaketetasu(a: Int, b: Int, c: Int) = a * b + c //返り値の型推論もされる

//クラスとそのプロパティ
class hoge(val a: Int, b: String, c: Int) { //アクセス修飾子がないのでpublic
//クラス名のすぐ後ろの括弧でプライマリーコンストラクターの引数を宣言できる。
//プライマリーコンストラクターの引数にvalまたはvarをつけるとその引数で自動的にプロパティを生成する。

    //kotlinのクラスはフィールド変数を持つことができず、全てプロパティになる
    var b: String //publicな読み書き可能なプロパティ
    var c: Int //読み込みはpublic、書き込みはprivateなプロパティ
        private set

    val d: Int
        get() = a + c //プロパティのカスタムgetter(なおdはvalで宣言されてるのでsetterはない) 


    init { //プライマリーコンストラクターの中身
        this.b = b
        this.c = c * 100
    }

}

javaで上と同等なコードは以下になります。

//型推論がないかす
int a = 1;
String b = "kotlinことりん";
final int c = 114514;

public int kaketetasu(int a, int b, int c) {
    return a * b + c;
}

public class Hoge {
    private final int a;
    private String b;
    private int c;

    public Hoge(int a, String b, int c) {
        this.a = a;
        this.b = b;
        this.c = c * 100;
    }

    //ゲッターとセッターの怒涛の宣言
    public int getA() { return a; }
    
    public String getB() { return b; }
    public void setB(String value) { b = value; }

    public int getC() { return c; }
    private void setC() { return c; }

    public int getD() { return a + c; }
 
}

気づいたかもしれないですが、kotlinには行末に”;”を打つ必要はありません。やったぜ。他にも色々記法が異なりますが、それは省略。

2.Null安全

javaで開発者を大きく悩ませるものというと、NullPointerException、ぬるぽです。大体ぬるぽはコードを実行させてみてから発生することに気づいたりするのですが、kotlinではこのぬるぽを発生させるようなコードは基本的に書けずにコンパイルエラーになります。具体的には、まず変数・定数の宣言時に型名の後ろに?をつけてないものにはnullを代入できません。すなわち同じ型に対してNull許容型と非Null型の2種類が存在し、このことによって型名に?がついていない変数・定数、すなわち非Null型は絶対にnullでないことが保証されます。

var a: String = "aiueo"

a = null //コンパイルエラー

var b: String? = "AIUEO"

b = null //OK

上のコードでいうaにはぬるぽ要素はありません。問題はbの方です。kotlinではこのbの関数やプロパティにアクセスする際には必ずbがnullでないかどうかを判定する必要があり、してない場合はコンパイルエラーです。判定の仕方はif文による判定以外に安全呼び出しというものがあり、非常に簡潔に書くことができます。

val ng = b.length //bのnull判定をしてないのでコンパイルエラー

val nullableL = b?.length //bがnullでない時はlengthを代入し、nullのときはnullを代入する。nullableLはIntのNull許容型

val notNullL = b?.length ?: -1 //elvis演算子を用いると、bがnull出ない時はlengthを代入し、
//nullの時は?:の後ろの-1を代入する。つまりnotNullLは非Null型

b?.toLowerCase() //bがnullでない時は関数toLowerCase()を呼び出し、nullの時は何もしない(正確にいうとnullを返す)

このNull安全によりnullに対して操作を行うことがないため、kotlinではまずぬるぽは発生しません。

3.data classと分解宣言で複数の返り値を持つ関数

kotlinには単にデータを保持しておくだけのクラスを作る際にdata classという便利なものがあります。

例えばPersonクラスという単に人物の情報(名前、年齢、住所、職業)を保持するクラスを考えた時、classの宣言の前にdataと付けます。

data class Person(val name: String, val age: Int, val address: String, val job: String)

dataをつけることによって何が起こるかというと、以下の関数が自動的に実装されます。

  • equals()とhashCode()
  • “Person(name=Ore, age=20, address=Tokyo, job=Student)”と出力するtoString()
  • componentN()関数
    • component1() : nameを返す関数
    • component2() : ageを返す関数
    • component3() : addressを返す関数
    • component4() : jobを返す関数
    • というふうに宣言した数だけ宣言順に返す関数ができる
  • copy() : このPersonクラスの場合はディープコピー

で、別にこれいらなくね?って思う人もいると思うんですが、このdata classのcomponentN()関数の実装により分解宣言というものが使えるようになります。

val taro = Person("Ore", 20, "Tokyo", "Student")

val (name, age, address, job) = taro //分解宣言

分解宣言はcomponent1(), component2(), … , componentN()を自動的に呼び出して代入します。つまり以下の同等のコードを非常に簡潔にしたということです。

val name = taro.component1()
val age = taro.component2()
val address = taro.component3()
val job = taro.component4()

このdata classと分解宣言を利用することで複数の返り値を持つ関数を作ることができます。

fun getPersonInfo(name: String): Person {
    //
    //〜データベースから情報を拾ってくるコードなど〜
    //
    return Person(name, _age, _address, _job)
}

val (name, age, address, job) = getPersonInfo("Jiro")

こんな感じです。

4.オブジェクト宣言(シングルトン宣言)

javaでシングルトンを作ると、以下の感じになります。

public class GameManager {
    public int stageNo = 1;
    public int score = 0;
    public int hp = 100;

    public void startGame(){
        //Initialize
    }

    public Status getPlayerStatus(){
        //プレイヤーの情報を返す
    }

    private static GameManager gameManager = new GameManager();

    private GameManager() {}

    public static GameManager getInstance(){
        return gameManager;
    }
}

なんかシングルトンの中身とは関係のないgetInstance()だとかもありますね。。。さらにこのシングルトンのgetPlayerStatus()を呼び出すには

GameManager.getInstance().getPlayerStatus()

と”余計な”文字getInstance()が要ります。

このシングルトンをkotlinではとてもスッキリ実装できます。

object GameManager {
    var stageNo :Int = 1
    var score:Int = 0
    var hp:Int = 100

    fun startGame(){
        //initialize
    }

    fun getPlayerStatus(): Status{
        //プレイヤーの情報を返す
    }
}

classと書くところをobjectにするだけです。

GameManager.getPlayerStatus()
GameManager.score += 10

みたいにシングルトンの名前だけで呼び出せます。

5.高階関数とラムダ

kotlinでは関数をパラメーターとして受け取ったり、関数を関数の返り値にすることができます。
例えば、あるListのすべての要素に対して、ある処理をしたListを返すmap関数を実装すると、下のようになります。

fun <T, R> map(list: List<T>, transform: (T) -> R): List<R> {
    val result = arrayListOf<R>() //空のArrayList<R>を生成する関数
    for (item in list) //イテレーター
        result.add(transform(item))
    return result
}

funの後の<T, R>はジェネリクスの型パラメータです。このmap関数のパラメーターのtransformに注目します。

transform: (T) -> R

この”(T) -> R”は「T型の引数を受け取り、T型の返り値を持つ関数」ということを表します。他に例えばこれが、”(Int, String) -> Unit”であれば「Int型とString型の引数を受け取り、返り値を持たない関数」というふうになります。(Unitというのはjavaでいうvoidです)
この関数は次のように使えます。

val list = listof(1, 3, 4, 8) //1,3,4,8を要素とするList<Int>を生成

val result = map(list){ value ->
    return@map value + 1
}

println(result[0]) // 2
println(result[1]) // 4
println(result[2]) // 5
println(result[3]) // 9

kotlinでは関数の最後の引数が関数を受け取るなら()の外に出すのが慣例で、{ } の中身はラムダ式となっています。ラムダ式の中のreturnはラムダ式の外のreturnと区別するために”@関数名”というラベルが付きます。
さらに、kotlinではラムダ式の引数が1つ以下なら”->”の左側は省略でき、ラムダ式の中身に条件分岐がなければreturnも省略できる(最後の行がreturnされる)ので、次のようにも書くことができます。

val result = map(list){ it + 1 } //引数はitで取得できる

javaで高階関数を実装しようと思うと、いろいろインターフェイスとかなんやら作ってやることになりますが、kotlinでは”transform: (T) -> R”のように関数の引数の型と返り値の型を指定するだけでできます。

さらにラムダ式の話をすると、kotlinのラムダ式はクロージャが効くので、javaと異なりそのラムダ式がキャプチャした外側の変数を書き換えることができます。

var count = 0
val result = map(list){
    count +=1 //ラムダ式の外側にある変数を書き換えている。
    return@map it + 1
}
println(count) // 4

6.インライン関数・スコープ関数

5で紹介した高階関数はクロージャをキャプチャしたりなんたらでオーバーヘッドが発生するのですが、関数の宣言にinline修飾子をつけることで、コンパイル時にその関数を呼び出し場所に展開することができ、このオーバーヘッドを除去できます。例えば5で挙げたmap関数を

inline fun <T, R> map(list: List<T>, transform: (T) -> R): List<R>

と書き換えると、

val result = map(list){ value ->
    return@map value + 1
}

は、コンパイル時にその高階関数を使わない場合と同等なコードに置き換わります。(簡単のため次のコードは実際の展開とは異なります)

val _result = arrayListOf<R>()
for (item in list){
    _result.add(item + 1)
}

val result = _result

map関数の中身が全部展開されて関数の呼び出しがなくなりました。

このinline展開を利用したのがスコープ関数と呼ばれるものです。
標準ライブラリにあるスコープ関数にはlet, with, run, applyの4種類がありますがwith以外の3つを簡単に紹介します。なおwith以外の3つは全ての型に対する拡張関数(7で紹介します)であるので任意の型から利用できます。この3つはkotlinを使っているとかなり使います。

let

inline fun <T, R> T.let(f: (T) -> R): R = f(this)

let関数は上で表されます。例えば以下のように主にNull許容型に対して使います。

//3.のPersonクラスを用いている
//getPerson関数は引数の名前の人物が見つからなかった場合nullを返すとする

val personA: Person? = getPerson("Jiro")

personA?.let {
    println(it.name)
    println(it.age)
    if (it.age >= 20) {
        println("セイクOK")
    }
}

もしpersonAがnullならば何も起こりません。nullでなければletの引数をpersonA(=it)とするラムダ式が実行されます。この時personAがnullでないことは分かっているので、itの型はPersonの非null型になります。要するに、null許容型を非null型に変換するやつです。letはinline関数なのでコンパイル時にletを使わない同等のコードに置き換わります。

run

inline fun <T, R> T.run(f: T.() -> R): R = this.f()

run関数は上で表されます。ここで引数の関数の引数にT.()というのが指定されていますが、これはrun関数を呼び出すインスタンスが”() -> R”の関数を呼び出すということです。例えばJavaFXでGUIアプリケーションを作るぞ〜ってなった時に、同じコントロールの関数やプロパティ何回も呼び出す時とかに次のように使います。

//JavaFXのChoiceBoxであるgradeChoiceBoxに対して
gradeChoiceBox.run {
    width = 100.0
    height = 20.0
    items.addAll("C","B","A","AA","AAA")
    value = items[0]
    valueProperty().addListener { ob, old, new -> personBExamResult.grade = new }
}

これは以下のコードと同等でgradeChoiceBoxを省略して書けるということです。

gradeChoiceBox.width = 100.0
gradeChoiceBox.height = 20.0
gradeChoiceBox.items.addAll("C","B","A","AA","AAA")
gradeChoiceBox.value = items[0]
gradeChoiceBox.valueProperty().addListener { ob, old, new -> personBExamResult.grade = new }

また、run関数は返り値をもたせれるのでこんな風にも使えます。僕はこの使い方はあまりしませんが。

val buri = "buriburiburi".run { 
    substring(2..6) //2..6というのはIntRange型のインスタンスになります。閉区間を表す。
    toUpperCase() //return toUpperCase()と同じ
}
println(buri) // RIBUR

このrun関数、かなり便利です。(書いてて気づいたんですが、run関数、ほとんどの場合で次のapply関数に置き換えられますね…)

apply

inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this }

apply関数は上で表されます。run関数との違いは、apply関数はapply関数を呼び出した自身を返すということです。このapply関数もかなり便利です。
使い方はこんな感じです。(またJavaFXです)

//JavaFXのAlertダイアログの表示
Alert(Alert.AlertType.Error).apply {
    title = "エラー"
    contentText = "無理無理無理無理無理無理"
    buttonTypes.setAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL)          
}.showAndWait()

上のようにAlertのインスタンスを入れておくローカル変数を定義すること無しに、Alertダイアログの各種設定をしてAlertダイアログの表示を行うことができます。

7.拡張関数

例えば、swingなどでImageクラスを扱っているとき、何かとBufferedImageに変換したい時がよくありますよね?(あるんです)。この時、Imageクラスから直接BufferedImageには変換できないので、javaでは次のようなコードを書くと思います。

public static BufferedImage convert(Image image){
    BufferedImage result = new BufferedImage(image.getWidth(null),image.getHeight(null),BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2 = result.createGraphics();
    g2.drawImage(image,0,0,null);
    g2.dispose();
    return result;
}

BufferedImage bufImg = convert(img);

関数名がconvertだとわかりにくいので実際は、「convertToBufferedImage()」とか、staticなUtilsクラスに入れて「ImageUtils.convertToBufferedImage()」みたいな感じにするんですが、「ImageクラスでtoBufferedImage関数を定義してくれてたらいいのに」って思いますよね。新しくImageを継承したクラスを作って関数を追加すればいいじゃんってなるかもしれませんが、でもそれってImageクラスを扱うのをやめなければいけません。。。(いちいちキャストしないといけないので)

kotlinでは、クラスを継承したりすること無しに、既存のクラスに関数を拡張することができます。

fun Image.toBufferedImage(): BufferedImage //この関数内ではImageのインスタンスがthisになる
     = BufferedImage(this.getWidth(null), this.getHeight(null), BufferedImage.TYPE_INT_ARGB).apply {
    createGraphics().run {//ついでにkotlinのスコープ関数を使って簡単に
        drawImage(this@toBufferedImage, 0, 0, null) //run内とthisとrun外のthisの指すものは異なるのでラベルで区別
        dispose()
    }
}

val bufImg = img.toBufferedImage() //Imageクラスにあたかもその関数が実装されてるかのように呼び出せる。

関数名の前に”型名.”をつけるだけであらゆる型を拡張できます。もうなんとかUtilsクラスを作ることはありません。

kotlin標準ライブラリにはJavaのいくつかのクラスに様々な拡張関数が定義されています。全てを把握してるわけではないですが、Stringの文字列操作や、Array、List、HashMap等のコレクションなどに対して数多くの拡張が最初からされています。

8.Java言語との運用

今までの紹介でjavaのライブラリを使ってたので今更なんですが、kotlinはjavaのコードと(ほぼ)100%互換性があるので、豊富にあるjavaライブラリを用いて開発することができます。また、kotlinはコンパイルするとJavaバイトコードになるので、javaからkotlinのコードを呼び出すことも可能です。
kotlinからjavaのコードを呼び出す際にはいくつかの簡略化ができます。

Getter/Setterなんてなかった

javaのコーディング規則に則ってgetterとsetter(例えばgetA()、setA(value))が実装されているならば、kotlinではそれはkotlinで言うプロパティと見なされます。すなわち「get」「set」「()」を省略できます。

//Javaでこうなら
public JavaClass {
    private int fieldA;
     
    public int getFieldA(){
        return fieldA;
    }

    public void setFieldA(int value){
        this.fieldA = value
    }
}

//kotlinではプロパティと同じように扱える。
val obj = JavaClass()
println(obj.fieldA) //Javaだと System.out.println(obj.getFieldA());
obj.fieldA = 100 //Javaだと obj.setFieldA(100);

正直javaのgetterとかsetterってgetter/setterを書く方にも呼ぶ方にもほんと邪魔ですよね

検査例外は考えなくて良い

javaはほとんどの例外(RuntimeExceptionとそのサブクラス以外)が検査例外と呼ばれ、検査例外を投げ得る関数を呼び出す際には必ずtry-catchしないといけませんが、この検査例外という仕組みは現在では嫌われる傾向があり、kotlinもその流れを汲んで検査例外というものを導入していません。つまりJavaでは必ず必要なtry-catchを、kotlinでは書かなくてもコンパイルエラーにはなりません。

SAM変換をしてくれる

これは抽象メソッドを1つしか持っていないインターフェイスを引数とする関数を呼び出す際にしてくれる変換で、本来なら次のように書かなければならないコード

//runLater関数はRunnableインターフェイスを引数とする。
Platform.runLater(object : Runnable { //object : RunnableはRunnableインターフェイスを実装した無名クラスを作る
    override fun run() {
        //something
    }
})

を、SAM変換によってラムダ式に置き換えることができます。

Platform.runLater { 
    //ラムダ式はRunnableインターフェイスのrun関数を実装している。
    //something
}

らくちん〜
ちなみにSAMってのはSingle Abstract Methodの略です。

kotlinやろう

今回は8個の項目を紹介しましたが、実はまだ紹介したいことは何個かあります。が、こういう記事を書くのが初めてでとても時間がかかってしまい、記事を前日の夜に書き始め、現在時刻(執筆終了時)は朝の4時。文字の色付けやリンク貼りもしたかったんですが、正直もう寝たいので残念ながらここまでとさせていただきます。しかし、ここまでの紹介で「kotlinでjavaの内容も書けるし、javaよりkotlinの方がモダンで生産性の高い言語だし、もはやjavaなんて要らなくね」ということが十分におわかりいただけたと思います。是非、javaのデベロッパーのみなさんはkotlinをやりましょう!

それでは、おやすみなさい。

明日はninja君、Shoma-M君のお二人がお送りいたします。

このエントリーをはてなブックマークに追加

コメントを残す

メールアドレスが公開されることはありません。