概要
Kotlinのちょっとしたコードを試すのに最適な[Try Kotlin] (https://try.kotlinlang.org/)というオンラインのサイトがありますが、IntelliJやEclipseなどのIDE上でソースコードを触りながらいろいろ試したいJavaプログラマー向けに、Kotlin学習用のサンプルプロジェクトを作りました。
ソースコードは[こちら] (https://github.com/rubytomato/kotlin-exercise)にあります。このプロジェクトに初歩的なコードがありますので、チェックアウトしてすぐに動作確認できるようになっています。
なお、私自身Kotlin学習中のためサンプルコードや説明に間違いがあったりKotlinの慣習に従っていない点などあるかと思いますが、そのときはご指摘いただければ助かります。
環境
- Java 1.8.0_152
- Kotlin 1.2.10
- Gradle
- IntelliJ IDEA
参考
- [Reference - Kotlin Programming Language] (https://kotlinlang.org/docs/reference/)
- [GitHub - JetBrains/kotlin-examples] (https://github.com/JetBrains/kotlin-examples/tree/master/gradle)
サンプルプロジェクト
利用する依存関係
Loggingフレームワーク
Kotlin用の[MicroUtils/kotlin-logging] (https://github.com/MicroUtils/kotlin-logging)というのもありましたが、サーバーサイドでは定番のslf4J + logbackの組み合わせを利用しました。
- [SLF4J] (https://www.slf4j.org/)
- [Logback] (https://logback.qos.ch/)
Jsonプロセッサー
Kotlin用の[cbeust/klaxon] (https://github.com/cbeust/klaxon)というものもありましたが、定番のJacksonを利用しました。
- [FasterXML/jackson] (https://github.com/FasterXML/jackson)
Httpクライアント
Kotlin/Android用のFuelというライブラリを使ってみました。
- [kittinunf/Fuel] (https://github.com/kittinunf/Fuel)
The easiest HTTP networking library for Kotlin/Android
Testingフレームワーク
こちらも定番のJUnit 4とAssertJを利用しました。
Kotlin用では[Spek] (http://spekframework.org/)というものがあるようです。
- [JUnit 4] (http://junit.org/junit4/)
- [AssertJ] (http://joel-costigliola.github.io/assertj/)
Mockサーバー
- [WireMock] (http://wiremock.org/)
Mockingフレームワーク
- [Mockito] (http://site.mockito.org/)
FatJar
実行可能なjarを生成するのにGradleのShadow Pluginを利用しています。
- [Shadow Plugin User Guide & Examples] (http://imperceptiblethoughts.com/shadow/)
> gradle build
ビルドするとbuild/libs下に実行可能なjarファイルkotlin-exercise-0.0.1-SNAPSHOT-all.jar
が生成されます。
Kotlinで利用できるライブラリを見つける
[Kotlin is Awesome!] (https://kotlin.link/)で簡単に検索できます。
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] (https://openweathermap.org/api)のサンプル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")
}
}
ユニットテスト
別記事に分けました。
用例
- [ユニットテストでファイルを扱うサンプルコード] (https://qiita.com/rubytomato@github/items/0c0aacd8b71dcd2cb813)
- [ユニットテストのassertionにAssertJを利用するサンプルコード] (https://qiita.com/rubytomato@github/items/a70395be1853848dacb2)
- [ユニットテストでMockサーバーにWireMockを利用するサンプルコード] (https://qiita.com/rubytomato@github/items/0af8179071324fa43d06)
- [ユニットテストでmocking frameworkにMockito 2.13を利用するサンプルコード] (https://qiita.com/rubytomato@github/items/525c12a6e786787784b1)
Kotlinの関数とラムダ式について
以降はKotlinの関数とラムダ式についてのおさらい的な内容になります。
関数 ([Functions] (https://kotlinlang.org/docs/reference/functions.html))
関数の定義
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] (https://kotlinlang.org/docs/reference/lambdas.html#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] (https://kotlinlang.org/docs/reference/lambdas.html#lambda-expression-syntax))
ラムダ式は関数のリテラル表現の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] (https://kotlinlang.org/docs/reference/reflection.html#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] (https://kotlinlang.org/docs/reference/extensions.html#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] (https://kotlinlang.org/docs/reference/lambdas.html#function-literals-with-receiver))
レシーバ付き関数リテラルについて、こちらの[[Kotlin]レシーバー指定ラムダとは何か] (https://dev.classmethod.jp/smartphone/android/kotlin-lambdas/)、で詳しく説明されていてとても参考になりました。
下記はラムダ式の記法でレシーバ付き関数リテラルを記述しています。ラムダ内でレシーバオブジェクトのメンバを呼び出すことができます。
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のスコープ関数を使い分けたい] (http://nyanyoni.hateblo.jp/entry/2017/08/19/152200)、で詳しく説明されていてとても参考になりました。
その他の参考にした情報
- [The difference between Kotlin’s functions: ‘let’, ‘apply’, ‘with’, ‘run’ and ‘also’] (https://medium.com/@tpolansk/the-difference-between-kotlins-functions-let-apply-with-run-and-else-ca51a4c696b8)
Kotlinの標準ライブラリに[apply] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html)、[let] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html)、[run] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/run.html)、[with] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/with.html)、[also] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/also.html)などの、一般的にスコープ関数と呼ばれる関数があります。
下記の用例で使用している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になる
}
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)
}
その他の標準関数
[Package kotlin] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/index.html)
[Functions] use
[use] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/use.html)関数は、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] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/lazy.html)関数は、ラムダ式でプロパティを遅延初期化します。
最初に参照したときにラムダ式が実行され、次回以降は最初の結果を返します。
初期化のコストが重く必ず参照されるか不定のプロパティの初期化に使用すると効果的です。
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] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/index.html)
[Functions] buildString
[buildString] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/build-string.html)は、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] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/index.html)
[Functions] onEach
[onEach] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/on-each.html)は、コレクションの各要素にラムダ式を適用し、そのコレクションを返します。Kotlin 1.1で追加されました。
ちなみにforEachは何も返しません。
inline fun <T, C : Iterable<T>> C.onEach(
action: (T) -> Unit
): C
[Package kotlin.properties] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/index.html)
[Functions] Delegates.notNull
[notNull] (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.properties/-delegates/not-null.html)関数は、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] (https://kotlinlang.org/docs/reference/properties.html#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")
}