- Published on
Kotlin完全ガイド2025:Spring Bootバックエンドからandroid、Multiplatform、Coroutinesまで
- Authors

- Name
- Youngju Kim
- @fjvbn20031
目次(もくじ)
1. 2025年(ねん)のKotlinの位置(いち)づけ
Kotlinは2025年、JVMエコシステムで最(もっと)も急速(きゅうそく)に成長(せいちょう)している言語(げんご)の一(ひと)つです。GoogleがAndroid開発(かいはつ)の公式(こうしき)言語として採用(さいよう)して以来(いらい)、サーバーサイド、マルチプラットフォーム、データサイエンスまで領域(りょういき)を拡大(かくだい)しています。
Kotlin採用(さいよう)状況(じょうきょう)
| 領域 | 状態 | 主な用途 |
|---|---|---|
| Android | 公式第一言語 | 新規プロジェクトの95%以上がKotlin |
| Spring Boot | 公式サポート(1st class) | コルーチン + WebFlux統合 |
| Ktor | Kotlinネイティブサーバー | 軽量マイクロサービス |
| KMP | Stable(1.9.20+) | iOS/Androidビジネスロジック共有 |
| Compose Multiplatform | BetaからStableへ | Android/Desktop/Web UI共有 |
| データサイエンス | 成長中 | Kotlin DataFrame、KotlinDL |
なぜKotlinなのか
- 簡潔性(かんけつせい): Javaと比(くら)べて40%少(すく)ないコード
- 安全性(あんぜんせい): 型(かた)システムレベルのNull安全性
- 相互運用性(そうごうんようせい): 既存のJavaコードと100%互換
- コルーチン: 構造化された並行性で非同期処理を単純化
- マルチプラットフォーム: 一つのコードベースで複数プラットフォームをサポート
2. Kotlin vs Java:比較(ひかく)とマイグレーション
構文(こうぶん)比較
// Java
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
// Kotlin — 1行で同じ機能
data class User(val name: String, val age: Int)
主要(しゅよう)な違(ちが)い
| 機能 | Java | Kotlin |
|---|---|---|
| Null安全性 | Optional、アノテーション | 型システム内蔵(?/!!) |
| Dataクラス | Record(Java 16+) | data class(1.0から) |
| 拡張関数 | 不可 | ネイティブサポート |
| コルーチン | Project Loom(Virtual Threads) | suspend/launch/async |
| Sealed型 | sealed(Java 17+) | sealed class/interface |
| スマートキャスト | instanceof + キャスティング | 自動キャスティング |
| デフォルトパラメータ | オーバーロード必要 | デフォルト値サポート |
| 文字列テンプレート | Java 21+ STR | 標準サポート |
| スコープ関数 | なし | let、run、apply、also、with |
JavaからKotlinへのマイグレーション
// ステップ1: build.gradle.ktsにKotlinを追加
plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
}
// ステップ2: JavaファイルをKotlinに変換(IntelliJ: Ctrl+Alt+Shift+K)
// ステップ3: Kotlinイディオムを適用
// ステップ4: コルーチンの導入
マイグレーション戦略(せんりゃく):
- 新規(しんき)ファイルはKotlinで作成
- テストからKotlinに転換
- Dataクラスを優先変換
- サービスレイヤーを段階的に転換
- コルーチン導入は最終段階
3. Kotlin核心(かくしん)言語機能
Null安全性(あんぜんせい)
// Nullable型の宣言
var name: String? = null
// 安全呼び出し演算子
val length = name?.length // nullならnullを返す
// エルビス演算子
val length = name?.length ?: 0 // nullならデフォルト値
// スマートキャスト
if (name != null) {
println(name.length) // 自動的にStringにキャスト
}
// letスコープ関数
name?.let { nonNullName ->
println(nonNullName.length)
}
Data Class
data class User(
val id: Long,
val name: String,
val email: String,
val role: Role = Role.USER // デフォルト値
) {
// copyで不変オブジェクトを変更
fun promote() = copy(role = Role.ADMIN)
}
// 分解宣言
val (id, name, email) = user
Sealed Class / Sealed Interface
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing>
}
// when式で網羅性を保証
fun handleResult(result: Result<User>) = when (result) {
is Result.Success -> println("User: ${result.data}")
is Result.Error -> println("Error: ${result.exception.message}")
is Result.Loading -> println("Loading...")
// else不要 — 全ケース処理済み
}
拡張関数(かくちょうかんすう)
// Stringに新しいメソッドを追加
fun String.toSlug(): String =
lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
// 使用例
val slug = "Hello World 2025!".toSlug() // "hello-world-2025"
// コレクション拡張
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
// ジェネリック拡張
inline fun <reified T> Any.castOrNull(): T? = this as? T
委譲(いじょう)(Delegation)
// クラス委譲
interface Repository {
fun findAll(): List<User>
}
class CachedRepository(
private val delegate: Repository
) : Repository by delegate {
private val cache = mutableMapOf<String, List<User>>()
override fun findAll(): List<User> =
cache.getOrPut("all") { delegate.findAll() }
}
// プロパティ委譲
class UserPreferences {
var theme: String by Delegates.observable("light") { _, old, new ->
println("Theme changed: $old -> $new")
}
val expensiveValue: String by lazy {
computeExpensiveValue()
}
}
DSL(ドメイン固有言語)
// HTML DSLの例
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
class HTML {
fun body(init: Body.() -> Unit) { /* ... */ }
}
class Body {
fun p(text: String) { /* ... */ }
fun div(init: Div.() -> Unit) { /* ... */ }
}
// 使用例
val page = html {
body {
p("Hello, Kotlin DSL!")
div {
p("Nested content")
}
}
}
// 実践: Gradle Kotlin DSL
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
}
4. Coroutinesディープダイブ
基本概念(きほんがいねん)
コルーチンはスレッドのように動作(どうさ)する軽量(けいりょう)な非同期処理メカニズムです。実際のOSスレッドをブロックせずにサスペンド(中断)して再開(さいかい)できます。
import kotlinx.coroutines.*
fun main() = runBlocking {
// launch: 結果を返さないコルーチン
val job = launch {
delay(1000L)
println("World!")
}
// async: 結果を返すコルーチン
val deferred = async {
delay(1000L)
42
}
println("Hello,")
job.join()
println("Answer: ${deferred.await()}")
}
suspend関数(かんすう)
// suspend関数はコルーチン内でのみ呼び出し可能
suspend fun fetchUser(id: Long): User {
return withContext(Dispatchers.IO) {
// ネットワーク/DB操作
userRepository.findById(id)
}
}
suspend fun fetchUserWithPosts(id: Long): UserWithPosts {
return coroutineScope {
// 並列実行
val userDeferred = async { fetchUser(id) }
val postsDeferred = async { fetchPosts(id) }
UserWithPosts(
user = userDeferred.await(),
posts = postsDeferred.await()
)
}
}
構造化(こうぞうか)された並行性(へいこうせい)
suspend fun processOrder(orderId: Long) = coroutineScope {
// 子コルーチンが全て完了するまで返さない
val inventory = async { checkInventory(orderId) }
val payment = async { processPayment(orderId) }
// 一つでも失敗すれば残りもキャンセル
try {
val inventoryResult = inventory.await()
val paymentResult = payment.await()
completeOrder(orderId, inventoryResult, paymentResult)
} catch (e: Exception) {
cancelOrder(orderId)
throw e
}
}
Flow(リアクティブストリーム)
import kotlinx.coroutines.flow.*
// Cold Flow — サブスクライブ時に実行
fun numberFlow(): Flow<Int> = flow {
for (i in 1..10) {
delay(100)
emit(i)
}
}
// 演算子チェーン
suspend fun processNumbers() {
numberFlow()
.filter { it % 2 == 0 }
.map { it * it }
.catch { e -> println("Error: $e") }
.collect { value -> println(value) }
}
// StateFlow(UI状態管理)
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: Long) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.fetchUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
}
// SharedFlow(イベントブロードキャスト)
class EventBus {
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
キャンセルとタイムアウト
// コルーチンのキャンセル
val job = launch {
repeat(1000) { i ->
println("Processing $i")
delay(500) // キャンセルポイント
}
}
delay(2500)
job.cancelAndJoin() // キャンセル後、完了を待つ
// タイムアウト
val result = withTimeoutOrNull(3000L) {
fetchDataFromNetwork()
} ?: defaultValue
// キャンセル不可能な作業
suspend fun saveToDb(data: Data) {
withContext(NonCancellable) {
// キャンセルされても必ず実行
database.save(data)
}
}
5. Spring Boot + Kotlin
プロジェクト設定(せってい)
// build.gradle.kts
plugins {
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
kotlin("jvm") version "2.0.0"
kotlin("plugin.spring") version "2.0.0"
kotlin("plugin.jpa") version "2.0.0"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.3")
testImplementation("io.mockk:mockk:1.13.10")
}
WebFlux + コルーチン
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
// suspend関数で非同期エンドポイント
@GetMapping("/{id}")
suspend fun getUser(@PathVariable id: Long): ResponseEntity<UserResponse> {
val user = userService.findById(id)
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(user.toResponse())
}
// Flowでストリーミングレスポンス
@GetMapping(produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun getAllUsers(): Flow<UserResponse> =
userService.findAll().map { it.toResponse() }
@PostMapping
suspend fun createUser(
@Valid @RequestBody request: CreateUserRequest
): ResponseEntity<UserResponse> {
val user = userService.create(request)
return ResponseEntity
.created(URI.create("/api/users/${user.id}"))
.body(user.toResponse())
}
}
@Service
class UserService(private val userRepository: UserRepository) {
suspend fun findById(id: Long): User? =
userRepository.findById(id)
fun findAll(): Flow<User> =
userRepository.findAll().asFlow()
@Transactional
suspend fun create(request: CreateUserRequest): User {
val user = User(
name = request.name,
email = request.email
)
return userRepository.save(user)
}
}
Spring Data R2DBC
interface UserRepository : CoroutineCrudRepository<User, Long> {
suspend fun findByEmail(email: String): User?
@Query("SELECT * FROM users WHERE name LIKE :pattern")
fun searchByName(pattern: String): Flow<User>
@Query("SELECT COUNT(*) FROM users WHERE active = true")
suspend fun countActive(): Long
}
@Table("users")
data class User(
@Id val id: Long? = null,
val name: String,
val email: String,
val active: Boolean = true,
val createdAt: LocalDateTime = LocalDateTime.now()
)
テスト:MockK + Kotest
class UserServiceTest : FunSpec({
val userRepository = mockk<UserRepository>()
val userService = UserService(userRepository)
test("findByIdはユーザーが存在する場合にユーザーを返す") {
// Given
val user = User(id = 1, name = "Kim", email = "kim@test.com")
coEvery { userRepository.findById(1L) } returns user
// When
val result = userService.findById(1L)
// Then
result shouldNotBe null
result!!.name shouldBe "Kim"
coVerify(exactly = 1) { userRepository.findById(1L) }
}
test("findByIdは存在しない場合にnullを返す") {
coEvery { userRepository.findById(any()) } returns null
val result = userService.findById(999L)
result shouldBe null
}
test("createはユーザーを保存して返す") {
val request = CreateUserRequest("Park", "park@test.com")
val savedUser = User(id = 1, name = "Park", email = "park@test.com")
coEvery { userRepository.save(any()) } returns savedUser
val result = userService.create(request)
result.id shouldBe 1
result.name shouldBe "Park"
}
})
6. Ktor:Kotlinネイティブサーバー
プロジェクト構成(こうせい)
// build.gradle.kts
plugins {
kotlin("jvm") version "2.0.0"
kotlin("plugin.serialization") version "2.0.0"
id("io.ktor.plugin") version "2.3.11"
}
dependencies {
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-auth-jvm")
implementation("io.ktor:ktor-server-auth-jwt-jvm")
implementation("io.ktor:ktor-server-websockets-jvm")
implementation("io.ktor:ktor-server-status-pages-jvm")
}
ルーティングとプラグイン
fun main() {
embeddedServer(Netty, port = 8080) {
configureSerialization()
configureRouting()
configureAuth()
}.start(wait = true)
}
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}
fun Application.configureRouting() {
routing {
route("/api") {
userRoutes()
postRoutes()
}
}
}
fun Route.userRoutes() {
val userService = UserService()
route("/users") {
get {
val users = userService.findAll()
call.respond(users)
}
get("/{id}") {
val id = call.parameters["id"]?.toLongOrNull()
?: throw BadRequestException("Invalid ID")
val user = userService.findById(id)
?: throw NotFoundException("User not found")
call.respond(user)
}
post {
val request = call.receive<CreateUserRequest>()
val user = userService.create(request)
call.respond(HttpStatusCode.Created, user)
}
}
}
WebSocket
fun Application.configureWebSocket() {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(15)
timeout = Duration.ofSeconds(60)
maxFrameSize = Long.MAX_VALUE
}
routing {
val connections = Collections.synchronizedSet<Connection>(mutableSetOf())
webSocket("/chat") {
val connection = Connection(this)
connections += connection
try {
for (frame in incoming) {
frame as? Frame.Text ?: continue
val text = frame.readText()
connections.forEach {
it.session.send("${connection.name}: $text")
}
}
} finally {
connections -= connection
}
}
}
}
7. Android + Jetpack Compose
Composeの基本(きほん)
@Composable
fun UserListScreen(
viewModel: UserListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(title = { Text("Users") })
}
) { padding ->
when (val state = uiState) {
is UiState.Loading -> LoadingIndicator(Modifier.padding(padding))
is UiState.Success -> UserList(
users = state.data,
modifier = Modifier.padding(padding)
)
is UiState.Error -> ErrorMessage(
message = state.message,
onRetry = viewModel::retry,
modifier = Modifier.padding(padding)
)
}
}
}
@Composable
fun UserList(users: List<User>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(users, key = { it.id }) { user ->
UserCard(user = user)
}
}
}
@Composable
fun UserCard(user: User) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = CardDefaults.cardElevation(4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = user.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
状態管理(じょうたいかんり)とナビゲーション
// ViewModel
@HiltViewModel
class UserListViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()
init { loadUsers() }
private fun loadUsers() {
viewModelScope.launch {
_uiState.value = UiState.Loading
userRepository.getUsers()
.onSuccess { users ->
_uiState.value = UiState.Success(users)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message ?: "Unknown error")
}
}
}
fun retry() = loadUsers()
}
// ナビゲーション(Type-safe)
@Serializable
sealed class Screen {
@Serializable
data object UserList : Screen()
@Serializable
data class UserDetail(val userId: Long) : Screen()
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.UserList) {
composable<Screen.UserList> {
UserListScreen(
onUserClick = { userId ->
navController.navigate(Screen.UserDetail(userId))
}
)
}
composable<Screen.UserDetail> { backStackEntry ->
val screen: Screen.UserDetail = backStackEntry.toRoute()
UserDetailScreen(userId = screen.userId)
}
}
}
8. Kotlin Multiplatform(KMP)
プロジェクト構造(こうぞう)
shared/
src/
commonMain/ <- 共有コード
androidMain/ <- Android実装
iosMain/ <- iOS実装
androidApp/ <- Androidアプリ
iosApp/ <- iOSアプリ(Swift)
expect / actual パターン
// commonMain — 共通インターフェース定義
expect class PlatformContext
expect fun getPlatformName(): String
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}
// androidMain — Android実装
actual typealias PlatformContext = Context
actual fun getPlatformName(): String = "Android ${Build.VERSION.SDK_INT}"
actual class DatabaseDriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver =
AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
// iosMain — iOS実装
actual class PlatformContext
actual fun getPlatformName(): String = UIDevice.currentDevice.systemName()
actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver =
NativeSqliteDriver(AppDatabase.Schema, "app.db")
}
共有(きょうゆう)ビジネスロジック
// commonMain
class UserRepository(
private val api: UserApi,
private val db: UserDatabase,
private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun getUsers(forceRefresh: Boolean = false): List<User> =
withContext(dispatcher) {
if (!forceRefresh) {
val cached = db.getUsers()
if (cached.isNotEmpty()) return@withContext cached
}
val users = api.fetchUsers()
db.saveUsers(users)
users
}
fun observeUsers(): Flow<List<User>> =
db.observeUsers()
}
// Ktorベースの共有HTTPクライアント
class UserApi(private val client: HttpClient) {
suspend fun fetchUsers(): List<User> =
client.get("https://api.example.com/users").body()
}
// 共有HttpClient設定
expect fun httpClient(): HttpClient
// androidMain
actual fun httpClient(): HttpClient = HttpClient(OkHttp) {
install(ContentNegotiation) { json() }
}
// iosMain
actual fun httpClient(): HttpClient = HttpClient(Darwin) {
install(ContentNegotiation) { json() }
}
Compose Multiplatform
// commonMain — 共有UI
@Composable
fun App() {
MaterialTheme {
var users by remember { mutableStateOf<List<User>>(emptyList()) }
LaunchedEffect(Unit) {
users = repository.getUsers()
}
UserListScreen(users)
}
}
// Android
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
// iOS(SwiftUI)
// ContentView.swift
// struct ContentView: View {
// var body: some View {
// ComposeView().ignoresSafeArea()
// }
// }
9. 高度(こうど)な機能
Inline / Reified
// inline関数:ラムダ呼び出しのオーバーヘッドを除去
inline fun <T> measureTime(block: () -> T): Pair<T, Long> {
val start = System.nanoTime()
val result = block()
val duration = System.nanoTime() - start
return result to duration
}
// reified:ランタイムで型情報を保持
inline fun <reified T> String.parseJson(): T =
Json.decodeFromString<T>(this)
// 使用例
val user = """{"name":"Kim","age":30}""".parseJson<User>()
// crossinline:非ローカルリターンを禁止
inline fun runSafe(crossinline block: () -> Unit) {
try { block() } catch (e: Exception) { /* handle */ }
}
Contracts
import kotlin.contracts.*
// コンパイラに関数の動作を伝える契約
fun String?.isNotNullOrEmpty(): Boolean {
contract {
returns(true) implies (this@isNotNullOrEmpty != null)
}
return this != null && this.isNotEmpty()
}
// 使用 — スマートキャストが有効化
fun greet(name: String?) {
if (name.isNotNullOrEmpty()) {
println("Hello, $name!") // nameがStringにスマートキャスト
}
}
Context Receivers(実験的(じっけんてき))
context(LoggerContext, TransactionContext)
fun transferMoney(from: Account, to: Account, amount: BigDecimal) {
log("Transfer $amount from ${from.id} to ${to.id}")
transaction {
from.withdraw(amount)
to.deposit(amount)
}
}
Value Classes
@JvmInline
value class UserId(val value: Long)
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email" }
}
}
// コンパイル時にLong/Stringにインライン化 — オーバーヘッドゼロ
fun findUser(id: UserId): User? = repository.findById(id.value)
// 型安全性の確保
val userId = UserId(42L)
val email = Email("user@example.com")
// findUser(email) // コンパイルエラー!
10. テスティング
Kotest
// BehaviorSpec(BDDスタイル)
class UserServiceBehaviorSpec : BehaviorSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
Given("ユーザーがデータベースに存在する") {
val user = User(1, "Kim", "kim@test.com")
coEvery { repository.findById(1L) } returns user
When("findByIdがユーザーのIDで呼ばれる") {
val result = service.findById(1L)
Then("ユーザーを返すべき") {
result shouldBe user
}
}
}
Given("ユーザーが存在しない") {
coEvery { repository.findById(any()) } returns null
When("findByIdが呼ばれる") {
val result = service.findById(999L)
Then("nullを返すべき") {
result shouldBe null
}
}
}
})
// プロパティベーステスト
class StringExtensionTest : StringSpec({
"toSlugは有効なスラグを生成すべき" {
checkAll(Arb.string(1..100)) { input ->
val slug = input.toSlug()
slug shouldNotContain " "
slug shouldMatch Regex("[a-z0-9-]*")
}
}
})
MockK
// コルーチンモッキング
coEvery { repository.findById(any()) } returns mockUser
coVerify { repository.save(match { it.name == "Kim" }) }
// 拡張関数のモッキング
mockkStatic("com.example.ExtensionsKt")
every { "test".toSlug() } returns "test-slug"
// オブジェクトモッキング
mockkObject(DateUtils)
every { DateUtils.now() } returns fixedDate
Turbine(Flowテスティング)
class UserViewModelTest : FunSpec({
test("loadUsersはLoadingの後Successを発行すべき") {
val viewModel = UserViewModel(mockRepository)
viewModel.uiState.test {
viewModel.loadUsers()
awaitItem() shouldBe UiState.Loading
val success = awaitItem()
success.shouldBeInstanceOf<UiState.Success<List<User>>>()
success.data.size shouldBe 3
cancelAndIgnoreRemainingEvents()
}
}
})
コルーチンテスト
class CoroutineTest : FunSpec({
test("構造化された並行性のキャンセル") {
runTest {
var completed = false
val job = launch {
try {
delay(10_000)
completed = true
} catch (e: CancellationException) {
// クリーンアップ
throw e
}
}
advanceTimeBy(5_000)
job.cancel()
job.join()
completed shouldBe false
}
}
})
11. インタビュー質問(しつもん)とクイズ
インタビュー質問15選(せん)
Q1: KotlinのNull安全性はどのように動作しますか?
Kotlinは型システムレベルでnullableとnon-nullableの型を区別(くべつ)します。Stringはnullになれず、String?はnull可能です。安全呼び出し演算子(?.)、エルビス演算子(?:)、非null断言(!!)などのツールを提供します。コンパイラがnullチェックを強制し、ランタイムのNullPointerExceptionを防止します。
Q2: data classと通常のclassの違いは?
data classはequals()、hashCode()、toString()、copy()、componentN()関数を自動生成します。主コンストラクタに1つ以上のパラメータが必要で、abstract/open/sealed/innerにはできません。DTO(Data Transfer Object)や値オブジェクトに適しています。
Q3: sealed classの使用例を説明してください。
sealed classは制限されたクラス階層を定義します。when式で全てのサブタイプを処理するとelseが不要になり、コンパイルタイムの安全性を保証します。APIレスポンスの状態(Success/Error/Loading)、ナビゲーションイベント、UI状態の表現に最適です。
Q4: コルーチンとスレッドの違いは?
コルーチンはスレッドよりはるかに軽量です。1つのスレッドで数千のコルーチンを実行できます。コルーチンは協調的マルチタスキング(サスペンドポイントで譲歩)で、スレッドはプリエンプティブマルチタスキングです。構造化された並行性でライフサイクル管理が容易になります。
Q5: launchとasyncの違いは?
launchはJobを返し、結果がないfire-and-forget方式です。asyncはDeferred<T>を返し、await()で結果を受け取れます。結果が不要ならlaunch、並列作業の結果が必要ならasyncを使用します。
Q6: FlowとChannelの違いは?
Flowはcoldストリームでサブスクライバーがいる時のみ実行されます。Channelはhotストリームでサブスクライバーの有無に関わらずデータを送信します。Flowは宣言的で変換が容易、Channelは命令的でコルーチン間通信に使用されます。
Q7: Spring BootでKotlinを使う際の注意点は?
kotlin-springプラグインでクラスをopenにする必要があります(Spring AOPプロキシ)。kotlin-jpaプラグインでno-argコンストラクタを追加します。Jacksonのkotlin-moduleでdata classのシリアライゼーションをサポートします。コルーチン使用時はWebFlux + kotlinx-coroutines-reactorが必要です。
Q8: KtorとSpring Bootの選択基準は?
Ktorは軽量、Kotlinネイティブ、DSLベースの設定が利点です。マイクロサービスやLambdaなど軽量環境に適しています。Spring Bootは広大なエコシステム、エンタープライズ機能、自動設定が強みです。大規模アプリケーションや既存のSpringインフラがある場合に有利です。
Q9: Jetpack Composeのリコンポジション最適化方法は?
rememberで値キャッシング、derivedStateOfで派生状態管理、Stable/Immutableアノテーションで安定性マーキング、keyでリストアイテム識別、ラムダの安定化(rememberで包む)、状態読み取りスコープの最小化などを使用します。
Q10: Kotlin Multiplatformでプラットフォーム固有の依存関係はどう管理しますか?
expect/actualメカニズムを使用します。commonMainでexpect宣言をし、各プラットフォーム(androidMain/iosMain)でactual実装を提供します。依存関係は各ソースセットのdependenciesブロックで管理します。
Q11: inline関数とreified型パラメータの関係は?
JVMの型消去により、通常の関数ではジェネリック型情報をランタイムで知ることができません。inline関数は呼び出し地点にコードがインライン化されるため、reifiedキーワードで型情報を保持できます。is TチェックやT::classアクセスが可能になります。
Q12: Kotlinの委譲(delegation)を説明してください。
クラス委譲(byキーワード)はインターフェース実装を他のオブジェクトに委譲します。デコレータパターンをボイラープレートなしで実装します。プロパティ委譲(lazy、observable、map)はgetter/setterロジックを再利用します。
Q13: 構造化された並行性が重要な理由は?
親コルーチンがキャンセルされると全ての子コルーチンが自動キャンセルされます。子コルーチンの一つが失敗すると兄弟コルーチンもキャンセルされます。コルーチンのライフサイクルがスコープにバインドされ、メモリリークとリソースリークを防止します。
Q14: value classの使用例と制約は?
value classはランタイムオーバーヘッドなしで型安全性を提供します。UserId、Emailなどのラッパー型に適しています。制約:プロパティは1つのみ、インターフェース実装可能だが継承不可、varプロパティ不可、===比較不可です。
Q15: Kotlin DSLはどのように動作しますか?
レシーバ付きラムダ(lambda with receiver)に基づいています。T.() -> Unit形式でスコープ内からTのメンバーを直接呼び出せます。これによりGradleビルドスクリプト、Ktorルーティング、HTMLビルダーなどの領域特化言語を構築します。
クイズ5選
Q1: このコードの出力は?
fun main() = runBlocking {
val result = coroutineScope {
val a = async { delay(100); 1 }
val b = async { delay(200); 2 }
a.await() + b.await()
}
println(result)
}
正解(せいかい): 3
2つのasyncコルーチンが並列実行されます。aは100ms後に1を返し、bは200ms後に2を返します。合計所要時間は約200msで、結果は1 + 2 = 3です。coroutineScopeは全ての子が完了するまで待ちます。
Q2: sealed interfaceとsealed classの違いは?
正解:
sealed classは状態(プロパティ)を持てますが単一継承のみ可能です。sealed interfaceは状態を持てませんが多重実装が可能です。sealed interfaceを使うと、サブクラスが他のクラスを継承しながらsealed階層に属することができ、より柔軟です。Kotlin 1.5からsealed interfaceが導入されました。
Q3: このコードの問題点を見つけてください。
class MyViewModel : ViewModel() {
fun loadData() {
GlobalScope.launch {
val data = repository.fetchData()
_state.value = data
}
}
}
正解:
GlobalScopeを使用するとViewModelが破棄されてもコルーチンが実行し続けます。メモリリークと不要な作業が発生します。viewModelScope.launchを使用すべきで、ViewModel破棄時に自動キャンセルされます。構造化された並行性の原則に違反したコードです。
Q4: let、run、apply、also、withの違いは?
正解:
| 関数 | オブジェクト参照 | 戻り値 | 使用例 |
|---|---|---|---|
| let | it | ラムダ結果 | nullチェック後の変換 |
| run | this | ラムダ結果 | オブジェクト設定 + 結果計算 |
| apply | this | レシーバオブジェクト | オブジェクト初期化 |
| also | it | レシーバオブジェクト | 副作用(ログ等) |
| with | this | ラムダ結果 | 既存オブジェクトのメソッド呼び出し |
Q5: StateFlowとSharedFlowの違いは?
正解:
StateFlowは現在の状態値を持ち、新しいサブスクライバーに即座に最新値を配信します。常に初期値が必要で、重複値は無視されます。SharedFlowは状態がなく、イベントブロードキャストに使用されます。replayパラメータでバッファリングを制御します。UI状態はStateFlow、一回限りのイベント(ナビゲーション、トースト)はSharedFlowが適しています。
参考資料(さんこうしりょう)
- Kotlin公式ドキュメント - 言語リファレンスとチュートリアル
- Kotlinコルーチンガイド - 公式コルーチンドキュメント
- Spring Boot Kotlinサポート - Spring Kotlinガイド
- Ktor公式ドキュメント - Ktorフレームワークガイド
- Jetpack Compose - Android Compose公式ドキュメント
- Kotlin Multiplatform - KMP公式ガイド
- Kotestフレームワーク - Kotlinテストフレームワーク
- MockK - Kotlinモッキングライブラリ
- Turbine - Flowテスティングライブラリ
- Kotlinコルーチン設計文書 - コルーチン設計思想
- Android Developers - Kotlin - Android Kotlinガイド
- Compose Multiplatform - JetBrains Compose Multiplatform
- Kotlin KEEP - Kotlin Evolution and Enhancement Process
- kotlinx.serialization - シリアライゼーションライブラリ
- Arrow - Kotlin関数型プログラミングライブラリ
- SQLDelight - KMP SQLライブラリ