Kotlin言語の文法解説
基本文法から、Null安全性・オブジェクト指向・コレクション操作・コルーチン、さらにデータ構造やアルゴリズムの実装、JVMの仕組みまで、基本情報技術者試験レベルを目指して解説します。
文法解説
1. コメント
1行コメントは//、複数行コメントは/* ... */で書きます。Kotlinの複数行コメントはネスト(入れ子)が可能です。
// これは1行コメント
/*
* これは複数行コメント
* /* ネストも可能 */
*/
fun main() {
println("Hello, Kotlin!") // 出力文の後ろにもコメントを書ける
}
2. 変数(val / var)
再代入不可の変数はval(value、Javaのfinal相当)、再代入可能な変数はvar(variable)で宣言します。予期しない値変更によるバグを防ぐため、基本的にはvalを優先するのがKotlinの推奨スタイルです。
val name: String = "太郎" // 再代入不可
var age: Int = 20 // 再代入可能
age += 1
// name = "次郎" // コンパイルエラー:valは再代入できない
println("$name さんは $age 歳です")
3. 静的型付けと型推論、基本型
Kotlinはコンパイル時に型が確定する静的型付け言語です。ただし右辺の値から型を推測する型推論があるため、多くの場面で型注釈を省略できます。基本型にはInt・Long・Double・Float・Boolean・Char・Stringなどがあり、JavaのプリミティブとBoxingを意識せず統一的に扱えます。
val score = 100 // Int と推論される
val pi = 3.14 // Double と推論される
val label: String = "合格" // 明示的に型注釈を付けることも可能
val isPassed: Boolean = score >= 60
// 型が異なる代入はコンパイルエラーになる(静的型付けの利点)
// val x: Int = "文字列" // コンパイルエラー
4. 制御構文(if / when / for / while)
Kotlinのifは文だけでなく式としても使え、値を返せます。whenはJavaのswitch文を拡張した分岐構文で、範囲(in)や複数条件、任意の式を条件に取れます。繰り返しにはforとwhileがあります。
val score = 75
// ifは式として値を返せる
val grade = if (score >= 80) "優" else if (score >= 60) "良" else "再履修"
// when式:範囲や複数条件で分岐
val comment = when (score) {
in 80..100 -> "優"
in 60..79 -> "良"
50, 55 -> "ボーダーライン"
else -> "再履修"
}
println("$grade / $comment")
for (i in 0 until 3) { // 0, 1, 2
println("for: $i")
}
var count = 0
while (count < 3) {
println("while: $count")
count++
}
5. 関数(デフォルト引数・名前付き引数・拡張関数・ラムダ)
funで関数を定義します。引数にはデフォルト値を設定でき、呼び出し時には名前付き引数で可読性を高められます。既存クラスに手を加えずメソッドを追加できる拡張関数や、関数を値として扱うラムダ式・高階関数もKotlinらしい特徴です。
// デフォルト引数
fun greet(name: String, honorific: String = "さん"): String {
return "$name$honorific、こんにちは"
}
fun square(x: Int) = x * x // 単一式関数
// 拡張関数:既存のStringクラスにメソッドを追加
fun String.addExclamation() = "$this!"
// 高階関数:関数を引数に取る
fun operate(a: Int, b: Int, op: (Int, Int) -> Int): Int = op(a, b)
fun main() {
println(greet("花子")) // 名前を省略
println(greet(name = "次郎", honorific = "君")) // 名前付き引数
println("すごい".addExclamation())
println(operate(3, 4) { a, b -> a + b }) // ラムダ式を末尾に渡す
}
6. Null安全性(Safe Call / Elvis演算子 / 非null表明)
Kotlinの大きな特徴が、型システムでnullを厳密に扱うNull安全性です。null許容型は型名の後ろに?を付けて宣言します。?.(Safe Call)はレシーバがnullなら処理全体をnullにし、?:(Elvis演算子)はnullの場合のデフォルト値を指定します。!!(非null表明)はnullでないことを開発者が保証しますが、実際にnullだとNullPointerExceptionが発生するため多用は避けます。
var nickname: String? = null // null許容型
val length1 = nickname?.length // Safe Call:nullなら結果もnull
val length2 = nickname?.length ?: 0 // Elvis演算子:nullなら0を使う
println("length1=$length1, length2=$length2")
nickname = "たろちゃん"
val length3 = nickname!!.length // 非null表明:nullなら例外発生(多用は非推奨)
println(length3)
7. クラスと継承・インターフェース
クラスはclassで定義します。Kotlinのクラスはデフォルトで継承不可(final)のため、継承させたい親クラスにはopenを付ける必要があります。interfaceで定義したインターフェースを:で実装し、overrideでメソッドをオーバーライドすることでポリモーフィズム(多態性)を実現します。
interface Speaker {
fun speak(): String
}
open class Animal(val name: String) : Speaker {
override fun speak() = "${name}が鳴きました"
}
class Dog(name: String) : Animal(name) {
override fun speak() = "$name: ワン!" // オーバーライドでポリモーフィズム
}
fun main() {
val animals: List = listOf(Animal("何か"), Dog("ポチ"))
for (a in animals) {
println(a.speak()) // 実際の型に応じたメソッドが呼ばれる
}
}
8. data classとobject(シングルトン)
データ保持用のクラスにはdata classを使うと、equals()・hashCode()・toString()・copy()が自動生成されます。アプリ内で唯一のインスタンスだけを持ちたい場合はobject宣言でシングルトンを簡潔に実装できます。
data class Point(val x: Int, val y: Int)
object AppConfig {
var appName: String = "プログラミングステーション"
fun printInfo() = println("App: $appName")
}
fun main() {
val p1 = Point(1, 2)
val p2 = p1.copy(x = 100) // 一部だけ変更したコピーを生成
println(p2) // data classはtoStringも自動生成
println(p1 == Point(1, 2)) // data classはequalsも自動生成 -> true
AppConfig.printInfo() // object はシングルトンなのでインスタンス化不要
}
9. コレクション操作(filter / map / reduceなど)
KotlinのList・Set・Mapには関数型プログラミングのスタイルで扱える豊富な拡張関数が用意されています。filter(絞り込み)、map(変換)、reduce/fold(畳み込み)、sortedBy(並び替え)などを組み合わせることで、for文を書かずに簡潔にデータ処理できます。
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evens = numbers.filter { it % 2 == 0 } // [2, 4, 6, 8, 10]
val squared = evens.map { it * it } // [4, 16, 36, 64, 100]
val sum = squared.reduce { acc, n -> acc + n } // 220(累積計算)
val sumWithInitial = squared.fold(0) { acc, n -> acc + n } // 初期値付き畳み込み
println("evens=$evens")
println("squared=$squared, sum=$sum, sumWithInitial=$sumWithInitial")
val sortedDesc = numbers.sortedByDescending { it } // 降順ソート
println(sortedDesc)
10. 例外処理
例外処理はtry / catch / finallyで行います。Kotlinには検査例外(checked exception)の区別がなく、すべての例外が非検査例外として扱われるため、Javaのようにthrows宣言を強制されません。tryは式としても使え、値を返せます。
fun divide(a: Int, b: Int): Int {
return try {
a / b
} catch (e: ArithmeticException) {
println("エラー: ${e.message}")
0
} finally {
println("処理を終了します")
}
}
fun main() {
println(divide(10, 2)) // 5
println(divide(10, 0)) // 0(0除算エラーをキャッチ)
}
11. コルーチンによる非同期処理
Kotlinの非同期処理は、スレッドより軽量なコルーチン(Coroutine)という仕組みで実現します。suspend関数は処理を中断・再開できる関数で、launch(結果を返さない起動)やasync/await(結果を返す並行実行)、runBlocking(コルーチンを同期的に実行するブリッジ)などのビルダーと組み合わせて使います。
import kotlinx.coroutines.*
suspend fun fetchUser(): String {
delay(1000) // 1秒待つ(スレッドをブロックしない)
return "ユーザーA"
}
suspend fun fetchOrder(): String {
delay(1000)
return "注文#123"
}
fun main() = runBlocking {
// asyncで2つの処理を並行実行し、await()で結果を待つ
val userDeferred = async { fetchUser() }
val orderDeferred = async { fetchOrder() }
// 直列に書けば2秒かかるが、並行実行のため約1秒で完了する
println("${userDeferred.await()} の ${orderDeferred.await()} を取得しました")
}
12. データ構造の実装:スタック
スタックはLIFO(Last In First Out:後入れ先出し)のデータ構造で、末尾への追加(push)と末尾からの取り出し(pop)だけを行います。Kotlinでは可変長リストMutableListをラップして自作するか、標準のArrayDequeを利用できます。push/popともに末尾操作のみのため計算量はO(1)です。
class Stack {
private val items = mutableListOf()
fun push(item: T) { // O(1)
items.add(item)
}
fun pop(): T? { // O(1)
return if (items.isEmpty()) null else items.removeAt(items.size - 1)
}
fun peek(): T? = items.lastOrNull()
fun isEmpty(): Boolean = items.isEmpty()
}
fun main() {
val stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
println(stack.pop()) // 3(最後に入れたものが先に出る)
println(stack.peek()) // 2
}
13. アルゴリズムと計算量(Big-O):線形探索と二分探索
線形探索は先頭から順に比較していく方法で、計算量はO(n)です。二分探索はソート済み配列に対して中央値と比較しながら探索範囲を半分に絞り込む方法で、計算量はO(log n)と非常に高速です。ただし二分探索には事前にデータがソートされている必要があります。
// 線形探索:計算量 O(n)
fun linearSearch(list: List, target: Int): Int {
for (i in list.indices) {
if (list[i] == target) return i
}
return -1
}
// 二分探索:計算量 O(log n)(sorted済みの配列が前提)
fun binarySearch(sorted: List, target: Int): Int {
var low = 0
var high = sorted.size - 1
while (low <= high) {
val mid = (low + high) / 2
when {
sorted[mid] == target -> return mid
sorted[mid] < target -> low = mid + 1
else -> high = mid - 1
}
}
return -1
}
fun main() {
val data = listOf(1, 3, 5, 7, 9, 11, 13)
println(linearSearch(data, 9)) // 4
println(binarySearch(data, 9)) // 4(比較回数が少なく高速)
}
14. アルゴリズムと計算量(Big-O):クイックソート
クイックソートは基準値(ピボット)より小さい要素と大きい要素に分割し、それぞれを再帰的にソートする分割統治法のアルゴリズムです。平均計算量はO(n log n)ですが、ピボットの選び方が悪いと最悪計算量はO(n²)になります。比較として、単純なバブルソートや選択ソートは常にO(n²)です。
// クイックソート:平均 O(n log n)、最悪 O(n^2)
fun quickSort(list: List): List {
if (list.size <= 1) return list
val pivot = list[list.size / 2]
val less = list.filter { it < pivot }
val equal = list.filter { it == pivot }
val greater = list.filter { it > pivot }
return quickSort(less) + equal + quickSort(greater)
}
fun main() {
val data = listOf(5, 2, 8, 1, 9, 3)
println(quickSort(data)) // [1, 2, 3, 5, 8, 9]
}
15. JVM上での実行とガベージコレクション
KotlinのコードはコンパイルされてJVMバイトコード(.class)になり、Java仮想マシン上で実行されます。そのためJavaのライブラリをそのまま利用でき、実行時の性能特性もJavaに準じます。オブジェクトのメモリ管理はプログラマが明示的に解放するのではなく、ガベージコレクション(GC)が到達不能になったオブジェクトを自動的に回収します。GCの実行タイミングは制御しづらいため、不要なオブジェクト生成を抑えることがパフォーマンス上重要です。
fun createGarbage() {
// ループのたびに新しいStringオブジェクトが生成される
// 参照されなくなったオブジェクトは、後でGCの対象になる
for (i in 0 until 1000) {
val temp = "temp-$i"
// tempはこのブロックを抜けると参照されなくなる
}
// JVMのGCが到達不能になったオブジェクトを回収してメモリを解放する
}
16. パッケージとモジュール(Gradleの基礎)
Kotlinのファイル先頭にはpackage宣言を書き、名前空間の衝突を防ぎます。他パッケージのクラスを使う際はimportで読み込みます。プロジェクトのビルドや依存ライブラリの管理には、多くの場合Gradleというビルドツールを使い、build.gradle.ktsに依存関係やビルド設定を記述します。
// ファイル: com/example/util/StringUtil.kt
package com.example.util
fun shout(text: String) = "$text!!!"
// 別ファイルから利用する場合
// import com.example.util.shout
// build.gradle.kts の依存関係の例(Kotlin DSL)
// dependencies {
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
// testImplementation(kotlin("test"))
// }
処理速度改善のポイント
KotlinはJVM上で動作するため、Javaと同様の最適化の考え方に加え、Kotlin独自の機能を活かすとさらに効率化できます。
- 大きなコレクション処理にはSequenceを使う:map/filterを連続で呼ぶと通常は中間リストが都度生成されますが、
asSequence()で遅延評価にすると中間コレクションの生成を抑えられます。 - インライン関数を活用する:高階関数(ラムダを引数に取る関数)に
inlineを付けると、呼び出し箇所にコードが展開され、ラムダのオブジェクト生成コストを削減できます。 - !!(非null表明)の多用を避ける:Null安全の仕組みを活かし、必要なチェックだけを行うことで、実行時例外のリスクと無駄なチェックコストを減らせます。
- data classのcopy()を必要な範囲で使う:不変データの一部だけ変更したコピーは便利ですが、頻繁な生成はメモリ確保・GCコストがかかる点に留意します。
- コルーチンで軽量な非同期処理を行う:スレッドを直接使うより軽量にI/O待ちや並行処理を扱え、
Dispatchers.IO/Defaultを用途で使い分けます。 - 適切なコレクション型・アルゴリズムを選ぶ:検索が多い場合はListの線形探索(O(n))ではなくHashMap/HashSet(平均O(1))を使い、ソート済みデータの検索には二分探索(O(log n))を活用します。
- 不要なオブジェクト生成を避ける:ループ内で毎回新しいオブジェクトや文字列を生成するとGCの負荷が増えるため、可能な範囲で使い回しやStringBuilderの利用を検討します。
- JVMのJITウォームアップを理解する:起動直後は最適化前のため遅く、繰り返し実行される中でJITコンパイラが効いて高速化されていく特性を踏まえてベンチマークします。