Kotlin

Javaプログラマー向けのKotlin学習用サンプルプロジェクト

概要

Kotlinのちょっとしたコードを試すのに最適なTry Kotlinというオンラインのサイトがありますが、IntelliJやEclipseなどのIDE上でソースコードを触りながらいろいろ試したいJavaプログラマー向けに、Kotlin学習用のサンプルプロジェクトを作りました。

ソースコードはこちらにあります。このプロジェクトに初歩的なコードがありますので、チェックアウトしてすぐに動作確認できるようになっています。

なお、私自身Kotlin学習中のためサンプルコードや説明に間違いがあったりKotlinの慣習に従っていない点などあるかと思いますが、そのときはご指摘いただければ助かります。

環境

  • Java 1.8.0_152
  • Kotlin 1.2.10
  • Gradle
  • IntelliJ IDEA

参考

サンプルプロジェクト

利用する依存関係

Loggingフレームワーク

Kotlin用のMicroUtils/kotlin-loggingというのもありましたが、サーバーサイドでは定番のslf4J + logbackの組み合わせを利用しました。

Jsonプロセッサー

Kotlin用のcbeust/klaxonというものもありましたが、定番のJacksonを利用しました。

Httpクライアント

Kotlin/Android用のFuelというライブラリを使ってみました。

The easiest HTTP networking library for Kotlin/Android

Testingフレームワーク

こちらも定番のJUnit 4とAssertJを利用しました。
Kotlin用ではSpekというものがあるようです。

Mockサーバー

Mockingフレームワーク

FatJar

実行可能なjarを生成するのにGradleのShadow Pluginを利用しています。

> gradle build

ビルドするとbuild/libs下に実行可能なjarファイルkotlin-exercise-0.0.1-SNAPSHOT-all.jarが生成されます。

Kotlinで利用できるライブラリを見つける

Kotlin is Awesome!で簡単に検索できます。

build.gradle

buildscript {
    ext {
        kotlinVersion = '1.2.20'
        jacksonVersion = '2.9.3'
        shadowVersion = '2.0.2'
    }
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("com.github.jengelman.gradle.plugins:shadow:${shadowVersion}")
    }
}

apply plugin: 'kotlin'
apply plugin: 'application'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
mainClassName = 'com.example.exercise.ApplicationKt'
applicationDefaultJvmArgs = ['-Xms512m', '-Xmx512m']

apply plugin: 'com.github.johnrengelman.shadow'

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

sourceSets {
    main.java.srcDirs += 'src/main/kotlin/'
    test.java.srcDirs += 'src/test/kotlin/'
}

repositories {
    mavenCentral()
    mavenLocal()
    jcenter()
}

dependencies {
    // jackson
    compile("com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}")
    // http client
    compile("com.github.kittinunf.fuel:fuel:1.12.0") //for JVM
    compile("com.github.kittinunf.fuel:fuel-jackson:1.12.0") //for Jackson support
    // logging
    compile("ch.qos.logback:logback-classic:1.2.3")
    // Kotlin Library
    compile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
    compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
    // test
    testCompile("junit:junit:4.12")
    testCompile("org.assertj:assertj-core:3.9.0")
    // mock server
    testCompile("com.github.tomakehurst:wiremock:2.14.0")
}

kotlinでJDK7またはJDK8から追加されたAPIを利用したい場合は、kotlin-stdlibを依存関係に追加します。

kotlin 1.1.xまで

// JDK7
compile("org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.0")
or
// JDK8
compile("org.jetbrains.kotlin:kotlin-stdlib-jre8:1.1.0")

kotlin 1.1.2以降

// JDK7
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.20")
or
// JDK8
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.20")

サンプルコード

Loggingフレームワーク

Javaとほとんど同じ使い方です。私自身の経験ではLombokの@Slf4jを使って済ませることが多かったのですが、KotlinからLombokの機能を使えるようにするには設定がややこしくなりそうだったので、単純にトップレベルに下記の関数を定義しました。

fun <T> logger(clazz: Class<T>) = LoggerFactory.getLogger(clazz)!!

用例

class LoggingExercise {
    private val log = logger(LoggingExercise::class.java)

    init {
        log.debug("init log")
    }

    fun logOutput() {
        val now = LocalDateTime.now()
        log.debug("now:{}", now)
        log.info("now:{}", now)
        log.warn("now:{}", now)
        log.error("now:{}", now)
    }

    fun logOutputWithException() {
        val e = RuntimeException("Something wrong happened")
        log.error("Raise an exception", e)
    }

}

Jsonプロセッサー

Javaとほとんど同じ使い方になりました。

マッピングに使用するデータクラス

import com.example.type.Blood

data class Person(val name: String, val age: Int, val blood: Blood)
enum class Blood {
    A,
    B,
    AB,
    O
}

用例

デシリアライズ (json -> Object)

fun readFromFile() {
    val jsonFile = File("person.json")
    val mapper = jacksonObjectMapper()

    val result: Person = mapper.readValue(jsonFile)
    // 下記のように記述することもできます
    // val result = mapper.readValue<Person>(jsonFile)
    // val result = mapper.readValue(jsonFile, Person::class.java)
}

シリアライズ (Object -> json)

fun writeFromObject() {
    val jsonFile = File("person.json").also { file ->
        if (file.exists()) {
            file.delete()
        }
    }

    val person = Person("george", 50, Blood.O)
    val mapper = jacksonObjectMapper()
    mapper.writeValue(jsonFile, person)
}

Httpクライアント

この例ではWeather API - OpenWeatherMapのサンプルAPIを利用しました。

下記のデータクラスでWeather APIのレスポンスを格納します。

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty

@JsonIgnoreProperties(ignoreUnknown = true)
data class WeatherMap constructor(
    val id: Long,
    val name: String,
    val cod: Int,
    val base: String,
    val dt: Int,
    val coord: Coord,
    val weather: Array<Weather>,
    val main: `Main`,
    val wind: Wind,
    val clouds: Clouds,
    val sys: Sys
) {
    data class Coord(
        val lon: Double,
        val lat: Double)

    data class Weather(
        val id: Long,
        val main: String,
        val description: String,
        val icon: String
    )
    data class `Main`(
        val temp: Double,
        val pressure: Double,
        val humidity: Int,
        @JsonProperty("temp_min")
        val tempMin: Double,
        @JsonProperty("temp_max")
        val tempMax: Double,
        @JsonProperty("sea_level")
        val seaLevel: Double,
        @JsonProperty("grnd_level")
        val grndLevel: Double
    )
    data class Wind(
        val speed: Double,
        val deg: Int
    )
    data class Clouds(
        val all: Int
    )
    data class Sys(
        val message: Double,
        val country: String,
        val sunrise: Int,
        val sunset: Int
    )
}

用例

GitHubに載っているUsageと同じコードです。

class HttpClientExercise {

    companion object {
        private const val SAMPLE_URL: String = "http://samples.openweathermap.org/data/2.5/weather?lat=35&lon=139&appid=b6907d289e10d714a6e88b30761fae22"
    }

    fun getApiWithAsyncMode() {
        println("Async mode api call before")
        SAMPLE_URL.httpGet().responseString { _, _, result ->
            when(result) {
                is Result.Success -> {
                    val rawData = result.get()
                    rawData.let {
                        val mapper = jacksonObjectMapper()
                        val jsonData = mapper.readValue<WeatherMap>(rawData)
                        println(jsonData)
                    }
                }
                is Result.Failure -> {
                    error("api call faiure: ${result.get()}")
                }
            }
        }
        println("Async mode api call after")
    }

}

ユニットテスト

別記事に分けました。

用例

Kotlinの関数とラムダ式について

以降はKotlinの関数とラムダ式についてのおさらい的な内容になります。

関数 (Functions)

関数の定義

fun sum(x: Int, y: Int): Int { return x + y }
//  ^^^ ^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^
//   |   |                       |
//  (A) (B)                     (C)
  • A: 関数名
  • B: 関数の型
  • C: 関数の本体(Body)

関数の本体が単一式の場合は中括弧(curly braces){ }returnを省略して記述できます。

fun sum(x: Int, y: Int): Int = x + y

匿名関数 (Anonymous Functions)

匿名関数は関数のリテラル表現の1つです。

val sum = fun(x: Int, y: Int): Int { return x + y }
//  ^^^      ^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^
//   |        |                       |
//  (A)      (B)                     (C)
  • A: 変数(関数)名
  • B: 変数(関数)の型
  • C: 関数の本体(Body)

関数と同じで単一式の場合は中括弧{ }returnを省略できます。

val sum = fun(x: Int, y: Int): Int = x + y

戻り値の型が推論できるのであれば、戻り値の型も省略できます。

val sum = fun(x: Int, y: Int) = x + y

ラムダ式 (Lambda Expressions)

ラムダ式は関数のリテラル表現の1つです。

val sum = { x: Int, y: Int -> x + y }
//  ^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
//   |     |
//  (A)   (B)
  • A: 変数名
  • B: ラムダ式 (ラムダ式は常に中括弧{}で囲まれます)
    • ->記号の前にパラメータを定義します。
    • ->記号の後にラムダ式の本体を記述します。
    • 戻り値の型は指定できません。

上記のラムダ式は、型宣言を明示して記述すると下記のようになります。

val sum: (Int, Int) -> Int = { x, y -> x + y }
//  ^^^  ^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^
//   |    |                    |
//  (A)  (B)                  (C)

// もう少し省略せずに書くとこのようになります
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
  • A: 変数名
  • B: 変数の型
  • C: ラムダ式

このスタイルは関数のパラメータにラムダ式を取る場合によく使われます。下記の例ではパラメータopがラムダ式を取ります。

fun calc(x: Int, y: Int, op: (Int, Int) -> Int): Long {
    return op(x, y).toLong()
}

関数の呼び出し側でラムダ式を与えます。

val result = calc(5, 2, { x, y -> x + y })

パラメータの最後がラムダ式を取る場合、ラムダ式は括弧()の外へ出すことができます。

val result = calc(5, 2) { x, y -> x + y }

関数参照 (Function References)

定義済みの関数をパラメータとして渡したい場合は、::オペレータを使用します。

private fun numIsEvenNumber(n: Int): Boolean = n % 2 == 0
(1..10).filter(::numIsEvenNumber).forEach { even ->
    println(even)
}

関数参照の箇所はラムダ式でも記述できます。

(1..10).filter { it % 2 == 0 }.forEach { even ->
    println(even)
}

拡張関数 (Extension Functions)

Kotlinの拡張機能の1つで、既存のクラスに(クラスの継承は行わずに)新しい関数を追加しクラスのメンバとして実行することができます。

fun Int.sum(other: Int): Int { return this + other }
//  ^^^ ^^^ ^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^
//   |   |   |                  |
//  (A) (B) (C)                (D)
  • A: レシーバの型
  • B: 拡張関数名
  • C: 拡張関数の型
  • D: 拡張関数の本体(Body)
    • 関数内のthisキーワードはレシーバオブジェクトを指します。
    • レシーバオブジェクトのprivateやprotectedなメンバ、プロパティにはアクセスできません。

用例

拡張関数を定義した同一パッケージ内からはimportせずに使用できますが、パッケージ外から使用するにはimportする必要があります。

val result = 10.sum(20)

レシーバ付き関数リテラル (Function Literals with Receiver)

レシーバ付き関数リテラルについて、こちらの[Kotlin]レシーバー指定ラムダとは何か、で詳しく説明されていてとても参考になりました。

下記はラムダ式の記法でレシーバ付き関数リテラルを記述しています。ラムダ内でレシーバオブジェクトのメンバを呼び出すことができます。

val sum: Int.(Int) -> Int = { other -> this + other }
//       ^^^ ^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^
//        |   |              |
//       (A) (B)            (C)

// 上記の書き方は次のように書くこともできます
val sum: Int.(Int) -> Int = { this + it }
  • A: レシーバの型(Receiver Type)
  • B: 関数の型
  • C: ラムダ式
    • キーワードthisが指すのはレシーバオブジェクト。

下記は匿名関数の記法でレシーバ付き関数リテラルを記述しています。

val sum = fun Int.(other: Int): Int = this + other
//            ^^^ ^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^
//             |    |                  |
//            (A)  (B)                (C)
  • A: レシーバの型(Receiver Type)
  • B: 関数の型
  • C: 関数の本体(Body)
    • キーワードthisが指すのはレシーバオブジェクト。

用例

val result = 5.sum(5)

スコープ関数

スコープ関数について、こちらの Kotlinのスコープ関数を使い分けたい、で詳しく説明されていてとても参考になりました。

その他の参考にした情報

Kotlinの標準ライブラリにapplyletrunwithalsoなどの、一般的にスコープ関数と呼ばれる関数があります。

下記の用例で使用しているResultクラス

open class Result(_key: String, _name: String) {

    companion object {
        private const val ANON: String = "ANONYMOUS"
    }

    val key: String = _key.also { println("set key:$it") }
    val name: String = _name.also { println("set name:$it") }
    var amount: Int = 0
    private var oldAmount: Int = 0

    // secondary constructor
    constructor(_key: String, _amount: Int = 0): this(_key, ANON) {
        amount = _amount
    }

    fun change(newAmount: Int): Unit {
        oldAmount = amount
        amount = newAmount
    }

    override fun toString(): String {
        return "key:$key, name:$name, amount:$amount, oldAmount:$oldAmount"
    }

}

also

レシーバ自体が戻り値となります。
レシーバに任意の名前を付けることができます。明示しなければitがレシーバを指します。

inline fun <T> T.also(block: (T) -> Unit): T

用例

val result = Result("also", "Mitty").also { result ->
    // resultはレシーバ
    // println(this) // thisはインスタンス
    result.amount = 888
    result.change(777)
}

apply

also関数とほぼ同じですが、レシーバに任意の名前を付けることはできず、thisがレシーバを指します。

inline fun <T> T.apply(block: T.() -> Unit): T

用例

val result = Result("apply", "Mitty").apply {
    // thisはレシーバ
    this.amount = 999
    this.change(888)
}

let

also関数とよく似ていますが、戻り値の型を選ぶことができ最後の式が戻り値となります。なのでUnit型を返すこともできます。
また、レシーバに任意の名前を付けることができます。

inline fun <T, R> T.let(block: (T) -> R): R

用例

val result = Result("let", "Mitty").let { result ->
    // resultはレシーバ
    // println(this) //thisはインスタンス
    result.amount = 777
    result.change(666)
    result  // 戻り値はResult型
    // Unit // コメントを外すと戻り値の型はUnitになる
}

Idiomsで紹介されているlet関数の使い方として「レシーバがnullでない場合に任意の処理を実行する」というものがあります。

val strIsNullable: String? = null

try {
    //
    val strIsNotNull: String = strIsNullable?.let {
        it.toUpperCase()
    } ?: error("String must Not-NULL")
} catch (e: Exception) {
    println(e)
}

run

apply関数とよく似ていますが、最後の式が戻り値となります。

inline fun <T, R> T.run(block: T.() -> R): R

用例

val result = Result("run", "Mitty").run {
    // thisはレシーバ
    this.amount = 666
    this.change(555)
    this // 戻り値はResult型
}

run関数は2種類定義されていて、もう1つの定義はレシーバを持たず任意の型Rを返します。

inline fun <R> run(block: () -> R): R

用例

トップレベルの関数定義でrun関数を使用するとこの定義が適用されます。
最後の式が戻り値となります。
またレシーバを持たないのでthisは参照できません。

fun getNow(): LocalDateTime = run {
  LocalDateTime.now()
}
val now = getNow()

with

also、apply、let、runなどの関数とは異なり、第1引数にレシーバを取り、第2引数に第1引数のレシーバを持つ関数リテラルを取ります。
戻り値の型を選ぶことができ最後の式が戻り値となります。

inline fun <T, R> with(receiver: T, block: T.() -> R): R

用例

val result = Result("with", "Mitty")
with(result) {
    // thisはレシーバ
    this.amount = 555
    this.change(444)
}

Idiomsで紹介されているwith関数の使い方として「レシーバオブジェクトの複数のメンバを呼び出す」というものがあります。

その他の標準関数

Package kotlin

[Functions] use

use関数は、AutoCloseableインターフェースを実装したクラスをレシーバに持つ拡張関数です。Java 1.7より実装された仕様Try-with-resourcesに相当する機能でリソースを自動的に閉じてくれます。

inline fun <T : AutoCloseable?, R> T.use(block: (T) -> R): R 

用例

val textLines = listOf("first line", "2nd line", "3rd line")

val file = File("newFile,txt").apply {
    if (exists()) delete()
}
FileOutputStream(file).use { writer ->
    textLines.forEach {
        writer.write(it)
        writer.writeln()
    }
}

[Functions] lazy

lazy関数は、ラムダ式でプロパティを遅延初期化します。
最初に参照したときにラムダ式が実行され、次回以降は最初の結果を返します。
初期化のコストが重く必ず参照されるか不定のプロパティの初期化に使用すると効果的です。

fun <T> lazy(initializer: () -> T): Lazy<T>

制約

  • varには使えません
  • Nullable Typeに使えますが再代入できません。

用例

val dateTime: LocalDateTime = LocalDateTime.now()
val lazyDateTime: LocalDateTime by lazy { LocalDateTime.now() }

fun demoLazy() {
    println(LocalDateTime.now())
    // 2018-02-05T22:43:35.132

    Thread.sleep(1500)

    println(dateTime)
    // 2018-02-05T22:43:34.500
    println(lazyDateTime)
    // 2018-02-05T22:43:36.645

    Thread.sleep(1500)

    println(lazyDateTime)
    // 2018-02-05T22:43:36.645

    Thread.sleep(1500)

    println(LocalDateTime.now())
    // 2018-02-05T22:43:39.661
}

Package kotlin.text

[Functions] buildString

buildStringは、StringBuilderクラスの拡張関数です。

public inline fun buildString(builderAction: StringBuilder.() -> Unit): String

用例

val str =  buildString {
    append("first line", "\n")
    append("2nd line", "\n")
    append("3rd line", "\n")
}
println(str)

Package kotlin.collections

[Functions] onEach

onEachは、コレクションの各要素にラムダ式を適用し、そのコレクションを返します。Kotlin 1.1で追加されました。
ちなみにforEachは何も返しません。

inline fun <T, C : Iterable<T>> C.onEach(
    action: (T) -> Unit
): C

Package kotlin.properties

[Functions] Delegates.notNull

notNull関数は、lazy関数と同じようにプロパティの遅延初期化を行います。
lazy関数と違い、Not Nullで再代入可能なプロパティの初期化に使えます。
また、lateinit修飾子と同様に初期化前にアクセスすると例外をスローします。

fun <T : Any> notNull(): ReadWriteProperty<Any?, T>

制約

  • Nullable Typeには使えません
  • 初期化前にアクセスすると例外をスローする

用途

var lazyInt by Delegates.notNull<Int>()

fun demoLazy() {
    // 初期化前にアクセスすると例外をスローする
    // println(lazyInt)

    lazyInt = 100
    println(lazyInt)
}

プロパティ、変数の遅延初期化

Late-Initialized Properties and Variables

lateinit修飾子(modifier)でプロパティの初期化を遅延させることができます。

lateinit修飾子はクラス本体内のvarプロパティ、Kotlin 1.2以降ではトップレベルのプロパティ、およびローカル変数に使用できます。

制限

  • valには使えません
  • Nullable Typeには使えません
  • Primitive Typesには使えません
  • 初期化前にアクセスすると例外をスローする

用例

lateinit var lazyVar: String

// コンパイルエラー
// Property must be initialized or be abstract
// var lazyVar: String 

fun demoLazy() {
    lazyVar = "lazy variable is initialized."
    println(lazyVar)
}

初期化前にアクセスすると例外がスローされます。

Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property lazyVar has not been initialized

Kotlin 1.2以降は、lateinit varプロパティが初期化されているか確認することが可能です。

if (::lazyVar.isInitialized) {
    println("lazyVar is initialized")
}