Kotlin
目次
主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。
- 概要
- 1. Kotlinとは何か・なぜ生まれたか
- 2. 環境構築とツールチェイン
- 3. 基本構文と型システム
- 4. null安全性
- 5. クラスとオブジェクト
- 6. 関数とラムダ
- 7. プロパティとフィールド
- 8. 制御フローとパターンマッチング
- 9. コレクションとSequence
- 10. 拡張関数とスコープ関数
- 11. ジェネリクスと型パラメータ
- 12. data class / sealed class / value class
- 13. オブジェクト式とobject宣言
- 14. デリゲーション
- 15. 例外処理
- 16. coroutines(コルーチン)
- 17. Flow
- 18. Javaとの相互運用
- 19. Kotlin Multiplatform
- 20. Android開発
- 21. Ktor / Spring Boot Kotlin
- 22. テスト戦略
- 23. メタプログラミング・KSP・コンパイラプラグイン
- 24. パフォーマンスチューニング
- 25. Kotlin 1.x〜2.0の進化
- 26. よくある落とし穴FAQ
- 27. 実践パターン集
- 28. 学習ロードマップ(30日)
- 29. 用語集
- 30. Kotlin学習の次のステップ
- 発展: Kotlinらしい設計
- 実践: Androidとサーバサイド
- 応用: ComposeとKMP
- Android での Kotlin 活用
- Kotlin学習の継続
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
Kotlinは、JetBrainsが開発したJVM互換のモダンな静的型付け言語で、Javaの冗長性を解消しつつ完全な互換性とnull安全性、コルーチン、マルチプラットフォーム対応を実現します。
このページでは、null安全、データクラス、scope functions、coroutines、Flow、Kotlin Multiplatform、Android開発、Java互換性を、Kotlinらしい設計思想とともに整理します。
1. Kotlinとは何か・なぜ生まれたか
このセクションでは「Kotlinがなぜ生まれたのか」「何を解決するのか」「Javaとの関係はどうなっているのか」を丁寧に説明します。最初にここをしっかり理解しておくと、Kotlinの設計判断が腑に落ちやすくなります。
Kotlinは **「JVM上で動く、Java互換のモダンな静的型付け言語」**です。
Kotlin = Javaの冗長性を解消 + null安全性 + 関数型機能 + コルーチン + Multiplatform
KotlinのコードはコンパイルされるとJVMバイトコード(あるいはJavaScript / ネイティブ機械語 / WASM)になります。Javaから呼ぶことも、Javaを呼ぶこともできる完全な相互運用性を持ちます。
1-1. Kotlinが生まれた背景
Javaの限界とJetBrainsの課題
JetBrainsはIntelliJ IDEA、PyCharm、WebStormなど、世界トップクラスのIDEを開発する会社です。これらのIDEはすべてJavaで書かれていました。2010年頃、JetBrainsは数百万行に及ぶJavaコードベースを抱え、次のような問題に直面していました。
- 冗長性: getter/setter、nullチェック、無名クラスなどboilerplateが多い
- null pointer exception: コンパイル時に検出できない
- 新機能の遅さ: Javaの進化が遅く、ラムダ式(Java 8、2014年)も長く待たされた
- 不変性が二級市民:
finalキーワードを書かないとデフォルトで可変 - 拡張性のなさ: 既存クラスに機能を追加できない(拡張関数がない)
既存の選択肢の不満
JetBrainsは既存のJVM言語を検討しました。
- Scala: 機能が豊富だが、コンパイルが遅く、学習曲線が急
- Groovy: 動的型付けで、IDE支援が弱い
- Clojure: Lispで、既存のJava開発者には馴染みにくい
「Javaと完全に互換性を保ちつつ、Javaの問題を解決する」という方向で、Kotlinの開発が始まりました。
Kotlinの誕生(2011〜2016)
- 2011年7月: JetBrainsがProject Kotlinを発表
- 2012年2月: オープンソース化(Apache License 2.0)
- 2016年2月: Kotlin 1.0リリース
- 2017年5月: GoogleがAndroid開発の公式言語として採用(Google I/O 2017)
- 2019年5月: Googleが「Kotlin First」を宣言
- 2023年11月: Kotlin 2.0ベータ(K2コンパイラ)
- 2024年5月: Kotlin 2.0正式リリース
「Kotlin」という名前は、ロシア・サンクトペテルブルク近郊の コトリン島 に由来します。Java(インドネシアのジャワ島)に対抗する命名です。
1-2. Kotlinの設計目標
Kotlinの公式な目標は次の4つです。
- Pragmatic(実用的): 業務で使える機能に絞る、研究言語ではない
- Concise(簡潔): Javaのboilerplateを減らす
- Safe(安全): null安全、型安全、不変性をデフォルトに
- Interoperable(相互運用): Javaと完全互換
これらがKotlinのすべての設計判断の根底にあります。
1-3. なぜJavaではなくKotlinか
JavaとKotlinは 完全に相互運用 できるので、技術的には共存可能です。それでもKotlinが選ばれる理由は次の通りです。
| 観点 | Java | Kotlin |
|---|---|---|
| null安全 | 言語レベル支援なし(@Nullableは注釈のみ) | 型システムで強制 |
| データクラス | record(Java 14+) | data class(豊富な機能) |
| 拡張関数 | なし | あり |
| プロパティ | getter/setterを書く | 自動生成 |
| ラムダ | OK(Java 8+) | より簡潔、receiver付き可 |
| 不変性 | デフォルトでミュータブル | valでデフォルト不変 |
| coroutine | Virtual Threads(Java 21+) | coroutines(標準) |
| Multiplatform | JVMのみ | JVM/JS/Native/WASM |
Java 21でVirtual Threadsやrecord、pattern matchingが入って差は縮まりましたが、Kotlinの 言語全体の一貫性と簡潔さ は依然として強力です。
1-4. Kotlinが動く場所
Kotlinは マルチプラットフォーム 言語です。
Kotlin/JVM JVMバイトコードへ。Javaとの相互運用、Android、サーバサイド
Kotlin/JS JavaScriptへ。フロントエンド、Node.js
Kotlin/Native LLVM経由でネイティブ。iOS、Linux、Windows、macOS、組み込み
Kotlin/WASM WebAssemblyへ(Alpha)
「1つの言語であらゆる場所」がKotlin Multiplatform(KMP)の理念です。
1-5. このセクションのまとめ
- 2011年JetBrainsがJavaの課題を解決するために開発開始
- 2016年1.0リリース、2017年GoogleがAndroid公式採用
- 設計目標: Pragmatic / Concise / Safe / Interoperable
- Javaと完全互換、ただし冗長性・null問題・拡張性などを大幅改善
- JVM / JS / Native / WASMのMultiplatform
2. 環境構築とツールチェイン
このセクションでは、Kotlinプロジェクトを作る・ビルドする・実行するまでの実用フローを整理します。
2-1. インストール方法
IntelliJ IDEA(推奨)
JetBrains製のIDE。Community版(無料)でも完全なKotlinサポートがあります。プロジェクト作成からビルド・実行まですべてGUIで完結。
コマンドラインツール
# SDKMAN(推奨)
curl -s "https://get.sdkman.io" | bash
sdk install kotlin
# Homebrew (macOS)
brew install kotlin
# 確認
kotlinc -version
kotlin -version
Kotlin Playground
ブラウザで動くREPL: play.kotlinlang.org
学習や軽いコード共有に便利。
2-2. プロジェクト構成(Gradle Kotlin DSL)
my-project/
├── build.gradle.kts # ビルド設定(Kotlin DSL)
├── settings.gradle.kts
├── gradle.properties
├── gradlew # Gradle Wrapper
├── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── com/example/
│ │ │ └── Main.kt
│ │ └── resources/
│ └── test/
│ └── kotlin/
└── build/ # 生成物
build.gradle.ktsの最小例
plugins {
kotlin("jvm") version "2.0.0"
application
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.MainKt")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}
2-3. 主要コマンド
# Gradle Wrapper(プロジェクト固有のバージョン)
./gradlew build # ビルド
./gradlew run # 実行
./gradlew test # テスト
./gradlew clean
./gradlew :app:assembleDebug # Android
# Kotlinコンパイラ直接
kotlinc Main.kt -include-runtime -d main.jar
java -jar main.jar
# REPL
kotlinc
# スクリプト実行(.kts)
kotlin script.main.kts
2-4. K2コンパイラ(Kotlin 2.0+)
Kotlin 2.0で K2コンパイラ がデフォルトに。従来のK1比で コンパイルが約2倍速く、エラーメッセージも改善されています。
// build.gradle.kts
kotlin {
compilerOptions {
languageVersion.set(KotlinVersion.KOTLIN_2_0)
}
}
2-5. このセクションのまとめ
- IntelliJ IDEA Community版で十分
- Gradle + Kotlin DSL(build.gradle.kts)が標準
- ./gradlew build / run / test
- Kotlin 2.0でK2コンパイラがデフォルト
3. 基本構文と型システム
3-1. Hello World
fun main() {
println("Hello, World!")
}
// コマンドライン引数を受け取る場合
fun main(args: Array<String>) {
println("Hello, ${args.joinToString()}!")
}
ファイル名は Main.kt、JVM上では MainKt クラスとしてコンパイルされます。
3-2. 変数:valとvar
val x = 10 // 不変(再代入不可)、推奨
var y = 20 // 可変
y = 30 // OK
val z: Int = 100 // 型を明示
val pi: Double = 3.14
x = 100 // エラー!valは再代入不可
val をデフォルトで使うのがKotlinの文化。Javaの final を毎回書かなくてよいだけで、不変性が当たり前になります。
3-3. プリミティブ型(実は参照型)
| 型 | 説明 | サイズ |
|---|---|---|
Byte |
符号付き8bit | 1 |
Short |
16bit | 2 |
Int |
32bit | 4 |
Long |
64bit | 8 |
Float |
単精度 | 4 |
Double |
倍精度 | 8 |
Boolean |
true / false | 1(実装依存) |
Char |
16bit Unicode | 2 |
String |
文字列(不変) | 参照 |
Kotlinでは すべての型がクラスですが、コンパイラがJavaのプリミティブにマッピングするので性能ペナルティはほぼゼロです。
val n: Int = 42
n.toString() // メソッド呼び出しできる
n.coerceAtLeast(0) // Intにメソッドが生えている
3-4. 暗黙の型変換は禁止
Kotlinは 暗黙の数値型変換を禁止します。
val n: Int = 10
val l: Long = n // エラー!型が違う
val l: Long = n.toLong() // 明示的変換が必要
これは「バグの温床」だったJavaの挙動を意図的に修正したもの。
3-5. 文字列とテンプレート
val name = "Alice"
val age = 30
// 文字列テンプレート
val msg1 = "Hello, $name!"
val msg2 = "Hello, ${name.uppercase()}!"
// 複数行(raw string)
val text = """
SELECT *
FROM users
WHERE id = $id
""".trimIndent()
$variable または ${expression} で埋め込み。Pythonのf-stringやRubyの #{} に相当。
3-6. このセクションのまとめ
- val(不変)をデフォルト、var(可変)は最小限
- プリミティブ型もクラスとしてメソッドを持つ(性能ペナルティなし)
- 暗黙の型変換は禁止 → toLong() などで明示
- "$name" / "${expr}" で文字列テンプレート
- """ raw string """ で複数行
4. null安全性
Kotlinの最大の特徴のひとつ。型システムでNullPointerExceptionをコンパイル時に防止します。
4-1. nullable型
var name: String = "Alice"
name = null // エラー!Stringはnullを許さない
var nullable: String? = "Alice"
nullable = null // OK
String?(クエスチョン付き)が nullable、String(なし)が non-nullable。
4-2. nullチェックの強制
val s: String? = ...
val length = s.length // エラー!sはnullかも
// 解決方法
if (s != null) {
println(s.length) // スマートキャストでnon-nullとして扱える
}
Kotlinは スマートキャスト により、nullチェック後はその変数をnon-nullとして扱えます(val またはlocal varの場合)。
4-3. safe call演算子(?.)
val s: String? = ...
val length: Int? = s?.length // sがnullならlengthもnull
// チェイン
val city: String? = user?.profile?.address?.city
4-4. Elvis演算子(?:)
val length: Int = s?.length ?: 0 // nullなら0
val name: String = nullable ?: "Anonymous"
fun fetchOrThrow(): String {
return fetch() ?: throw IllegalStateException("not found")
}
「nullなら別の値」を一行で書ける。スウェーデンのElvis Presleyに似ているからこの名前。
4-5. !! 演算子(強制非null化)
val s: String? = ...
val length = s!!.length // sがnullならNullPointerException
「ここは絶対にnullでない」と明示する演算子。乱用は危険で、本当に確信があるときだけ使う。
4-6. let、alsoと組み合わせる
val s: String? = ...
s?.let {
println(it.length) // it = s(非nullとして扱える)
println(it.uppercase())
}
s?.also { println("processing: $it") }?.let { it.uppercase() }
let で 「nullでなければ何かする」処理を簡潔に。
4-7. プラットフォーム型(Javaとの相互運用)
// Javaから呼んだ値は「プラットフォーム型」
val s = javaList.get(0) // 型はString!(nullableかnon-nullableか不明)
String! は「nullかもしれないし、そうでないかもしれない」型。Javaのコードはnull注釈なしのことが多いため。
// Java側で @Nullable / @NonNullを付けるとKotlinで正しく解釈される
@NonNull String name() { ... }
@Nullable String email() { ... }
4-8. このセクションのまとめ
- String?(nullable)とString(non-nullable)を型レベルで区別
- スマートキャストでnullチェック後にnon-nullとして扱える
- ?. (safe call)、?: (Elvis)、!!(強制non-null)、letと組合せ
- Javaとの相互運用は「プラットフォーム型」で曖昧、注釈で明確化
- null起因のNPEをコンパイル時に防げる
5. クラスとオブジェクト
5-1. クラスの基本
// Javaと同等
class Person {
var name: String = ""
var age: Int = 0
}
val p = Person()
p.name = "Alice"
p.age = 30
new キーワードは 不要。
5-2. プライマリコンストラクタ
class Person(val name: String, val age: Int)
val p = Person("Alice", 30)
println(p.name)
println(p.age)
プライマリコンストラクタの引数に val / var を付けると、そのままプロパティになる。Javaで言うboilerplate(フィールド宣言+getter/setter+コンストラクタ)が 1行で完了。
5-3. initブロック
class Person(val name: String, val age: Int) {
init {
require(age >= 0) { "Age must be non-negative" }
}
}
init ブロックでバリデーションや派生プロパティの初期化を行えます。複数 init を書けば宣言順に実行。
5-4. セカンダリコンストラクタ
class Person(val name: String, val age: Int) {
constructor(name: String) : this(name, 0) // プライマリへ委譲
}
プライマリコンストラクタへ 必ず委譲する必要があります。
5-5. アクセス修飾子
| 修飾子 | スコープ |
|---|---|
public |
どこからでも(デフォルト) |
private |
同じファイル内(トップレベル)/ 同じクラス内 |
protected |
同じクラス + サブクラス |
internal |
同じモジュール内 |
Javaと違い デフォルトが public。internal という独自の修飾子は「同じビルドモジュール内」を意味し、Javaにはない概念です。
5-6. 継承
open class Animal { // openがないと継承不可
open fun speak() = "..." // openがないとオーバーライド不可
}
class Dog : Animal() { // Animalを継承(() でコンストラクタ呼び出し)
override fun speak() = "Woof!" // override必須
}
val a: Animal = Dog()
println(a.speak()) // "Woof!"
デフォルトでfinal(継承不可)。open キーワードで明示的に解放。Javaの final をデフォルトにしたデザイン。
5-7. abstractクラス
abstract class Shape {
abstract fun area(): Double // 実装必須
fun describe(): String = "Area: ${area()}"
}
class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
}
5-8. interface
interface Drawable {
fun draw() // 抽象メソッド
fun describe(): String = "drawable" // デフォルト実装
}
class Circle : Drawable {
override fun draw() { println("○") }
}
Javaのinterfaceに近いが、デフォルト実装 が普通に書けます。
5-9. このセクションのまとめ
- class(new不要)、プライマリコンストラクタでboilerplate削減
- initブロックで初期化処理
- アクセス修飾子: public(デフォルト)/ private / protected / internal
- 継承はデフォルト不可(openで明示)
- override必須
- abstract / interface(デフォルト実装可)
6. 関数とラムダ
6-1. 関数定義
fun add(a: Int, b: Int): Int {
return a + b
}
// 単一式関数
fun add(a: Int, b: Int) = a + b
// デフォルト引数
fun greet(name: String = "World") = "Hello, $name!"
greet() // "Hello, World!"
greet("Alice") // "Hello, Alice!"
// 名前付き引数
fun connect(host: String = "localhost", port: Int = 8080, ssl: Boolean = false) { ... }
connect(port = 443, ssl = true)
Javaよりはるかに柔軟。Builderパターンが多くの場面で不要になります。
6-2. 可変長引数(vararg)
fun printAll(vararg items: String) {
items.forEach { println(it) }
}
printAll("a", "b", "c")
// 配列を展開(spread演算子)
val list = arrayOf("a", "b", "c")
printAll(*list)
6-3. ラムダ式
val add = { a: Int, b: Int -> a + b }
add(1, 2) // 3
// 型を明示
val add: (Int, Int) -> Int = { a, b -> a + b }
// 引数1つの場合itを使える
val double: (Int) -> Int = { it * 2 }
最後の引数がラムダの場合、括弧の外に出せる:
list.filter { it > 0 }.map { it * 2 }
// 等価
list.filter({ it > 0 }).map({ it * 2 })
これがKotlinのDSLやscope functionsの基盤です。
6-4. 高階関数
fun applyTwice(x: Int, op: (Int) -> Int): Int = op(op(x))
applyTwice(5) { it + 1 } // 7
// メソッド参照
fun double(x: Int) = x * 2
list.map(::double)
// クラスのメソッド参照
class Foo { fun bar(x: Int) = x + 1 }
val f = Foo()
list.map(f::bar)
6-5. インライン関数
inline fun measure(block: () -> Unit): Long {
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
val time = measure {
Thread.sleep(100)
}
inline キーワードを付けるとコンパイラが 呼び出し位置にコードを展開。ラムダのオブジェクト生成コストを排除。forEach、let、apply などはすべて inline。
6-6. このセクションのまとめ
- fun ... : 戻り値型 / = 単一式
- デフォルト引数 + 名前付き引数で柔軟
- vararg + spread演算子(*)
- ラムダ { x, y -> ... }、it(単一引数)、最後ならカッコ外へ
- 高階関数で関数を引数・戻り値に
- inlineで性能ロスなし
7. プロパティとフィールド
7-1. プロパティの基本
class Person {
var name: String = "" // 自動getter / setter
val id: Int = 0 // val: getterのみ
}
Kotlinでは フィールドの直接公開は禁止。name は実際にはアクセサ経由で動きます(Javaから呼ぶと getName())。
7-2. カスタムgetter / setter
class Rectangle(val width: Int, val height: Int) {
val area: Int
get() = width * height // 計算プロパティ
var label: String = ""
set(value) {
require(value.isNotEmpty())
field = value // バッキングフィールド
}
}
field は バッキングフィールドで、setter / getter内でだけ使える特別な識別子。
7-3. lateinitとby lazy
class Service {
lateinit var dependency: Dependency // 後から初期化、null不可
fun init() {
dependency = Dependency()
}
}
class Heavy {
val data: List<Int> by lazy { // 初回アクセス時に計算
loadFromDatabase()
}
}
lateinit は 後から初期化するnon-null型。by lazy は 初回アクセス時に評価(thread safe)。
7-4. const
object Constants {
const val MAX_SIZE = 100 // コンパイル時定数(プリミティブ・Stringのみ)
val timestamp = System.currentTimeMillis() // ランタイム計算
}
const は コンパイル時定数で、Javaの static final 相当。プリミティブとStringのみ使えます。
7-5. このセクションのまとめ
- プロパティはアクセサ経由(field直接公開禁止)
- val(getter)/ var(getter + setter)
- カスタムget() / set(value) でロジック追加
- lateinit(後から初期化)、by lazy(遅延初期化)
- constはstatic final相当(プリミティブ + String)
8. 制御フローとパターンマッチング
8-1. ifは式
val max = if (a > b) a else b // 式として値を返す
val label = if (x > 0) {
"positive"
} else if (x < 0) {
"negative"
} else {
"zero"
}
Kotlinの if は 式で、値を返します。三項演算子は不要。
8-2. when式(強力なswitch)
val description = when (status) {
200 -> "OK"
in 400..499 -> "Client Error"
in 500..599 -> "Server Error"
else -> "Unknown"
}
// 式を引数にせず条件で分岐
val sign = when {
x > 0 -> "+"
x < 0 -> "-"
else -> "0"
}
// 型チェック
when (obj) {
is String -> println("string: ${obj.length}")
is Int -> println("int: ${obj + 1}")
null -> println("null")
else -> println("other")
}
Javaのswitchより強力で、Java 21のswitch式に大きく影響を与えた機能です。
8-3. forループ
for (i in 1..10) println(i) // 1〜10 inclusive
for (i in 1 until 10) println(i) // 1〜9
for (i in 10 downTo 1 step 2) println(i) // 10, 8, 6, 4, 2
for (item in list) println(item)
for ((index, item) in list.withIndex()) println("$index: $item")
for ((key, value) in map) println("$key: $value")
8-4. while / do-while
while (cond) { ... }
do { ... } while (cond)
8-5. breakとcontinue(ラベル付き)
outer@ for (i in 1..10) {
for (j in 1..10) {
if (i * j > 20) break@outer
}
}
ラベルで多重ループから脱出。
8-6. このセクションのまとめ
- ifは式として値を返す(三項演算子不要)
- whenは強力なswitch(範囲、型、式での分岐)
- forはrange(1..10、1 until 10、downTo、step)とiteration
- ラベル付きbreak / continue
9. コレクションとSequence
9-1. List / Set / Map
// 不変
val list: List<Int> = listOf(1, 2, 3)
val set: Set<Int> = setOf(1, 2, 3)
val map: Map<String, Int> = mapOf("a" to 1, "b" to 2)
// 可変
val mutableList: MutableList<Int> = mutableListOf(1, 2, 3)
mutableList.add(4)
val mutableMap = mutableMapOf<String, Int>()
mutableMap["a"] = 1
読み取り専用と可変が型で区別されます。
9-2. コレクション操作
val nums = listOf(1, 2, 3, 4, 5)
nums.filter { it > 2 } // [3, 4, 5]
nums.map { it * 2 } // [2, 4, 6, 8, 10]
nums.flatMap { listOf(it, -it) } // [1, -1, 2, -2, ...]
nums.fold(0) { acc, x -> acc + x } // 15
nums.reduce { acc, x -> acc + x } // 15
nums.sum() // 15
nums.average() // 3.0
nums.maxOrNull() // 5
nums.minOrNull() // 1
nums.count { it > 2 } // 3
nums.any { it > 4 } // true
nums.all { it > 0 } // true
nums.none { it < 0 } // true
nums.partition { it % 2 == 0 } // ([2, 4], [1, 3, 5])
nums.groupBy { it % 2 } // {1=[1, 3, 5], 0=[2, 4]}
nums.associateBy { it.toString() } // {"1"=1, "2"=2, ...}
nums.zip(listOf("a", "b", "c")) // [(1, "a"), (2, "b"), (3, "c")]
nums.windowed(3) // [[1,2,3], [2,3,4], [3,4,5]]
nums.chunked(2) // [[1,2], [3,4], [5]]
9-3. Sequence(遅延評価)
// List: 各操作で中間Listを生成
list.filter { ... }.map { ... }.first() // 中間List 2つ生成
// Sequence: 遅延評価
list.asSequence()
.filter { ... }
.map { ... }
.first() // 中間Sequence、必要分だけ計算
巨大なコレクションや無限シーケンスで重要。JavaのStreamに相当。
9-4. このセクションのまとめ
- listOf / setOf / mapOf(不変)とmutableListOf等(可変)
- 豊富な高階関数(filter, map, flatMap, fold, groupBy, ...)
- Sequenceで遅延評価(Java Stream相当)
- 読み取り専用と可変が型レベルで区別
10. 拡張関数とスコープ関数
10-1. 拡張関数
// 既存クラスに後付けでメソッドを追加
fun String.lastChar(): Char = this[length - 1]
"hello".lastChar() // 'o'
// 演算子オーバーロードも拡張で可能
operator fun String.times(n: Int): String = this.repeat(n)
"ab" * 3 // "ababab"
Javaのstaticメソッドとしてコンパイルされる。実行時にクラスに加わるわけではない。
10-2. スコープ関数(let / run / apply / also / with)
「オブジェクトに対して何かする」共通パターンを表す5つの関数。
| 関数 | 引数 | 戻り値 | 使い所 |
|---|---|---|---|
let |
it |
ラムダ結果 | nullableのチェイン、変換 |
run |
this |
ラムダ結果 | 設定 + 計算 |
apply |
this |
レシーバ | 設定(Builder) |
also |
it |
レシーバ | 副作用(ログ等) |
with |
this |
ラムダ結果 | レシーバを括る |
// let: nullable + 変換
val length: Int? = nullable?.let { it.length }
// apply: builder風
val person = Person().apply {
name = "Alice"
age = 30
}
// also: 副作用
val list = listOf(1, 2, 3).also { println("got: $it") }
// run: ブロック内でthis
val result = config.run {
require(host.isNotEmpty())
"$host:$port"
}
// with: with(obj) { ... }
val result = with(builder) {
add("a")
add("b")
build()
}
これらの 使い分けがKotlinマスターへの道。最初は let と apply から覚える。
10-3. takeIf / takeUnless
val x: Int? = ...
val positive = x?.takeIf { it > 0 } // nullになり得る
val nonZero = x?.takeUnless { it == 0 }
「条件を満たすときだけ自分自身を返す」関数。null安全と組み合わせて便利。
10-4. このセクションのまとめ
- 拡張関数で既存クラスにメソッド追加(実体はstaticメソッド)
- 演算子オーバーロード(operator fun)
- let / run / apply / also / withの使い分け
- takeIf / takeUnlessで条件付き値
11. ジェネリクスと型パラメータ
11-1. ジェネリック関数
fun <T> firstOf(list: List<T>): T = list.first()
firstOf(listOf(1, 2, 3)) // Int推論
firstOf(listOf("a", "b")) // String推論
firstOf<Int>(listOf(1, 2)) // 明示
11-2. ジェネリッククラス
class Box<T>(val content: T) {
fun describe(): String = "Box of ${content}"
}
val box = Box(42)
val box2 = Box("hello")
11-3. 制約
fun <T : Number> sum(list: List<T>): Double {
return list.sumOf { it.toDouble() }
}
// 複数制約
fun <T> process(value: T) where T : Comparable<T>, T : Cloneable { ... }
11-4. variance(in / out)
class Producer<out T>(val value: T) { // 共変(読み取りのみ)
fun get(): T = value
}
class Consumer<in T> { // 反変(書き込みのみ)
fun set(value: T) { ... }
}
val anyProducer: Producer<Any> = Producer<String>("hello") // OK(共変)
val stringConsumer: Consumer<String> = Consumer<Any>() // OK(反変)
out / in で declaration-site variance(クラス定義時に分散を指定)。Javaの ? extends / ? super のクラス側版。
11-5. reified(実体化)
inline fun <reified T> Any.isInstanceOf(): Boolean = this is T
42.isInstanceOf<Int>() // true
"hello".isInstanceOf<Int>() // false
通常Javaのgenericsは 型消去されるが、inline + reified で ランタイムに型情報を保持できる。Kotlinの独自機能。
11-6. このセクションのまとめ
- fun <T>、class Box<T>(...)
- 制約: T : Number、where句
- in / outでdeclaration-site variance
- reifiedで型消去を回避
12. data class / sealed class / value class
12-1. data class
data class Person(val name: String, val age: Int)
val p1 = Person("Alice", 30)
val p2 = Person("Alice", 30)
p1 == p2 // true(自動equals)
p1.hashCode() // 自動hashCode
println(p1) // Person(name=Alice, age=30)(自動toString)
val (name, age) = p1 // 分解宣言(自動componentN)
val p3 = p1.copy(age = 31) // 自動copy(必要なフィールドだけ更新)
equals / hashCode / toString / copy / componentN を 自動生成。Javaの record の精神的祖先。
12-2. sealed class / sealed interface
sealed interface Result<out T> {
data class Success<T>(val value: T) : Result<T>
data class Failure(val error: Throwable) : Result<Nothing>
object Loading : Result<Nothing>
}
fun handle(result: Result<Int>) = when (result) {
is Result.Success -> "got ${result.value}"
is Result.Failure -> "error: ${result.error.message}"
Result.Loading -> "loading..."
// else不要!コンパイラが網羅性をチェック
}
「継承可能なクラス・実装可能なinterfaceを限定」する仕組み。Scalaのsealed trait、Java 17+ のsealedと同じ。**代数的データ型(ADT)**を実現します。
12-3. value class(旧inline class)
@JvmInline
value class UserId(val value: Int)
val id = UserId(42)
fun fetch(id: UserId): User = ...
fetch(UserId(42)) // OK
fetch(42) // エラー!UserIdとIntは別の型
「ランタイムでIntとして扱われるが、コンパイル時には別の型」。意味的型安全性をゼロコストで実現。
12-4. enum class
enum class Status(val code: Int) {
PENDING(0),
ACTIVE(1),
BANNED(2);
fun describe(): String = when (this) {
PENDING -> "保留中"
ACTIVE -> "有効"
BANNED -> "停止"
}
}
Status.ACTIVE.code // 1
Status.ACTIVE.describe() // "有効"
Status.entries // 全値(Kotlin 1.9+、values() の置き換え)
12-5. このセクションのまとめ
- data class: equals/hashCode/toString/copy/componentN自動生成
- sealed class/interface: 継承可能性を限定、ADT実現
- value class: ランタイムゼロコストの型安全性
- enum class: メソッド・プロパティ付き列挙型
13. オブジェクト式とobject宣言
13-1. object(シングルトン)
object Logger {
fun log(msg: String) {
println("[LOG] $msg")
}
}
Logger.log("hello") // インスタンス化不要
シングルトンを言語レベルで提供。Javaの enum + 1要素やprivate constructor + static instanceのイディオムが不要。
13-2. companion object
class User(val name: String) {
companion object {
fun create(name: String): User = User(name)
const val MAX_NAME_LENGTH = 100
}
}
User.create("Alice") // ファクトリメソッド
User.MAX_NAME_LENGTH // 定数
クラスに紐づく 静的メンバー。Javaの static 相当。
13-3. オブジェクト式(無名クラス)
val listener = object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
println("clicked")
}
}
Javaの 匿名クラス相当。複数interface実装も可能。
13-4. このセクションのまとめ
- objectでシングルトン
- companion objectでstatic相当
- object : Type { ... } で匿名クラス
14. デリゲーション
14-1. クラスデリゲーション
interface Logger {
fun log(msg: String)
}
class ConsoleLogger : Logger {
override fun log(msg: String) = println(msg)
}
class TimestampedLogger(private val inner: Logger) : Logger by inner {
// by innerでLoggerの全メソッドをinnerに委譲
}
「継承の代わりに合成」を簡潔に書ける。
14-2. プロパティデリゲーション
class Config {
val host: String by lazy { System.getenv("HOST") ?: "localhost" }
var port: Int by Delegates.observable(8080) { _, old, new ->
println("port changed: $old → $new")
}
}
by でプロパティのアクセスをカスタマイズ。lazy、observable、vetoable、Map から委譲などが標準で用意されています。
14-3. 自作デリゲート
class Trim {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = "..."
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { ... }
}
class Foo {
var name: String by Trim()
}
14-4. このセクションのまとめ
- class Foo : Bar by innerでinterfaceを委譲(合成)
- val/var ... by ... でプロパティ動作をカスタマイズ
- by lazy / by Delegates.observableが頻出
15. 例外処理
15-1. try / catch / finally
try {
risky()
} catch (e: IOException) {
log(e)
} catch (e: Exception) {
log(e)
} finally {
cleanup()
}
// tryは式
val result = try { parse(s) } catch (e: NumberFormatException) { 0 }
15-2. checked exceptionがない
Kotlinには checked exception がありません。throws 宣言は不要。Javaのchecked exceptionはKotlin側では強制されず、catchしなくてもコンパイルエラーになりません。
fun read(): String = File("data").readText() // throws IOException不要
これは「checked exceptionは失敗した実験」というKotlinの判断。
15-3. Result型
fun parse(s: String): Result<Int> = runCatching { s.toInt() }
parse("42")
.onSuccess { println(it) }
.onFailure { println(it.message) }
val n: Int = parse("42").getOrElse { 0 }
例外を 値として扱うスタイル。RustのResultに近い。
15-4. このセクションのまとめ
- tryは式として値を返せる
- checked exceptionなし(throws不要)
- runCatching + Resultでモナド風エラーハンドリング
16. coroutines(コルーチン)
Kotlin最大の特徴のひとつ。軽量な並行プリミティブを言語に統合した革新的な機能。
16-1. なぜcoroutinesか
従来のスレッド:
数MBのスタック、コンテキストスイッチが重い、数千が限界
コールバック:
非同期処理が地獄化(コールバック地獄)
リアクティブ(Rx):
複雑なAPI、学習コストが高い
coroutines:
軽量(数KB)、同期コードのまま記述、構造化並行性
16-2. suspend関数
suspend fun fetchUser(id: Int): User {
delay(1000) // ノンブロッキング待機
return User(id, "Alice")
}
suspend fun main() {
val user = fetchUser(1) // suspend関数はsuspend関数からのみ呼べる
println(user)
}
suspend は「この関数は中断(suspend)できる」という印。実行中に実際にスレッドをブロックせずに「コンパイラが状態機械に変換」して再開ポイントを管理します。
16-3. CoroutineScopeとlaunch
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(1000)
println("hello from coroutine")
}
job.join() // 完了を待つ
}
16-4. asyncとawait
suspend fun fetchAll() = coroutineScope {
val a = async { fetchUser(1) }
val b = async { fetchUser(2) }
val c = async { fetchUser(3) }
listOf(a.await(), b.await(), c.await()) // 並行に取得
}
JS / C# のasync/awaitに近い書き味。
16-5. Dispatcher
launch(Dispatchers.IO) {
val data = readFile() // IO用スレッドプール
}
launch(Dispatchers.Default) {
val result = compute() // CPUバウンド用
}
launch(Dispatchers.Main) {
updateUI() // UIスレッド(Android)
}
「どのスレッドプールで実行するか」をDispatcherで指定。
16-6. 構造化並行性
suspend fun work() = coroutineScope {
launch { task1() }
launch { task2() }
// すべて完了するか1つでも失敗するまでこの関数は終わらない
}
子コルーチンが 親のスコープ内で生きることを保証。エラー伝播・キャンセルが自動的に。
val job = scope.launch {
delay(10000)
}
job.cancel() // キャンセル
job.cancelAndJoin()
16-7. このセクションのまとめ
- suspend関数で中断可能、runBlocking / launch / async
- coroutineScopeで構造化並行性
- Dispatchers.IO / Default / Mainで実行先選択
- async + awaitで並行
- キャンセルが言語レベルでサポート
17. Flow
coroutines上に構築された 非同期ストリーム。Reactive StreamsのKotlin版。
17-1. Flowの基本
fun simple(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}
suspend fun main() {
simple().collect { println(it) }
}
flow { emit(...) } で生成、collect で消費。遅延・キャンセル可能・suspend関数を使える。
17-2. オペレータ
flow.filter { it > 0 }
.map { it * 2 }
.take(10)
.onEach { log(it) }
.collect { println(it) }
Stream風のオペレータが豊富。
17-3. StateFlow / SharedFlow
val state = MutableStateFlow(0)
state.value = 1 // 値の更新
state.collect { println(it) } // 現在値 + 変更を購読
val events = MutableSharedFlow<Event>()
events.emit(Event.Click)
ホット・ストリーム(複数購読者で共有)。Android UI状態管理で頻出。
17-4. このセクションのまとめ
- Flow = 非同期ストリーム(cold)
- StateFlow = 現在値を持つホット
- SharedFlow = 複数購読のホット
- collectで消費、map/filter/take等のオペレータ
18. Javaとの相互運用
18-1. KotlinからJavaを呼ぶ
val list = java.util.ArrayList<String>()
list.add("hello")
val map = java.util.HashMap<String, Int>()
ほぼJavaと同じ。プラットフォーム型に注意(4-7)。
18-2. JavaからKotlinを呼ぶ
// Kotlin側
class User(val name: String) {
fun greet() = "Hi, $name"
}
object Utils {
fun log(msg: String) = println(msg)
@JvmStatic fun staticLog(msg: String) = println(msg)
}
@JvmField
val MAX_AGE = 150
// Java側
User user = new User("Alice");
String name = user.getName(); // プロパティはgetter
user.greet();
Utils.INSTANCE.log("hi"); // objectはシングルトン
Utils.staticLog("hi"); // @JvmStaticでstatic化
int max = MAX_AGE; // @JvmFieldでfield化
18-3. ヘルパーアノテーション
| アノテーション | 効果 |
|---|---|
@JvmStatic |
objectのメソッドをJavaのstaticとして呼べる |
@JvmField |
プロパティをJavaのpublic fieldとして公開 |
@JvmOverloads |
デフォルト引数の各組み合わせをオーバーロード生成 |
@JvmName |
Javaから見える名前を変更 |
@JvmDefault |
interfaceのdefault実装をJavaから使えるように |
@Throws |
Javaのchecked exception互換 |
18-4. このセクションのまとめ
- 双方向で完全相互運用
- プラットフォーム型に注意(4-7)
- @Jvm* アノテーションでJavaからの呼び出しを最適化
19. Kotlin Multiplatform
「1つのコードをJVM / iOS / JS / Nativeに出力」する仕組み。
19-1. プロジェクト構成
shared/
├── src/
│ ├── commonMain/ # 全プラットフォーム共通
│ ├── androidMain/ # Android固有
│ ├── iosMain/ # iOS固有
│ ├── jvmMain/ # JVM
│ └── jsMain/ # JS
commonMain に共通ロジックを書き、各プラットフォームの実装は expect/actual で分離。
// commonMain
expect fun platformName(): String
// androidMain
actual fun platformName(): String = "Android"
// iosMain
actual fun platformName(): String = "iOS"
19-2. 主要ライブラリ
| ライブラリ | 用途 |
|---|---|
kotlinx.coroutines |
並行処理 |
kotlinx.serialization |
JSON等 |
Ktor |
HTTPクライアント・サーバ |
SQLDelight |
SQL DB |
Compose Multiplatform |
UI |
Koin |
DI |
19-3. iOSとの統合
// Kotlin/NativeでiOS frameworkを生成
./gradlew :shared:assembleXCFramework
Swift / Objective-Cから呼べるframeworkとして配布。Trip.com、Netflix、Cash Appなどが採用。
19-4. このセクションのまとめ
- 1つの言語でJVM/iOS/JS/Native/WASM
- expect/actualでプラットフォーム差分
- Compose MultiplatformでUIも共通化可能
- iOSではSwift/Obj-Cと相互運用
20. Android開発
20-1. ActivityとFragment
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
@Composable
fun MyApp() {
Text("Hello, Android!")
}
Jetpack Compose が現代の標準UIフレームワーク。ReactやSwiftUIに近い宣言的UI。
20-2. ViewModelとStateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value++
}
}
@Composable
fun Counter(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Button(onClick = viewModel::increment) {
Text("Count: $count")
}
}
20-3. Coroutines + lifecycleScope
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = repository.fetch()
_state.value = data
}
}
}
ViewModel終了時に自動キャンセル。リーク防止。
20-4. このセクションのまとめ
- Jetpack Composeで宣言的UI
- ViewModel + StateFlowで状態管理
- viewModelScopeでcoroutinesのライフサイクル管理
- Hilt(DI)、Room(DB)、Retrofit(HTTP)が標準スタック
21. Ktor / Spring Boot Kotlin
21-1. Ktor(KotlinネイティブWeb)
fun main() {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) { json() }
routing {
get("/users/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest)
val user = userService.find(id)
if (user == null) call.respond(HttpStatusCode.NotFound)
else call.respond(user)
}
post("/users") {
val req = call.receive<CreateUserRequest>()
val user = userService.create(req)
call.respond(HttpStatusCode.Created, user)
}
}
}.start(wait = true)
}
JetBrains製。coroutinesベースで軽量・高速。
21-2. Spring Boot Kotlin
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
@RestController
@RequestMapping("/users")
class UserController(private val service: UserService) {
@GetMapping("/{id}")
fun get(@PathVariable id: Long): User =
service.findById(id) ?: throw NotFoundException()
@PostMapping
fun create(@RequestBody req: CreateUserRequest): User =
service.create(req)
}
Java同様のSpring BootをKotlinで書ける。よりシンプルで読みやすい。
21-3. このセクションのまとめ
- Ktor: Kotlinネイティブ、coroutinesベース、軽量
- Spring Boot Kotlin: Java互換 + Kotlin利点
- どちらも実プロダクション採用
22. テスト戦略
22-1. JUnit 5 + Kotlin
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
private val calc = Calculator()
@Test
fun `add should return sum`() { // バックティックで自然な名前
assertEquals(3, calc.add(1, 2))
}
@Test
fun `divide by zero should throw`() {
assertThrows<ArithmeticException> {
calc.divide(1, 0)
}
}
}
メソッド名にバッククォート(`)で空白や日本語が使えます。
22-2. kotlin.test
import kotlin.test.*
class ExampleTest {
@Test
fun example() {
assertEquals(3, 1 + 2)
assertTrue(1 < 2)
assertNull(null)
}
}
JUnitを抽象化したマルチプラットフォームテストAPI。
22-3. MockK(Kotlinネイティブのモック)
import io.mockk.*
@Test
fun test() {
val repo = mockk<UserRepository>()
every { repo.findById(1) } returns User("Alice")
val service = UserService(repo)
val user = service.getName(1)
assertEquals("Alice", user.name)
verify { repo.findById(1) }
}
MockitoのKotlin版。final クラスにも対応。
22-4. Kotest(DSLベース)
class CalculatorSpec : StringSpec({
"add returns sum" {
Calculator().add(1, 2) shouldBe 3
}
"divide by zero throws" {
shouldThrow<ArithmeticException> {
Calculator().divide(1, 0)
}
}
})
RSpec / Pest風の流暢なDSL。
22-5. このセクションのまとめ
- JUnit 5 + バックティック関数名
- kotlin.testでmultiplatform対応
- MockKがKotlin標準モック
- KotestでDSLスタイル
23. メタプログラミング・KSP・コンパイラプラグイン
23-1. リフレクション
import kotlin.reflect.full.*
class Person(val name: String, val age: Int)
val person = Person("Alice", 30)
val klass = person::class
klass.simpleName // "Person"
klass.memberProperties // プロパティ一覧
klass.constructors // コンストラクタ
JVMのリフレクションよりKotlinらしいAPI。kotlin-reflect 依存が必要。
23-2. KSP(Kotlin Symbol Processing)
@AutoService(SymbolProcessorProvider::class)
class MyProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
// アノテーション付きクラスを発見してコード生成
return emptyList()
}
}
KAPT(旧)の後継。コンパイル時のコード生成。Room、Hilt、Moshiが採用。リフレクションより速い。
23-3. このセクションのまとめ
- kotlin-reflectで実行時リフレクション
- KSPでコンパイル時コード生成(KAPTの後継)
- コンパイラプラグインも書ける(高度)
24. パフォーマンスチューニング
24-1. 計測
import kotlin.system.measureTimeMillis
val time = measureTimeMillis {
heavyOperation()
}
println("$time ms")
// JMHで本格ベンチマーク
@Benchmark
fun benchmark() {
heavyOperation()
}
24-2. inlineとnoinline
inline fun measure(block: () -> Unit): Long { // ラムダがインライン展開
val start = System.nanoTime()
block()
return System.nanoTime() - start
}
inline fun foo(noinline callback: () -> Unit) { // インライン化しないラムダ
saveCallback(callback)
}
24-3. Sequenceで中間List回避
// list.filter().map().first() → 中間List 2つ生成
list.asSequence().filter().map().first() // 中間なし
巨大コレクションでは大きな差。
24-4. value classでゼロコスト型安全
@JvmInline
value class UserId(val value: Int)
ランタイムは Int のまま、コンパイル時の型チェックだけ。
24-5. このセクションのまとめ
- measureTimeMillis / JMH
- inlineでラムダコスト削減
- Sequenceで中間List回避
- value classでゼロコスト型安全
- JVMの最適化(GC、JIT)はJavaと同じ知識が活きる
25. Kotlin 1.x〜2.0の進化
1.0 (2016) 初版安定リリース
1.3 (2018) coroutines安定化、契約API(contracts)
1.4 (2020) 関数型interface(fun interface)
1.5 (2021) value class安定化、sealed interface
1.6 (2021) exhaustive when(強制ではない)
1.7 (2022) K2コンパイラalpha、definitely non-nullable types
1.8 (2023) JVM target 19
1.9 (2023) enum.entries、open ended ranges、data object
2.0 (2024) K2コンパイラ正式、コンパイル2倍速
2.1 (2024) Guard conditions in when、Multi-dollar interpolation
26. よくある落とし穴FAQ
Q1. String! は何?
Javaから来たプラットフォーム型。nullかどうか不明。@Nullable/@NonNull 注釈があるJavaコードを呼ぶと正しい型になる。
Q2. lateinit を初期化前にアクセスしたら?
UninitializedPropertyAccessException。isInitialized でチェック可能。
Q3. data class を継承できる?
通常はできない。open を付けないと継承不可。copy の挙動も難しくなるので 継承しないのが推奨。
Q4. companion object のメンバーはJavaのstatic?
厳密には違う。デフォルトでは Companion インスタンスのメンバー。@JvmStatic を付けるとJavaのstaticになる。
Q5. when にelseが要る?
when を 式として使うときは網羅性が必要(elseまたはsealed class)。文として使うときは不要。
Q6. Unit と Void の違い
Unit はKotlinの戻り値なし型(実体は Unit シングルトン)。Javaの Void とは別物。Kotlinから見えるJavaの void は Unit にマッピング。
Q7. runBlocking を本番で使ってよい?
mainエントリやテストではOK。それ以外は コルーチンの利点を消すので避ける。
Q8. coroutinesが動かない(最初の例で何も出ない)
runBlocking で囲んでいないか、Dispatcherが間違っている可能性が高い。
Q9. Javaのchecked exceptionをKotlinでcatchしなくてよい?
その通り。コンパイルエラーにならない。逆にJavaから呼ぶKotlinで例外を投げるなら @Throws を付ける。
Q10. apply と also どっち?
戻り値が this でいいなら apply(builder風)、副作用したいだけなら also(ログ等)。apply のラムダ内では this、also では it。
27. 実践パターン集
27-1. Builderパターンの代替
// 名前付き引数 + デフォルト引数で十分
fun createUser(
name: String,
email: String,
age: Int = 0,
admin: Boolean = false,
) = User(name, email, age, admin)
createUser(name = "Alice", email = "a@b.com", admin = true)
27-2. DSL(apply + lambda with receiver)
fun html(block: HtmlBuilder.() -> Unit): String {
val builder = HtmlBuilder()
builder.block()
return builder.toString()
}
class HtmlBuilder {
private val sb = StringBuilder()
fun body(block: HtmlBuilder.() -> Unit) {
sb.append("<body>")
block()
sb.append("</body>")
}
fun p(text: String) { sb.append("<p>$text</p>") }
}
// 使用
val page = html {
body {
p("Hello, World")
}
}
Anko、Compose、Gradle Kotlin DSLの基盤。
27-3. Resultでエラー値化
fun parse(s: String): Result<Int> = runCatching { s.toInt() }
parse("42")
.map { it * 2 }
.onFailure { log(it) }
.getOrDefault(0)
27-4. このセクションのまとめ
- 名前付き + デフォルト引数でBuilder不要
- 拡張関数 + lambda with receiverでDSL
- Result + runCatchingでエラー値化
28. 学習ロードマップ(30日)
Week 1: 基礎
- IntelliJ IDEAセットアップ
- val/var、基本型、null安全
- 関数、デフォルト引数、ラムダ
- if/when/for
Week 2: OOPと関数型
- class、data class、sealed
- 継承、interface、object
- 高階関数、拡張関数、scope functions
- ジェネリクス
Week 3: 並行・I/O
- coroutines、suspend
- Flow、StateFlow
- KtorまたはSpring BootでREST API
- JUnit 5 + MockKでテスト
Week 4: 応用
- Kotlin Multiplatform入門
- Android入門(Compose)
- KSP / コンパイラプラグイン
- 実プロジェクトを公開
29. 用語集
あ行
- アクセサ: プロパティのgetter/setter
- イミュータブル: 不変。
valがデフォルト - 拡張関数: 既存クラスに後付けするメソッド
か行
- コルーチン(coroutine): 中断可能な軽量タスク
- 構造化並行性: 親スコープを抜ける前に子も終わる規律
さ行
- suspend関数: 中断可能な関数
- scope function: let/run/apply/also/with
た行
- デリゲーション: byキーワードによる委譲
- データクラス: data class、自動生成equals/hashCode等
な行
- null安全性: 型レベルでnullを区別
は行
- プラットフォーム型: Javaから来たnull不明型(
String!) - プロパティ: フィールド + アクセサの組
ま行
- マルチプラットフォーム: KMP、JVM/iOS/JS/Native/WASM
A〜Z
- ADT: 代数的データ型(sealed + data classes)
- DSL: Domain-Specific Language
- K2: Kotlin 2.0の新コンパイラ
- KMP: Kotlin Multiplatform
- KSP: Kotlin Symbol Processing
- KAPT: Kotlin Annotation Processing Tool(KSPに置き換わった)
30. Kotlin学習の次のステップ
Kotlinは 「Javaの良さを残しつつ、現代的に再設計」した稀有な言語です。null安全、coroutines、data class、scope functions ─ どの機能も「Javaで書きにくかった」を解決しています。
- Android開発: Google First Class
- バックエンド: Spring Boot Kotlin / Ktor
- マルチプラットフォーム: 1コードでiOS/Android/Web
- スクリプト: .ktsで軽量スクリプト
- Server-side: Netflix、Pinterest、Trelloが採用
JetBrainsの継続的投資、Googleのサポート、Kotlin Multiplatformエコシステムの広がりにより、KotlinはAndroidだけでなくサーバーサイドや共有ロジックの実装でも使われます。
Kotlinの精神は、次の言葉にまとまっています。
“Concise, safe, interoperable, tool-friendly.”
── Kotlin公式
Kotlinを使うときは、null安全、コルーチン、data class、Java相互運用を個別機能として覚えるだけでなく、「安全に短く書き、既存資産と接続する」ための設計道具として捉えると理解しやすくなります。
発展: Kotlinらしい設計
ここからはKotlinの各機能を 実例とともに深掘り。coroutines、Flow、Multiplatform、Android開発、メタプログラミング、性能チューニングまで詳細に。
31. 関数型プログラミング深掘り
Kotlinは オブジェクト指向 + 関数型のハイブリッド。Scalaほど純粋ではないが、関数型の豊かなツールが揃っています。
31-1. 不変性の徹底
// Bad: var多用
class Counter {
var count: Int = 0
fun increment() { count++ }
}
// Good: イミュータブルなデータ + 関数
data class Counter(val count: Int) {
fun increment() = copy(count = count + 1)
}
val initial = Counter(0)
val updated = initial.increment()
// initialは変わらない、新しいCounterが返る
イミュータブルなdata classと copy で 「状態変化を新インスタンスとして表現」するのが関数型流。
31-2. 高階関数の活用
// 関数を引数に
fun <T, R> List<T>.fold(initial: R, op: (R, T) -> R): R {
var acc = initial
for (x in this) acc = op(acc, x)
return acc
}
val sum = listOf(1, 2, 3, 4).fold(0) { acc, x -> acc + x }
val product = listOf(1, 2, 3, 4).fold(1) { acc, x -> acc * x }
// 関数を返す
fun makeAdder(n: Int): (Int) -> Int = { x -> x + n }
val add5 = makeAdder(5)
val add10 = makeAdder(10)
add5(3) // 8
add10(3) // 13
31-3. パイプライン処理
val result = listOf(1, 2, 3, 4, 5)
.filter { it % 2 == 0 }
.map { it * it }
.sum()
// 4 + 16 = 20
let / also / apply と組み合わせると、命令型より宣言的に書ける。
31-4. tailrec(末尾再帰最適化)
tailrec fun factorial(n: Long, acc: Long = 1): Long =
if (n <= 1) acc
else factorial(n - 1, acc * n)
factorial(20)
tailrec 修飾子で 末尾再帰をループに変換。スタックオーバーフローを避けつつ再帰スタイルで書ける。
31-5. inline classとvalue semantics
@JvmInline
value class Email(val value: String) {
init {
require("@" in value) { "Invalid email" }
}
}
fun sendEmail(to: Email) { ... }
sendEmail(Email("a@b.c"))
sendEmail("a@b.c") // エラー!StringとEmailは別の型
ランタイム上はStringそのまま、コンパイル時の型安全性のみ。ゼロコストの値オブジェクト。
31-6. このセクションのまとめ
- data class + copyでイミュータブルな状態変化
- 高階関数で関数を引数/戻り値
- tailrecで末尾再帰最適化
- value classでゼロコストの意味的型
- パイプライン処理で宣言的コード
32. coroutines深掘り
32-1. 内部仕組み(Continuation)
suspend 関数はコンパイラによって Continuation Passing Style(CPS) に変換されます。
suspend fun fetchUser(id: Int): User {
delay(1000)
return User(id, "Alice")
}
// コンパイラが内部で生成(疑似コード)
fun fetchUser(id: Int, cont: Continuation<User>) {
// 状態機械として実装される
}
「スレッドを止めずに中断・再開できる」のがこの仕組みの核。JavaのVirtual Threadsとは別アプローチで同じ目的を達成。
32-2. CoroutineContextとCoroutineScope
// Contextは要素の組み合わせ
val context = Dispatchers.IO + Job() + CoroutineName("worker")
// Scopeはcontextを持つ
class MyScope(override val coroutineContext: CoroutineContext) : CoroutineScope
// 標準的なScope
GlobalScope // アプリ全体(避ける)
runBlocking // main関数用
viewModelScope // Android ViewModel
lifecycleScope // Android Lifecycle
「どこで実行するか」「親子関係」「キャンセル伝播」をContextが管理。
32-3. キャンセルと協調的中断
val job = launch {
try {
repeat(10) {
delay(100)
println(it)
}
} catch (e: CancellationException) {
// クリーンアップ
println("cancelled")
} finally {
// 必ず実行
}
}
delay(500)
job.cancel()
delay などの suspend関数はキャンセル可能ポイント。CPUバウンドなループでは yield() か ensureActive() で明示的にチェック。
launch {
repeat(1_000_000) {
// CPUバウンド処理
ensureActive() // ここでキャンセル確認
}
}
32-4. SupervisorJobとエラー伝播
// 通常のJob: 子の失敗で親と兄弟もキャンセル
coroutineScope {
launch { throw Exception() } // 全部キャンセル
launch { /* これも止まる */ }
}
// SupervisorJob: 子の失敗が他に波及しない
supervisorScope {
launch { throw Exception() } // これだけ失敗
launch { /* これは継続 */ }
}
UIコンポーネント(一部失敗しても全体は止めたくない)で重要。
32-5. CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, e ->
println("Caught: $e")
}
GlobalScope.launch(handler) {
throw RuntimeException("oops")
}
トップレベルの例外をフック。
32-6. このセクションのまとめ
- 状態機械にコンパイル(Continuation Passing Style)
- Context = Dispatcher + Job + Nameなど
- CancellationExceptionが伝播
- ensureActive() / yield() でキャンセル可能ポイント
- SupervisorJobで子の独立性
- CoroutineExceptionHandlerでグローバル捕捉
33. Flow深掘り
33-1. cold flowとhot flow
Cold Flow:
Flow<T> collectされるたびに最初から流れる
Hot Flow:
StateFlow現在値を保持、新規購読者にも最新値
SharedFlow複数購読者で共有、replay可能
33-2. Flowオペレータ
flow {
emit(1)
emit(2)
emit(3)
}
.map { it * 2 }
.filter { it > 2 }
.onEach { println("got $it") }
.flatMapConcat { v -> flow { emit(v); emit(v + 100) } }
.zip(otherFlow) { a, b -> a + b }
.take(10)
.flowOn(Dispatchers.IO) // upstreamをIOで実行
.catch { e -> emit(-1) } // エラーハンドリング
.onCompletion { println("done") }
.launchIn(scope)
33-3. StateFlow(推奨される状態管理)
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() { _count.value++ }
fun reset() { _count.value = 0 }
}
// 購読側(Compose)
@Composable
fun Counter(vm: CounterViewModel) {
val count by vm.count.collectAsState()
Text("$count")
}
「現在値を持つObservable」。LiveDataの現代版。
33-4. SharedFlow(イベントストリーム)
class EventBus {
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
「複数購読者にイベントを配信」。
違い
StateFlow: 常に1つの最新値、初回購読時に最新値が来る
SharedFlow: 値を保持しない、購読開始後のイベントだけ
replay引数で過去N件を保持可能
33-5. Flow vs Channel
// Channel: producer-consumer
val channel = Channel<Int>()
launch {
for (i in 1..3) channel.send(i)
channel.close()
}
for (item in channel) println(item)
// Flow: 宣言的なストリーム
val flow = flow {
for (i in 1..3) emit(i)
}
flow.collect { println(it) }
「Channelは通信、Flowは変換パイプライン」。
33-6. このセクションのまとめ
- Flow(cold)/ StateFlow / SharedFlow(hot)
- map / filter / onEachなどオペレータ豊富
- flowOnでupstreamのDispatcher切替
- catch / onCompletionでエラー・完了
- StateFlowでUI状態、SharedFlowでイベント
- Channelはproducer/consumer通信
34. Android開発深掘り
34-1. Jetpack Compose詳細
@Composable
fun TodoApp() {
var todos by remember { mutableStateOf(listOf<Todo>()) }
var input by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = input,
onValueChange = { input = it },
label = { Text("New todo") }
)
Button(onClick = {
if (input.isNotBlank()) {
todos = todos + Todo(input)
input = ""
}
}) {
Text("Add")
}
LazyColumn {
items(todos) { todo ->
TodoItem(todo) {
todos = todos - todo
}
}
}
}
}
@Composable
fun TodoItem(todo: Todo, onDelete: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
Text(todo.text, modifier = Modifier.weight(1f))
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, "Delete")
}
}
}
宣言的UI。Reactに近い。remember でローカル状態、by mutableStateOf で可変。
34-2. ViewModel + Repositoryパターン
// Repository
class UserRepository(private val api: UserApi, private val dao: UserDao) {
fun getUsers(): Flow<List<User>> = flow {
emit(dao.getAll()) // キャッシュから即座に
try {
val fresh = api.fetchUsers()
dao.insertAll(fresh)
emit(fresh)
} catch (e: Exception) {
// エラー時は キャッシュのまま
}
}
}
// ViewModel
class UsersViewModel(private val repo: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
repo.getUsers()
.catch { _uiState.value = UiState.Error(it.message ?: "") }
.collect { _uiState.value = UiState.Success(it) }
}
}
}
sealed class UiState {
object Loading : UiState()
data class Success(val users: List<User>) : UiState()
data class Error(val message: String) : UiState()
}
// UI
@Composable
fun UsersScreen(vm: UsersViewModel) {
val state by vm.uiState.collectAsState()
when (val s = state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> UserList(s.users)
is UiState.Error -> Text("Error: ${s.message}")
}
}
sealed class + when で網羅的なUI状態管理。Kotlinらしい書き方。
34-3. Hilt(DI)
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
@Provides
fun provideUserApi(retrofit: Retrofit): UserApi =
retrofit.create(UserApi::class.java)
}
@HiltViewModel
class UsersViewModel @Inject constructor(
private val repo: UserRepository
) : ViewModel() { ... }
Google公式のDIフレームワーク。Daggerベース。
34-4. Room(DB)
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: Long,
val name: String,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAll(): Flow<List<UserEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: UserEntity)
@Query("DELETE FROM users WHERE id = :id")
suspend fun delete(id: Long)
}
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Android公式のSQLiteラッパー。Flow統合でreactive。
34-5. このセクションのまとめ
- Jetpack Composeで宣言的UI
- ViewModel + Repository + Flowパターン
- sealed class + whenで網羅的状態
- HiltでDI
- RoomでSQLite + Flow
35. Kotlin Multiplatform深掘り
35-1. 共有モジュール構成
shared/
├── src/
│ ├── commonMain/kotlin/ # 全プラットフォーム共通
│ │ ├── DataLayer.kt
│ │ └── BusinessLogic.kt
│ ├── androidMain/kotlin/ # Android固有
│ │ └── Platform.android.kt
│ ├── iosMain/kotlin/ # iOS固有
│ │ └── Platform.ios.kt
│ └── commonTest/ # 共通テスト
└── build.gradle.kts
35-2. expect / actual
// commonMain
expect class PlatformInfo() {
val name: String
val osVersion: String
}
// androidMain
actual class PlatformInfo {
actual val name: String = "Android"
actual val osVersion: String = android.os.Build.VERSION.RELEASE
}
// iosMain
actual class PlatformInfo {
actual val name: String = "iOS"
actual val osVersion: String = NSProcessInfo.processInfo.operatingSystemVersionString
}
「プラットフォーム差分をexpect/actualで隔離」。共通コードは80〜90% を目指す。
35-3. Compose Multiplatform
@Composable
fun App() {
MaterialTheme {
Text("Hello from Compose Multiplatform")
}
}
JetBrains製。Android / iOS / Desktop / Webで同じComposeコード。「SwiftUI / React Native統合版」とも言える。
35-4. iOS配信
# XCFrameworkとしてiOSにエクスポート
./gradlew :shared:assembleSharedXCFramework
Swift / Objective-Cから呼べるframeworkが生成される。
35-5. このセクションのまとめ
- expect/actualでプラットフォーム差分
- 共通コードを80% 以上目指す
- Compose MultiplatformでUI統一
- iOSへのXCFrameworkエクスポート
- KMP採用例: Trip.com、Netflix、Cash App
36. ジェネリクス深掘り
36-1. variance(in / out)
// 共変(out): Tを「生み出す」だけ、上位型として扱える
interface Producer<out T> {
fun produce(): T
}
val stringProducer: Producer<String> = ...
val anyProducer: Producer<Any> = stringProducer // OK
// 反変(in): Tを「消費する」だけ
interface Consumer<in T> {
fun consume(item: T)
}
val anyConsumer: Consumer<Any> = ...
val stringConsumer: Consumer<String> = anyConsumer // OK
「Producer Extends, Consumer Super」。Javaの ? extends T / ? super T のクラス側版。
36-2. star projection
fun describe(list: List<*>) = "size: ${list.size}"
// 何のListかは不明、サイズなど共通操作だけ可能
36-3. reified型パラメータ
inline fun <reified T> Any?.cast(): T? = this as? T
inline fun <reified T> List<*>.filterIsInstance(): List<T> =
filter { it is T }.map { it as T }
// 使用
val ints = listOf<Any>(1, "a", 2, "b").filterIsInstance<Int>()
// [1, 2]
通常のJava/Kotlin genericsは 型消去されるが、inline + reified で型情報を保持できる。Kotlinの独自機能。
36-4. 型パラメータの制約
fun <T : Comparable<T>> max(a: T, b: T): T = if (a > b) a else b
fun <T> insertSort(list: List<T>) where T : Comparable<T>, T : Cloneable {
// 複数の制約はwhere句
}
36-5. このセクションのまとめ
- out(共変)/ in(反変)/ なし(不変)
- star projection(List<*>)
- reified(inline関数のみ)で型消去回避
- where句で複数制約
37. インライン関数とラムダ最適化
37-1. inlineの効果
// inlineなし
fun forEach(items: List<Int>, op: (Int) -> Unit) {
for (item in items) op(item)
}
// inlineあり
inline fun forEach(items: List<Int>, op: (Int) -> Unit) {
for (item in items) op(item)
}
inlineすると ラムダのオブジェクト生成がなくなる。forEach / map / let などの標準関数はすべてinline。
37-2. crossinline
inline fun runIn(noinline block: () -> Unit) {
Thread { block() }.start() // 別スレッドで実行
}
noinline は inlineされないラムダ。crossinline は returnできないラムダ。
inline fun runChecked(crossinline block: () -> Unit) {
Thread {
block() // ここでreturnすると外側を抜けてしまう
// crossinlineでreturnを禁止
}.start()
}
37-3. 非局所return
fun findFirst(): Int? {
listOf(1, 2, 3, 4).forEach {
if (it > 2) return it // findFirstからreturn
}
return null
}
inline関数のラムダから外側関数のreturnができる。便利だが混乱の元。
37-4. このセクションのまとめ
- inlineでラムダのオブジェクト生成排除
- noinline / crossinlineで挙動制御
- 非局所return(inline関数限定)
- forEach / map / letは標準でinline
38. Kotlin DSL
38-1. ラムダ + receiverでDSL
class HtmlBuilder {
private val sb = StringBuilder()
fun body(block: HtmlBuilder.() -> Unit) {
sb.append("<body>")
block()
sb.append("</body>")
}
fun p(text: String) { sb.append("<p>$text</p>") }
override fun toString() = sb.toString()
}
fun html(block: HtmlBuilder.() -> Unit): String =
HtmlBuilder().apply(block).toString()
// 使用
val page = html {
body {
p("Hello, World")
p("from Kotlin DSL")
}
}
HtmlBuilder.() -> Unit は HtmlBuilderをreceiverとするラムダ。ブロック内で this がHtmlBuilder。
38-2. 実例: Gradle Kotlin DSL
plugins {
kotlin("jvm") version "2.0.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
tasks.test {
useJUnitPlatform()
}
plugins { ... }、dependencies { ... } などすべてKotlin DSL。
38-3. @DslMarker
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HtmlBuilder { ... }
@HtmlDsl
class BodyBuilder { ... }
@DslMarker を付けると 異なるDSL receiverの混在を防ぐ。
38-4. このセクションのまとめ
- T.() -> Unitでreceiver付きラムダ
- apply / with / runと組み合わせてDSL
- @DslMarkerでスコープ汚染防止
- Gradle / Compose / KtorがDSLの好例
39. Kotlin Compiler Plugin / KSP
39-1. KSP(Kotlin Symbol Processing)
KAPT(Kotlin Annotation Processing Tool)の後継。コンパイル時にコード生成。
@Serializable
data class User(val name: String, val age: Int)
// kotlinx.serializationのKSPプラグインが
// シリアライズコードを自動生成
主な利用先:
kotlinx.serialization
Room(Android DB)
Hilt / Dagger
Moshi
Ktorのルート定義
39-2. 自作KSPプロセッサ
class MyProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.MyAnnotation")
symbols.forEach { symbol ->
// コード生成
}
return emptyList()
}
}
リフレクションより 桁違いに速い(ランタイムコストなし)。
39-3. このセクションのまとめ
- KSPがコンパイル時コード生成の標準
- KAPTより高速(10倍以上)
- Room / Hilt / Serializationで活用
- 自作可能、proc macro風
40. テスト戦略詳細
40-1. JUnit 5 + Kotlin
class CalculatorTest {
@Test
fun `addition works`() {
assertEquals(3, add(1, 2))
}
@ParameterizedTest
@CsvSource("1, 2, 3", "0, 0, 0", "-1, 1, 0")
fun `addition cases`(a: Int, b: Int, expected: Int) {
assertEquals(expected, add(a, b))
}
@Test
fun `throws on overflow`() {
assertThrows<ArithmeticException> {
Math.addExact(Int.MAX_VALUE, 1)
}
}
}
バックティック(`)でメソッド名に空白・記号が使えるので テスト名が読みやすい。
40-2. Kotest
class CalculatorSpec : StringSpec({
"addition" {
(1 + 2) shouldBe 3
}
"negative" {
(-1 - 1) shouldBe -2
}
})
class TableTestSpec : FunSpec({
context("addition") {
withData(
row(1, 2, 3),
row(0, 0, 0),
) { (a, b, expected) ->
(a + b) shouldBe expected
}
}
})
DSLベース。RSpec / Pest風でKotlinらしい。
40-3. MockK
@Test
fun test() {
val repo = mockk<UserRepository>()
every { repo.findById(1) } returns User(1, "Alice")
val service = UserService(repo)
val name = service.getName(1)
name shouldBe "Alice"
verify { repo.findById(1) }
}
// coroutinesのモック
coEvery { suspendFn() } returns "result"
Kotlin専用モック。final クラスやcoroutinesにも対応。
40-4. coroutinesテスト
@Test
fun `coroutines test`() = runTest {
val result = async { fetchData() }
result.await() shouldBe "data"
}
class TimeoutTest {
@Test
fun `with timeout`() = runTest {
withTimeout(1000) {
doSomething()
}
}
}
// 仮想時間で高速テスト
class DelayTest {
@Test
fun `delay is fast in test`() = runTest {
val time = System.currentTimeMillis()
delay(10000) // 実時間ではほぼ0、仮想時間で10000ms
// テストはすぐ終わる
}
}
runTest で 仮想時間を使い、delay 等が即座に進む。テストが速い。
40-5. このセクションのまとめ
- JUnit 5 + Kotlin(バッククォートで自然な名前)
- KotestがDSLスタイル
- MockKが標準的モック(final / coroutines OK)
- runTestでcoroutinesテスト(仮想時間)
41. パフォーマンスチューニング
41-1. 計測
import kotlin.system.measureTimeMillis
import kotlin.system.measureNanoTime
val time = measureTimeMillis {
heavyOperation()
}
println("$time ms")
// JMH(Java Microbenchmark Harness)
@Benchmark
fun benchmark() {
heavyOperation()
}
41-2. inline / value classでゼロコスト
// 高頻度呼び出しにオーバーヘッドなし
inline fun measureBlock(block: () -> Unit): Long { ... }
@JvmInline
value class UserId(val value: Int)
41-3. Sequenceの活用
// list: 中間Listを毎回生成
list.filter { ... }.map { ... }.first()
// sequence: 必要分だけ計算
list.asSequence().filter { ... }.map { ... }.first()
巨大コレクションで効果絶大。
41-4. プリミティブvsボクシング
// IntArray: int[] 相当
val arr = IntArray(1000) { it }
// List<Int>: List<Integer> (ボクシングあり)
val list = List(1000) { it }
// 性能優先ならIntArray / FloatArray / ...
41-5. coroutineの選択
ブロッキング処理:
Dispatchers.IO(多めのスレッドプール)
CPUバウンド:
Dispatchers.Default(コア数のスレッドプール)
UI:
Dispatchers.Main
41-6. このセクションのまとめ
- measureTimeMillis / JMHで計測
- inline関数でラムダコスト削減
- Sequenceで中間コレクション回避
- IntArrayでボクシング回避
- 適切なDispatcher選択
42. Kotlin周辺ツール
42-1. ktlint / detekt
# ktlint
ktlint .
ktlint --format .
# detekt
./gradlew detekt
ktlint はフォーマット、detekt はより複雑なlint。CI必須。
42-2. Spek / Kotest
スペックスタイルのテスト。
42-3. dokka
./gradlew dokkaHtml
KDocからHTMLドキュメント生成。
42-4. Gradle Version Catalog
# gradle/libs.versions.toml
[versions]
kotlin = "2.0.0"
coroutines = "1.8.0"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
// build.gradle.kts
dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines)
}
依存バージョンを 一元管理。マルチモジュールで重宝。
42-5. このセクションのまとめ
- ktlint / detektで品質
- Spek / Kotestでスペック風
- dokkaでドキュメント
- Version Catalogで依存統一
43. 実践プロジェクト構成
my-project/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle.properties
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
├── app/ # Androidアプリ(Multiplatformなら)
├── shared/ # 共通モジュール
│ ├── src/
│ │ ├── commonMain/
│ │ ├── androidMain/
│ │ ├── iosMain/
│ │ └── commonTest/
│ └── build.gradle.kts
├── server/ # JVMサーバ(Ktor等)
└── README.md
44. 拡張FAQ
Q1. data classを継承できる?
通常はできない。Kotlin 1.1+ で @Suppress("ClassMustBeAbstract") open で可能だが、copy() の挙動が複雑になり推奨されない。
Q2. varを完全に避けるべき?
理想はそうだが、ローカルでの更新変数や、フレームワークが要求する場合は使う。フィールドはなるべくval。
Q3. lateinit使いすぎ
null安全性を捨てるので避けたい。by lazy や constructor注入で代替できないか検討。
Q4. companion objectの使い所
ファクトリメソッド、定数、static相当が必要なとき。object 単体でも可。
Q5. typealiasとvalue class
typealias UserId = Int // 単なる別名(同じ型)
@JvmInline value class UserId(val v: Int) // 別の型
型安全性が必要ならvalue class。
Q6. JavaとKotlinのヌル相互運用
Javaから来た値は プラットフォーム型(String!)。Java側で @Nullable / @NonNull を付けると正しく解釈。
Q7. extension function vs member function
member: クラス内、サブクラスでオーバーライド可
extension: クラス外、overrideできない(静的解決)
ユーティリティはextension、本質的振る舞いはmember。
Q8. sealedとenum
sealed: 各サブクラスが異なる型・データを持てる、ADT
enum: 固定の値の列挙、メソッドは持てる
複数の状態が異なるデータを持つならsealed。
Q9. Coroutines Flow vs RxJava
Flowはcoroutinesネイティブ、coldストリーム、簡潔。RxJavaはより成熟、機能豊富。新規はFlow推奨。
Q10. Kotlin/JSの現状
JetBrainsが継続投資。React連携も改善中。Kotlin MultiplatformでiOS/AndroidとWebを同じコードで書ける可能性。
実践: Androidとサーバサイド
46. ジェネリクスと変位の実例
46-1. 関数型のジェネリクス
typealias Mapper<A, B> = (A) -> B
fun <A, B, C> compose(f: Mapper<B, C>, g: Mapper<A, B>): Mapper<A, C> =
{ a -> f(g(a)) }
val intToString: Mapper<Int, String> = { it.toString() }
val stringLength: Mapper<String, Int> = { it.length }
val intToLength = compose(stringLength, intToString)
intToLength(123) // 3
46-2. 型推論の限界
// 型注釈なしでは推論できない
val list = mutableListOf<String>() // 明示
val map = mutableMapOf<String, Int>()
// 推論可能なケース
val list = listOf("a", "b")
val map = mapOf("a" to 1)
46-3. 自作コレクション
class Stack<T> {
private val items = mutableListOf<T>()
fun push(item: T) { items.add(item) }
fun pop(): T? = items.removeLastOrNull()
fun peek(): T? = items.lastOrNull()
val size: Int get() = items.size
val isEmpty: Boolean get() = items.isEmpty()
}
val stack = Stack<Int>()
stack.push(1)
stack.push(2)
println(stack.pop()) // 2
47. コルーチンの実用パターン
47-1. Repositoryパターン
class UserRepository(
private val api: UserApi,
private val cache: UserDao
) {
fun getUsers(): Flow<List<User>> = flow {
val cached = cache.getAll()
if (cached.isNotEmpty()) emit(cached)
try {
val fresh = api.fetchAll()
cache.insertAll(fresh)
emit(fresh)
} catch (e: Exception) {
if (cached.isEmpty()) throw e
}
}.flowOn(Dispatchers.IO)
}
47-2. デバウンス検索
class SearchViewModel(private val repo: SearchRepository) : ViewModel() {
private val _query = MutableStateFlow("")
val results: StateFlow<List<Result>> = _query
.debounce(300)
.filter { it.isNotBlank() }
.distinctUntilChanged()
.mapLatest { query -> repo.search(query) }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
fun setQuery(q: String) { _query.value = q }
}
mapLatest で 古い検索をキャンセルし新しいものだけ実行。ComposeやReact Query風。
47-3. 並行API呼び出し
suspend fun fetchUserDetails(id: Int): UserDetails = coroutineScope {
val user = async { userApi.fetch(id) }
val orders = async { orderApi.fetchByUser(id) }
val activities = async { activityApi.fetchByUser(id) }
UserDetails(
user = user.await(),
orders = orders.await(),
activities = activities.await()
)
}
3つを並行に取得。Promise.all相当。
47-4. リトライロジック
suspend fun <T> retry(
maxAttempts: Int = 3,
delayMs: Long = 1000,
block: suspend () -> T
): T {
var lastError: Throwable? = null
repeat(maxAttempts) { attempt ->
try {
return block()
} catch (e: Exception) {
lastError = e
if (attempt < maxAttempts - 1) {
delay(delayMs * (1L shl attempt)) // exponential backoff
}
}
}
throw lastError ?: IllegalStateException()
}
// 使用
val data = retry { api.fetch() }
47-5. このセクションのまとめ
- RepositoryはFlow + flowOn(IO)
- mapLatestで「最新のみ実行」
- async / awaitで並行
- retry + exponential backoff
- viewModelScopeで自動キャンセル
48. 現代のAndroidアーキテクチャ
推奨スタック(Google):
UI: Jetpack Compose
Navigation: Navigation Compose
ViewModel: ViewModel + StateFlow
Repository: Flow + Result
DB: Room
Network: Retrofit + OkHttp + Kotlinx Serialization
DI: Hilt
Image: Coil
Background: WorkManager
Auth: CredentialsManager
Test: Espresso / Compose Test
Clean Architecture
presentation/ Composables, ViewModels
domain/ UseCases, Entities, Repositories interface
data/ Repository impl, DataSource, DTO
現代のAndroidでは、UIをCompose、状態をViewModel、非同期ストリームをFlowで扱う構成が中心になる。重要なのは、Composeの再コンポジションに耐える状態設計である。UI状態はイミュータブルなdata classにまとめ、イベントはViewModelへ送り、Repositoryはデータ取得の詳細を隠す。
Clean Architectureは便利だが、画面が小さい段階で層を増やしすぎると逆に読みにくい。ドメインロジックが薄いCRUD画面では、ViewModelとRepositoryだけで十分なこともある。課金、同期、オフライン対応、権限、複雑な検証が出てきたときに、UseCaseやDomain Layerを厚くするのが現実的である。
49. Server-Side Kotlin
49-1. Spring Boot Kotlin
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
@RestController
@RequestMapping("/api/users")
class UserController(private val service: UserService) {
@GetMapping
suspend fun getAll() = service.findAll()
@GetMapping("/{id}")
suspend fun get(@PathVariable id: Long): User =
service.findById(id) ?: throw NotFoundException()
@PostMapping
suspend fun create(@RequestBody @Valid req: CreateUserRequest): User =
service.create(req)
}
@Service
class UserService(private val repo: UserRepository) {
suspend fun findAll(): List<User> = withContext(Dispatchers.IO) {
repo.findAll()
}
}
Spring Bootが 公式にKotlin/coroutines対応。WebFlux + coroutinesでreactiveバックエンドが書ける。
49-2. Ktor
fun main() {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) { json() }
install(StatusPages) {
exception<NotFoundException> { call, _ ->
call.respond(HttpStatusCode.NotFound)
}
}
routing {
route("/api/users") {
get {
call.respond(userService.findAll())
}
get("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest)
val user = userService.findById(id)
?: return@get call.respond(HttpStatusCode.NotFound)
call.respond(user)
}
post {
val req = call.receive<CreateUserRequest>()
val user = userService.create(req)
call.respond(HttpStatusCode.Created, user)
}
}
}
}.start(wait = true)
}
JetBrains製。coroutinesネイティブで軽量・高速。
49-3. Exposed(DSLベースORM)
object Users : Table() {
val id = long("id").autoIncrement()
val name = varchar("name", 50)
val email = varchar("email", 100).uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
transaction {
SchemaUtils.create(Users)
Users.insert {
it[name] = "Alice"
it[email] = "a@b.c"
}
val users = Users.selectAll().map {
it[Users.name] to it[Users.email]
}
}
JetBrains製。Kotlinらしい型安全なSQL DSL。
49-4. このセクションのまとめ
- Spring Boot Kotlin(業務での主流)
- Ktor(軽量、JetBrains)
- Exposed(型安全ORM)
- gRPC、GraphQL、RESTすべて対応
50. Kotlinの周辺と他言語比較
50-1. Javaとの比較
Kotlin: Java:
- null安全 - @Nullableのみ
- data class - record(Java 14+)
- 拡張関数 - なし
- coroutines - Virtual Threads(Java 21+)
- val / var - final / 普通
- Multiplatform - JVMのみ
- 演算子オーバーロード - なし
- リファイドジェネリクス - 型消去のみ
Java 21で差は縮まったが、Kotlinの一貫した簡潔さは依然として優位。
50-2. Scalaとの比較
Kotlin: Scala:
- 実用主義 - 学術寄り、機能豊富
- コンパイル速い - 遅い
- Java互換に重点 - JVMだが文化が違う
- coroutines - cats-effect / ZIO
- Spring等と相性 - Akkaなど
50-3. Swiftとの比較
Kotlin: Swift:
- JVMベース - LLVMベース
- Androidが主戦場 - Appleが主戦場
- coroutines - async/await + actor
- nullable T? - Optional<T> / T?
- MultiplatformでiOSも - クロス苦手(vapor等)
設計が 驚くほど似ている(双方が影響を受けた)。
50-4. このセクションのまとめ
- Java 21で差は縮まったがKotlinが依然簡潔
- Scalaより実用主義
- Swiftと「兄弟」関係(似た設計)
- MultiplatformがKotlinの大きな差別化
51. Kotlin学習リソース
書籍
- 『Kotlin in Action』Dmitry Jemerov, Svetlana Isakova
- 『Programming Kotlin』Stephen Samuel
- 『Kotlin in Depth』Aleksei Sedunov
- 『Atomic Kotlin』Bruce Eckel
Web
- kotlinlang.org(公式)
- Kotlin Koans(対話的演習)
- play.kotlinlang.org(ブラウザ実行)
- discuss.kotlinlang.org
コミュニティ
- /r/Kotlin(Reddit)
- Kotlin Slack
- KotlinConf(年次カンファレンス)
- JetBrains Blog
応用: ComposeとKMP
53. Composeのさらなる深掘り
53-1. recompositionの仕組み
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
// countが変わると、この関数全体が再実行される(recomposition)
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
「stateが変わったら、それに依存するComposableだけ再描画」。Reactのre-renderに近いが、より細かい単位。
53-2. side effectとLaunchedEffect
@Composable
fun UserScreen(userId: Int) {
var user by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
user = api.fetchUser(userId) // userIdが変わったら再実行
}
user?.let { Text(it.name) }
}
LaunchedEffect(key) で keyが変わったときだけ実行するコルーチン。
DisposableEffect(Unit) {
val listener = SomeListener(...)
register(listener)
onDispose {
unregister(listener)
}
}
DisposableEffect で クリーンアップを実装。
53-3. state hoisting
// Bad: stateが内部に閉じ込められる
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}
// Good: 親がstateを持つ
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("$count") }
}
@Composable
fun ParentScreen() {
var count by remember { mutableStateOf(0) }
Counter(count) { count++ }
}
「stateを親に持たせる」ことで再利用・テスト容易性が上がる。Reactのlifting state upと同じ。
53-4. derivedStateOf
val list by remember { mutableStateOf(listOf<Item>()) }
val filteredCount by remember(list) {
derivedStateOf { list.count { it.active } }
}
「派生する状態」を効率化。listが変わったときだけ再計算。
53-5. Layoutの自作
@Composable
fun MyLayout(content: @Composable () -> Unit) {
Layout(content = content) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
var y = 0
placeables.forEach {
it.place(0, y)
y += it.height
}
}
}
}
ComposeのLayout APIで 自作レイアウト。
53-6. このセクションのまとめ
- mutableStateOf + rememberでreactive state
- LaunchedEffectでコルーチン
- DisposableEffectでクリーンアップ
- state hoistingで再利用
- derivedStateOfで派生状態
- Layout APIで自作レイアウト
54. KMP(Kotlin Multiplatform)のさらに
54-1. iOSとの実際の連携
// shared/src/commonMain/.../Greeting.kt
class Greeting {
fun greet(): String = "Hello from KMP!"
}
# XCFramework生成
./gradlew :shared:assembleSharedXCFramework
// Swift側
import shared
let greeting = Greeting()
print(greeting.greet()) // "Hello from KMP!"
54-2. Compose MultiplatformでUI共通化
// commonMain
@Composable
fun App() {
MaterialTheme {
Counter()
}
}
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
このコードが Android / iOS / Desktop / Web すべてで動く。100% UI共通化が現実的に。
54-3. SQLDelight(Multiplatform DB)
-- shared/src/commonMain/sqldelight/User.sq
CREATE TABLE User (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
selectAll:
SELECT * FROM User;
insert:
INSERT INTO User(id, name, email) VALUES (?, ?, ?);
// Kotlin側で型安全に
val users: List<User> = database.userQueries.selectAll().executeAsList()
database.userQueries.insert(1, "Alice", "a@b.c")
「SQLを書くとKotlin型が生成」されるORM。Android(SQLite)/ iOS(SQLite)/ JVM / JSで動く。
54-4. KMPの現在地
成熟領域:
- データレイヤー(Repository、ネットワーク、DB)
- ビジネスロジック
- ViewModels(一部)
成熟途上:
- Compose Multiplatform UI(iOSがbeta)
- WASMターゲット
- 大規模アプリでの実績増加中
成功事例:
- Cash App(Square)
- Netflix
- Trip.com(CTrip)
- Touchlab、Phillips、Apptly
54-5. このセクションのまとめ
- iOS配信はXCFramework経由
- Compose MultiplatformでUI共通化
- SQLDelightで型安全SQL
- 業務利用が増加中、特にデータレイヤー共通化
55. 完全な実例:TODOアプリ
// data/Todo.kt
@Serializable
data class Todo(
val id: Long = 0,
val title: String,
val completed: Boolean = false,
val createdAt: Long = System.currentTimeMillis()
)
// data/TodoRepository.kt
class TodoRepository {
private val _todos = MutableStateFlow<List<Todo>>(emptyList())
val todos: StateFlow<List<Todo>> = _todos.asStateFlow()
fun add(title: String) {
if (title.isBlank()) return
val newTodo = Todo(
id = System.currentTimeMillis(),
title = title.trim()
)
_todos.update { it + newTodo }
}
fun toggle(id: Long) {
_todos.update { list ->
list.map { if (it.id == id) it.copy(completed = !it.completed) else it }
}
}
fun remove(id: Long) {
_todos.update { it.filter { todo -> todo.id != id } }
}
}
// ui/TodoViewModel.kt
class TodoViewModel(private val repo: TodoRepository) : ViewModel() {
val todos = repo.todos
fun addTodo(title: String) = repo.add(title)
fun toggleTodo(id: Long) = repo.toggle(id)
fun removeTodo(id: Long) = repo.remove(id)
}
// ui/TodoScreen.kt
@Composable
fun TodoScreen(vm: TodoViewModel) {
val todos by vm.todos.collectAsState()
var input by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
Row {
TextField(
value = input,
onValueChange = { input = it },
modifier = Modifier.weight(1f),
label = { Text("New todo") }
)
Button(onClick = {
vm.addTodo(input)
input = ""
}) {
Text("Add")
}
}
LazyColumn {
items(todos, key = { it.id }) { todo ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
Checkbox(
checked = todo.completed,
onCheckedChange = { vm.toggleTodo(todo.id) }
)
Text(
todo.title,
modifier = Modifier.weight(1f),
textDecoration = if (todo.completed)
TextDecoration.LineThrough else null
)
IconButton(onClick = { vm.removeTodo(todo.id) }) {
Icon(Icons.Default.Delete, "Delete")
}
}
}
}
}
}
これだけで 完全に動くTODOアプリ。Composeの宣言的UI、StateFlowのreactive、unidirectional data flowがすべて見える。
56. Kotlinのための雑多な知識
56-1. typealias
typealias UserId = Long
typealias Callback<T> = (T) -> Unit
typealias Handler = suspend () -> Unit
型に別名。長いgeneric型を短くできる。
56-2. infix関数
infix fun Int.times(other: Int): Int = this * other
infix fun <K, V> K.to(v: V): Pair<K, V> = Pair(this, v)
3 times 4 // 12
"a" to 1 // Pair("a", 1)
infix で 空白区切りの中置記法。to は標準でinfix関数。
56-3. operator関数
data class Vec(val x: Int, val y: Int) {
operator fun plus(other: Vec) = Vec(x + other.x, y + other.y)
operator fun times(scalar: Int) = Vec(x * scalar, y * scalar)
operator fun get(index: Int) = if (index == 0) x else y
}
val a = Vec(1, 2)
val b = Vec(3, 4)
a + b // Vec(4, 6)
a * 3 // Vec(3, 6)
a[0] // 1
演算子オーバーロード。
56-4. destructuring
val (name, age) = Person("Alice", 30)
val (a, b, c) = Triple(1, 2, 3)
val (key, value) = "a" to 1
for ((k, v) in mapOf("a" to 1, "b" to 2)) {
println("$k=$v")
}
componentN() を持つ型で分解可能。data classは自動で生成。
56-5. Rangeとprogression
val range = 1..10 // IntRange
val downRange = 10 downTo 1
val stepRange = 1..10 step 2 // 1, 3, 5, 7, 9
val openRange = 1 until 10 // 1..9
for (i in 1..10) { ... }
if (x in 1..10) { ... }
56-6. whenは式
val description = when {
x < 0 -> "negative"
x == 0 -> "zero"
else -> "positive"
}
val area = when (shape) {
is Circle -> Math.PI * shape.radius * shape.radius
is Square -> shape.side * shape.side
is Rectangle -> shape.width * shape.height
}
56-7. Result型
val result: Result<Int> = runCatching { "42".toInt() }
result.onSuccess { println("got $it") }
.onFailure { println("error: $it") }
.getOrDefault(0)
val mapped: Result<String> = result.map { it.toString() }
val recovered = result.recover { e -> e.message?.length ?: 0 }
エラーを値として扱う。RustのResult風。
57. Kotlinのキャリアと業界
業務利用:
- Android開発 (Google First Class、世界中のAndroidアプリの過半数)
- JVMバックエンド (Spring Kotlin、Ktor、Vert.x)
- Multiplatform (iOS / Web共通化)
- Server-side(Stripe、Netflix、Pinterest)
求人:
- Android開発者はKotlinが標準スキル
- JVMバックエンドでも増加中
- Multiplatformは新興だが伸びている
学習投資:
- Java経験者は2週間で実用可
- Spring / Ktorで業務API
- ComposeでUI
- 6ヶ月でジュニアAndroidエンジニア相当
59. Kotlin実装チェックリスト
59-1. 新規プロジェクト
☐ Kotlin 2.0以上
☐ K2コンパイラ有効
☐ build.gradle.kts(Groovyではなく)
☐ Version Catalog(gradle/libs.versions.toml)
☐ ktlint / detektをCIに
☐ Nullable警告を有効
☐ JVM Toolchainで21
☐ kotlinx-coroutinesを使う
59-2. コード品質
☐ valを優先、varは最小限
☐ data classでデータクラス
☐ sealed classで網羅的状態
☐ Result / Flowでエラー・ストリーム
☐ scope functionを場面で使い分け
☐ extension functionでユーティリティ
☐ value classで意味的型
☐ inlineで関数オブジェクト排除
59-3. パフォーマンス
☐ Sequenceで中間List回避
☐ IntArrayでボクシング回避
☐ inline + reifiedで型情報保持
☐ Dispatcherを適切に選ぶ
☐ Composeでremember / derivedStateOf
☐ -O最適化(Production)
59-4. テスト
☐ JUnit 5かKotest
☐ MockKでモック
☐ runTestでcoroutinesテスト
☐ Compose TestでUIテスト
☐ カバレッジ計測
Android での Kotlin 活用
Jetpack Compose の革新性
Jetpack Composeは、Android UIを宣言的に記述するためのフレームワークです。
Jetpack Compose (2021リリース)は、Android 上で従来の View ベース UI から React/Flutter に近い宣言的 UI へ移行させました。
Compose と従来の XML レイアウトの比較
// 従来: XML + Kotlin コード (分離)
// res/layout/user_card.xml
<LinearLayout>
<TextView android:id="@+id/name" />
<Button android:id="@+id/edit_btn" />
</LinearLayout>
// MainActivity.kt
val nameView = findViewById<TextView>(R.id.name)
nameView.text = user.name
editBtn.setOnClickListener { onEdit() }
// Compose: 1つのコード
@Composable
fun UserCard(user: User, onEdit: () -> Unit) {
Column {
Text(user.name)
Button(onClick = onEdit) { Text("Edit") }
}
}
Compose の利点:
- レイアウトとロジックが同じ言語(Kotlin)で表現
- リコンポーズ最適化で効率的な再描画
- XML パースのオーバーヘッドなし
Compose の再コンポーズ最適化
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users) { user ->
UserCard(user) // user が変わったときのみ再コンポーズ
}
}
}
Compose はスマート差分検出により、変更がない部分は再描画しません。
Kotlin Coroutine と Android ライフサイクル
Kotlin Coroutine は、コールバック地獄から解放します(developer.android.com)。
// コールバック地獄
userRepository.getUser(id,
onSuccess = { user ->
userRepository.getOrders(user.id,
onSuccess = { orders ->
// 深いネスト...
},
onError = { /* error */ }
)
},
onError = { /* error */ }
)
// Coroutine (sequential に読める)
viewModelScope.launch {
val user = userRepository.getUser(id)
val orders = userRepository.getOrders(user.id)
updateUI(user, orders)
}
viewModelScope.launch は、ViewModel が破棄されると自動的に Coroutine をキャンセル。
ViewModel + Coroutine の組み合わせ
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: Int) {
viewModelScope.launch {
try {
val user = userRepository.getUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
}
// UI層
@Composable
fun UserScreen(viewModel: UserViewModel) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> UserCard((uiState as Success).user)
is UiState.Error -> ErrorText((uiState as Error).message)
}
}
Kotlin DSL (Domain Specific Language)
Kotlin の高階関数により、DSL のような API が実現できます(developer.android.com):
// layoutDsl のような DSL
Box(modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Column {
repeat(5) { index ->
Text("Item $index", modifier = Modifier.padding(8.dp))
}
}
}
// Gradle の buildscript (KTS: Kotlin-based build script)
plugins {
kotlin("android")
kotlin("plugin.serialization")
}
android {
compileSdk = 34
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
}
}
dependencies {
implementation("androidx.compose.ui:ui:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0")
}
Kotlin Multiplatform Mobile (KMM)
Kotlin 1.4+ では、共通ロジックを iOS/Android で共有できます:
// shared/src/commonMain/kotlin
fun validateEmail(email: String): Boolean = email.contains("@")
// Android と iOS で同じ実装を使う
ただし、UI層は各プラットフォーム(Jetpack Compose for Android, SwiftUI for iOS)で個別実装。
Android の依存性注入(Dependency Injection)
Hilt(Google が推奨)は、Kotlin の annotation によって DI コンテナを自動生成:
@HiltAndroidApp
class MyApplication : Application() {
// Hilt を有効化
}
// Module (依存関係定義)
@Module
@InstallIn(SingletonComponent::class)
object UserRepositoryModule {
@Provides
fun provideUserRepository(
api: UserApi,
db: UserDatabase
): UserRepository = UserRepositoryImpl(api, db)
}
// 注入
@AndroidEntryPoint
class UserViewModel @Inject constructor(
val userRepository: UserRepository
) : ViewModel() {
// userRepository は自動的に注入される
}
デバッグとプロファイリング
Kotlinアプリのデバッグでは、IDE、ログ、プロファイラ、UI検査ツールを組み合わせます。
- Android Studio Debugger: ブレークポイント、ステップ実行
- Logcat: アプリケーションログ
- Profiler: CPU, メモリ, ネットワーク, バッテリ使用量
- Layout Inspector: UI ツリーの検査
// ログ出力
Log.d("UserViewModel", "User loaded: ${user.name}")
// プロファイリング (Tracing)
Trace.beginSection("loadUsers")
val users = userRepository.getUsers()
Trace.endSection()
メモリリークは View.post { view.clearFocus() } で消えることがあり、Fragment の onDestroyView で null 化が推奨。
Google Play への配信とバージョニング
developer.android.com での配信ガイドライン:
versionCode = 1 // Google Play 上でのバージョン番号(必ず増加)
versionName = "1.0" // ユーザーに見えるバージョン
AAB (Android App Bundle) 形式で配信。Google Play が端末に応じて最小 APK を生成。
APK サイズ最適化:
// build.gradle.kts
android {
bundle {
language.enableSplit = true // 言語別に分割
density.enableSplit = true // 解像度別に分割
abi.enableSplit = true // CPU ABIごとに分割
}
}
Kotlin学習の継続
真理1:null安全は革命
String? と String を区別する型システム。NPEがコンパイル時に防げる。Java開発者なら、これだけでKotlinに移行する価値がある。
真理2:拡張関数は薬であり毒
fun String.lastChar(): Char = this[length - 1]
"Hello".lastChar() // 'o'
便利だが多用すると名前空間が汚れる。コンポーネント設計で慎重に。
真理3:Coroutinesは構造化並行性
launch した子は親のスコープを抜けると自動キャンセル。リソースリークが構造的に防げる。
真理4:MultiplatformでSwiftと共存
iOSとAndroidで70% のロジックを共通化できる時代。Kotlin NativeはApple Siliconでも快適。
真理5:JetBrainsの継続投資
IDEのIntelliJ IDEA / Android Studioが公式サポート。言語の進化が止まらない安心感。
fun main() = println("Kotlin forever, productively yours.")
まとめ
Kotlinは、Java相互運用、null安全、data class、拡張関数、コルーチンを組み合わせて、簡潔で安全なアプリケーションを書きやすくする言語です。Androidだけでなくサーバーサイドや共有ロジックでも、型で意図を表し、非同期処理を構造化する姿勢が重要になります。