Open7

読書メモ「Kotlin サーバーサイド プログラミング実践開発」

Naoya.TNaoya.T

第1章

3 コードの安全性を高めるKotlinの型とNull非許容/許容

val str1: String = null // コンパイルエラー
val str2: String? = null

対処法

  1. if 文でnull チェック
  2. エルビス演算子
fun checkNullElvis(message: String?) {
    message ?: return
    println(message.length)
}
  1. 「安全呼び出し」: 変数の後ろに?をつけて呼び出す
fun checkNullSaveCall(message: String?) {
    println(message?.length)
}
  1. 強制アンラップ
fun checkNullUnWrap(message: String?) {
    println(message!!.length)
}
Naoya.TNaoya.T

第2章(第1部)

lateinitでプロパティの初期化を遅延する

class User{
    lateinit var name: String
}
val Usr = User()
  • このインスタンス生成時にはエラーにならない
  • getterを呼び出す時には値を格納しておく

拡張プロパティでgetter, setterの処理を拡張する

getter/setterを拡張することで独自の処理を記述することができる
例えば空文字が入った時にSampleTextを入れたい場合

class User{
    var name: String = ""
        set(value) {
            if (value == "") {
                field = "SampleText"
            } else {
                field = value
            }
        }
}
val user = User()
user.name = ""
println(user.name) // SampleText
user.name = "ZennJo" 
println(user.name) // ZennJo
  • field識別子は値を保持する必要がある場合に用いる
    • バッキングフィールドが生成され、それを介して値の格納と取得をしている

データクラスの定義

data class User(
    val id: Int,
    var name: String
)

複数回インスタンスを生成し、以下の関数を用いて比較しても同等の結果を得られる

  • equals
  • hashCode
  • toString
  • componentN
  • copy

関数型

val calc: (Int, Int) -> Int

関数リテラル

  • 関数を値として記述するもの
  1. ラムダ式
val calc: (Int, Int) -> Int = { num1: Int, num2: Int -> num1 + num2 }
  1. 無名関数
val calc: (Int) -> Int = fun(num: Int): Int { return num * num }

高階関数

  • 関数型のオブジェクトを引数に受け取る関数のこと
fun printCalcResult(num1: Int, num2: Int, calc: (Int, Int) -> Int) {
    val res = calc(num1, num2)
    println(res)
}
printCalcResult(10, 20, {num1, num2 -> num1 + num2 }) // 30
printCalcResult(10, 20) {num1, num2 ->
    num1 * num2 
} // 200

タイプエイリアス

  • 関数型を使い回す時に名前をつける
typealias Calc = (Int, Int) -> Int

fun printCalcResult(num1: Int, num2: Int, calc: Calc) {
    val res = calc(num1, num2)
    println(res)
}

拡張関数

  • 定義 fun クラスの型 関数名
  • 既存のクラスに対して、関数を追加する
  • thisを使用する
  • スコープは通常の関数と同様で、他のパッケージからimportできる
  • 特定のクラス内でのみ使用したい場合はprivateをつける
fun Int.square(): Int = this * this
fun main() {
    println(2.square())
} // 4
Naoya.TNaoya.T

第2章の2部(スコープ関数まとめ)

はじめに

null 返す値 名前
with 扱えない 複数の処理をまとめて行う(ラムダの結果) なしor this
run 扱える 複数の処理をまとめて行う(ラムダの結果) なしor this
let 扱える 複数の処理をまとめて行う(ラムダの結果) つけるor it
apply 呼び出し元のインスタンス なしor this
also 呼び出し元のインスタンス つけるor it

with — 複数の処理をまとめて行う

  • あるオブジェクトに対して複数の処理をまとめて行いたい場合に使用する
// 通常

val list = mutableListOf<Int>()
for (i in 1..10) {
    if (i % 2 == 1) list.add(i)
}
val oddNumbers = list.joinToString(separator = " ")
println(oddNumbers)
// 1 3 5 7 9
// withを使用
val odddNumbers = with(mutableListOf<Int>()) {
    for (i in 1..10) {
        if (i % 2 == 1) this.add(i)
    }
    this.joinToString(separator = " ")
}
println(odddNumbers)
// 1 3 5 7 9
  • withの第一引数はレシーバとなるオブジェクト
    • スコープ関数を実行する対象のオブジェクトのことをレシーバオブジェクトとという
    • ex)MutableListのインスタンス
  • 第二引数としてレシーバのオブジェクトを処理し任意の値を返す関数
    • ex)後ろのコードブロック
    • ここではthisは省略可能

run —Nullableなオブジェクトに複数の処理をまとめて行う

val oddNumbers = mutableListOf<Int>().run {
	for (i in 1..10) {
		if (i % 2 == 1) this.add(i)
	}
	this.joinToString(separator = " ")
}
  • レシーバに値が入っている場合のみ処理を実行し、そうでない場合はエルヴィス演算子を用いて任意の値を返すことができる(エルヴィス演算子を書かない場合はnullが返る)
data class User(val name: String, val age: Int)
data class UserInfo(val name: String, val age: Int, val address: String)
fun main(user: UserInfo?): User? {
    return user?.run {
        User(
            name = this.name,
            age = 2023 - this.age
        )
    } ?: User(
        name = "taro",
        age = 20
    )
}
val users = UserInfo("taro", 20, "埼玉")
println(main(null))

let —Nullableなオブジェクトに名前をつけて処理を行う

val oddNumbers = mutableListOf<Int>().let { list -> 
	for (i in 1..10) {
        if (i % 2 == 1) list.add(i)
    }
    list.joinToString(separator = " ")
}
println(oddNumbers)
  • listは冗長なのでrunでthisを省略する方がいい
  • 暗黙的にitを使うことができる
  • nullでないかを判定した処理を書く時にletはよく使われる
    • if (hoge != null).....
data class User(val name: String)
fun createUser(name: String?): User? {
    return name?.let { it -> User(it)}
}
println(createUser("taro"))
println(createUser(null))
//User(name=taro)
//null

apply —オブジェクトに変更を加えて返す

val oddNumbers = mutableListOf<Int>().apply {
        for (i in 1..10) {
            if (i % 2 == 1) this.add(i)
        }
        this.joinToString(separator = " ")
    }
    println(oddNumbers)

// [1, 3, 5, 7, 9]
  • dataクラスのようなプロパティを持ったオブジェクトに対して変更を加えて返却するという処理を書く時に役に立つ
data class User(val id: Int, var name: String, var address: String)
fun getUser(id: Int): User {
    return User(id, "taro", "Tokyo")
}
fun updateUser(id: Int, newName: String, newAddress: String) {
    val user = getUser(id).apply {
        this.name = newName
        this.address = newAddress
    }
    println(user)
}
updateUser(100, "shiro", "osaka")

also —オブジェクトに変更を加えて返す(名前をつけて扱う)

  • レシーバオブジェクトに変更を加えて返却するスコープ関数
    • letと同じように名前をつけて扱う
    • 名前を省略してitで扱うことができる
data class User(val id: Int, var name: String, var address: String)
fun getUser(id: Int): User {
    return User(id, "taro", "Tokyo")
}
fun updateUser(id: Int, newName: String, newAddress: String) {
    val user = getUser(id).also { u ->
        u.name = newName
        u.address = newAddress
    }
    println(user)
}
updateUser(100, "shiro", "osaka")
Naoya.TNaoya.T

第2章の3部(コレクションライブラリ)

  • コレクション・・・List, Map, Set
  • コレクションを扱うためのライブラリ
  • コレクションライブラリはitを使って表現できる
data class User(val id: Int, val teamId: Int, val name: String)

forEach --- コレクションの要素を順番に処理する

  • 順番に表示する
val list = listOf(1,2,3)
list.forEach { num -> println(num)}
list.forEach { println(it)}
// 1
// 2
// 3

map --- 要素を別の形に変換したListを生成する

用途

  • データクラスの一部のプロパティだけを持ったListの生成
  • 数字文字列のListを数値のListに変換するなど
data class User(val id: Int, val teamId: Int, val name: String)
val list = listOf(User(1, 100, "taro"), User(2, 100, "shiro"))
val idList = list.map { it.id }
list.forEach { println(it)}
// 1
// 2

filter --- 条件に該当する要素を抽出する

  • コレクションの中から任意の条件に該当する要素のみフィルタリングしたListを生成する
data class User(val id: Int, val teamId: Int, val name: String)
val list = listOf(User(1, 100, "taro"), User(2, 100, "shiro"), User(3, 200, "saro"))
val idList = list.filter { it.teamId == 100 }
list.forEach { println(it)}
// User(id = 1, teamId = 100, name = "taro")
// User(id = 2, teamId = 100, name - "shiro")

firstOrNull, lastOrNull -- 条件に該当する先頭、末尾の要素を抽出する(該当しない場合はnullを返す)

data class User(val id: Int, val teamId: Int, val name: String)
val list = listOf(User(1, 100, "taro"), User(2, 100, "shiro"), User(3, 200, "saro"))
println(list.firstOrNull { it.teamId == 100 }) // User(id = 1, teamId = 100, name = "taro")
println(list.lastOrNull { it.teamId == 100 }) // User(id=2, teamId=100, name=shiro)
println(list.firstOrNull { it.id == 9 }) // null
println(list.lastOrNull { it.name == "ken" }) // null

distinct --- 重複を排除したListを生成する

val list = listOf(1,2,3,4,1,2,3,6,7,8,8,9,1)
val distinctList = list.distinct()
println(distinctList)
// [1, 2, 3, 4, 6, 7, 8, 9]

associateBy,associateWith --- コレクションからMapを生成する

  • associateBy
    • コレクションに対し任意の値をKeyにコレクションの要素をvalueとしたMapを生成する
    • ラムダ式の中にはkeyにしたい値を生成する処理
data class User(val id: Int, val teamId: Int, val name: String)
val list = listOf(User(1, 100, "taro"), User(2, 100, "shiro"), User(3, 200, "saro"))
val map = list.associateBy {it.id}
println(map)
// {1=User(id=1, teamId=100, name=taro), 2=User(id=2, teamId=100, name=shiro), 3=User(id=3, teamId=200, name=saro)}
  • associateWith
    • 要素をKeyに任意の値をvalueとする
    • ラムダ式の中にはvalueにしたい値を生成する処理
val list = listOf("kotlin", "java", "python")
val map = list.associateWith { it.length }
println(map)
// {kotlin=6, java=4, python=6}

groupBy --- keyごとに要素をまとめたMapを生成する

  • 同一のKeyごとにまとめた要素のListをvalueとしたMapを生成
    • associateByと同様にkeyにしたい値をラムダ式の中に記述
    • valueは同一になる要素
data class User(val id: Int, val teamId: Int, val name: String)
val list = listOf(User(1, 100, "taro"), User(2, 200, "shiro"), User(3, 100, "saro"), User(4, 200, "ken"))
val map = list.groupBy{it.teamId}
println(map)
//{100=[User(id=1, teamId=100, name=taro), User(id=3, teamId=100, name=saro)], 200=[User(id=2, teamId=200, name=shiro), User(id=4, teamId=200, name=ken)]}

count --- 条件に該当する要素の件数

  • 条件を指定し該当する件数取得できる
val list = listOf(1,2,3,4,5)
val oddNumberCount = list.count {it % 2 == 1}
println(oddNumberCount)
// 3

chunked --- 指定の要素ごとに分割したListを生成する

  • 指定の要素ごとに分割する
    • 引数に分割したい数を入れる
val list = listOf("kotlin", "java", "Scala", "python", "Ruby", "PHP")
val chunckedList = list.chunked(3)
println(chunckedList)
// [[kotlin, java, Scala], [python, Ruby, PHP]]

reduce --- 要素を畳み込む

  • ある処理を要素の値に累積的に適用していき、1つの値にまとめる
    • sumは現在まで処理された値
    • valueは順番に取り出している現在の値
    • これまで処理したものと現在の値を処理をして最終的に1つの値にまとめる
val list = listOf(1,2,3,4,5,6)
val sumList = list.reduce{ sum, value -> 
    println("$sum + $value")
    sum + value}
println(sumList)
// 1 + 2
// 3 + 3
// 6 + 4
// 10 + 5
// 15 + 6
// 21
Naoya.TNaoya.T

第2章最後と第3章

非同期処理

  • コルーチンは非同期処理を実装する昨日
    • 実行しているスレッドを停止することなく効率の良処理を実装できる

コルーチン

GlobalScope.launch { 
    delay(1000L)
    println("kotlin")
}
println("My name is")
Thread.sleep(2000L)
// My name is
// koltin
  • GlobalScope.launchのブロック内で書かれている処理が、非同期処理
    • Global
      • コルーチンスコープ(コルーチンが実行される仮想領域)
    • launch
      • コルーチンビルダー(コルーチンを構築するためのもの)
  • コルーチンスコープビルダー
    • 以下のようなrunBlockingを使うとコルーチンスコープを構築できる
    • これはスコープ内のコルーチンの処理が全て終わるまで終了しないようスレッドをブロック
    • 上記のThread.sleep(2000L)がいらない
runBlocking {
    launch {
        delay(1000L)
        println("kotlin")
    }
    println("My name is")
}
// My name is
// kotlin

サスペンド関数

  • コルーチンの処理を中断することができる関数
    • 中断処理を書かなければ中断されない
    • 上記のdelayのような
    • コルーチンか別のサスペンド関数の中でのみ使用できる
suspend fun pringName() {
    delay(1000L)
    println("kotlin")
}
runBlocking{
    launch{printName()}
    println("My name is")
}
// My name is
// kotlin

asyncで並列処理を実装

  • async
    • コルーチンビルダー
    • launchとの違いは処理結果を受け取れること
    • 代入されたnum1とnum2からawaitという関数を呼び出すことで結果が取得できる
    • 並列処理ができる
      • 両方の処理が終わったタイミングで最後の計算、出力処理が実行されている
runBlocking {
    val result = async {
        delay(2000L)
        1 + 2
    }
    val result = async {
        delay(2000L)
        3 + 4
    }
    println("計算中")
    println("sum = ${num1.await() + num2.await()}")
}
// 計算中
// sum = 10

Javaのライブラリを呼び出す

kotlinからjavaのライブラリを呼び出すことができる

  • UUID
import java.util.UUID
val uuid: UUID = UUID.randomUUID()
println(uuid.toString())
// aa64f578-e865-4158-849f-79929517a52f
  • LocalDateTime
import java.time.LocalDateTime
data class Time(val time: LocalDateTime)
fun main(){
    val now = Time(LocalDateTime.now())
    println(now.time)
    // 2023-01-29T16:39:08.962588
}

クラス内にstaticな変数や関数を定義する時、companion objectを使う

class CompanyConstants {
   companion object{
       val maxEmployeeCount = 100
   }
}
Naoya.TNaoya.T

第4章 Kotlinでのサーバーサイド開発

Spring Bootでの開発

  • これはwebアプリケーションフレームワークの1つ
  • Spring Initializrでプロジェクトの雛形を作成できる

始め方

  1. 例えば、以下のように設定する
    • Project: Gradle
    • Launguage: Kotlin
    • Spirng Boot: 3.0.2
    • Project Metadata: 全てデフォルトの値
    • Dependencies: Spring Web, Thymeleaf
  2. そして、ダウンロードして任意の場所で展開
  3. bulid.gradle.ktsをInteliJ IDEAで開く

補足

  • dependenciesに書かれているのはアプリケーションで使用する依存関係
    • org.springframework.boot:spring-boot-starter-thymeleafはHTMLなどwebページ作成で使用するThymeleafというテンプレートエンジンを使うためのstarter
    • org.springframework.boot:spring-boot-starter-webはルーティングなど、webアプリケーションのサーバーサイドプログラムで必要な機能を提供するstarter

アプリケーション開発

基本

@Controller // ルーティングを行うことができる
@RequestMapping("hello") // classに対してルーティングを設定できる
class HelloController {
    @GetMapping("/world") // Getでアクセスできる "http://localhost:8080/hello/world"
    fun index(model: Model): String{
        model.addAttribute("message", "Hello World!")
        return "index"
    }
}

REST API

@RestController // f戻り値のオブジェクトをJSONにシリアライズしてレスポンスとして返す
@RequestMapping("greeter")
class GreeterController {
    @GetMapping("/hello")
    // @RequestParamはこの引数の名前のパラメータをクエリストリングで受け取り、後ろの変数に入れる
    fun hello(@RequestParam("name") name: String): HelloResponse{
        return HelloResponse("Hello ${name}")
    }
    // PathVariableはパラメータで受け取った値を後ろの変数に入れる
    @GetMapping("/hello/{name}")
    fun helloPathValue(@PathVariable("name") name: String): HelloResponse{
        return HelloResponse("Hello ${name}")
    }
    // RequestBodyはPOSTで値を受け取ることができ、後ろの変数に入れる
    @PostMapping("/hello") // POST
    fun helloByPost(@RequestBody request: HelloRequest): HelloResponse{
        return HelloResponse("Hello ${request.name}")
    }
}

data class HelloResponse(val message: String)
data class HelloRequest(val name: String)

DI(Dependency Injection: 依存性の注入)

  • Spring Frameworkでよく使う機能
  • 簡単に言うと
    • 各クラスで使用するオブジェクトの生成を自動化してくれるもの
  • 何が嬉しいか
    • 同一クラス内の複数箇所でオブジェクトを使い回す場合に都度インスタンスを生成する必要がなくなる
    • シングルトンとなる
      • あるクラスのインスタンスを2つ以上作成できないようにすることで、「どこからアクセスしても常に同一のインスタンスが参照される」ことを保証するデザインパターン:参考資料
// インターフェイスのファイル
interface Greeter {
    fun sayHello(name: String): String
}
// その対象クラスのファイル
@Component //これをつけることでDIの対象であることを表すことができる
class GreeterImpl : Greeter{
    override fun sayHello(name: String): String = "Hello ${name}"
}

コンストラクタインジェクション

  • いくつかの方法があるが、基本的にはコンストラクタインジェクションを使う
// コントローラーファイル
@RestController
@RequestMapping("greeter")
class GreeterController(
    private val greeter: Greeter // コンストラクタにGreeter型の引数を定義
    ) {
    @GetMapping("/hello/byservice/{name}")
    fun helloByService(@PathVariable("name") name: String): HelloResponse{
        val message = greeter.sayHello(name) //呼び出しが簡単!
        return HelloResponse(message)
    }
}
  • 1つのインターフェイスに対して複数のクラスが存在する場合
interface MessageService{
    // 省略
}
@Component("JapaneseMessageService")
classs JapaneseMessageService: MessageService{
    // 省略
}
@Component("EnglishMessageService")
classs EnglishMessageService: MessageService{
    // 省略
}

// 呼び出し側では
@RestController
@RequestMapping("greeter")
class GreeterController(
    @Qualifier("EnglishMessageService")
    private val messageService: MessageService
) {
    @GetMapping("/hello/byservice/{name}")
    fun helloByService(@PathVariable("name") name: String): HelloResponse{
        val message = greeter.messageService(name)
        return HelloResponse(message)
    }
}