たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Kotlinはじめてみた

積読していたKotlinの入門本をちょっとずつ読んでみる。

基礎からわかる Kotlin | 富田健二 | 工学 | Kindleストア | Amazon

環境構築

環境設定から始める。

JetBrainsの公式サイトからIntelliJ IDEAをダウンロードする。

ちなみに、僕の場合はCommunity版のApple Siliconバージョンをダウンロードした。

ダウンロード IntelliJ IDEA: JetBrains の人間工学に基づく高機能 Java IDE

プロジェクトを作成する

IntelliJ IDEAでNew Projectをクリックする。

各種設定を行って、Createボタンをクリックする。

変数

varは再代入可能な変数を宣言するときに使う。

var text: String = "Hello"
text = "World"

valは再代入不可能な変数を宣言するときに使う。

val text: String = "Hello World"

constは定数を宣言するのに使う。注意点として、トップレベルでの宣言かオブジェクト宣言であり基本型の定数を宣言するときのみ使用可能になる。

なのでList型はconstで宣言できない。

const val text: String = "Hello World"

基本型

  • Boolean
    • true
    • false
  • Char
    • 単一の文字を表す。
    • シングルクォートで囲む必要がある。
  • String
    • 文字列を扱う。
    • ダブルクォーテーションで囲む。
    • トリプルクォーテーションで囲むと複数行の文字列に対応する。これを使うときは.trimMargin()を利用する。
    • 文字列テンプレート("${x + y}"))を使うことで式や変数を文字列に展開できる。
  • Byte
    • 8ビット
  • Short
    • 16ビット
  • Int
    • 32ビット
  • Long
    • 64ビット
  • Float
    • 32ビット
  • Double
    • 64ビット
  • UByte
    • 符号なし8ビット
  • UShort
    • 符号なし16ビット
  • UInt
    • 符号なし32ビット
  • ULong
    • 符号なし64ビット
  • Any
    • すべてのクラスの親クラス
    • 特定の値の型チェックを無効にしてコンパイルを通すために利用される。
    • 極力使用しないようにする。
  • Unit
    • 型がまったくないことを表す。
    • 何も返さない関数の返り値の型として利用される。
      • fun a(text: String): Unit {}
  • Nothing
    • すべてのクラスのサブクラス
    • 存在しない値を示す。つまりインスタンスを生成できない。
    • 例外をスローする関数の返り値の型として利用される。なのでNull許容型を使わずにそういった関数を実装できる。
数値型のTips

0x0bをつかって16進数やバイナリリテラルを表現できる。

val a: Int = 0xAAAAAA
val b: Int = 0b1111

_を使うことで桁の多い数字も読みやすく表現できる。

val a: Int = 9_999 // 9999

符号なしをリテラルで表現する場合は末尾にUをつける。

val a: UInt = 100U

Null許容型

Null許容型を宣言するには普段の変数んの宣言の時に型名の横に?を付与する。

var a: String? = "Hello World"

そして、Null許容型にアクセスする際は?を使って連結することでチェーン呼び出しができる。

println(a?.length)

標準ライブラリのrequire()check()を使用すると、事前にNullでないことを確認できる。

require(a != null)
check(a != null)

他にもエルビス演算子?:)を利用することで、Nullの場合に何かをするなどの指定ができる。

// aがnullの場合に出力する
a ?: println("Nullです")

列挙型

列挙型はenumを使って宣言する。

enum class Fruit {
  Apple,
  Orange,
  Banana,
}

列挙型でordinalプロパティを利用すると0から始まる序数を取得できる。nameプロパティを利用すると、その列挙型の名前を取得できる。

Kotlinには三項演算子がない

Kotlinには三項演算子がないので代わりにif (bar) a else bのような式を利用する。

範囲の演算子

1..10を使用すると1から10までの範囲を取得できる。使い道としてはinイテレーターと一緒に利用する。

var total = 0
for (i in 1..10) {
  total += i
}
println(total)

比較演算子の違い

==は内容が一致している場合にtrueを返す。

===は二つのオブジェクトが同じ参照を指していればtrueを返す。

if式を代入する

ifは式なので、結果を変数に代入できる。

var a = if (true) {
  return 1
} else {
  return 0
}

when式とは

when式は、条件が満たされるまですでの条件を順次照合する。switch文みたいに使える。

if式と同じように代入できる。

条件式の中で、変数に代入もできる。(キャプチャ機能)

すべての条件に合致しない場合のelse -> {}も使用できる。列挙型を利用する場合は必要ない。

when(val fruit = Fruit.Apple) {
  Fruit.Apple -> {
    println("Appleです")
  }
  Fruit.Orange -> {
    println("Orangeです")
  }
  Fruit.Banana -> {
    println("Bananaです")
  }
}

forの使い方

Kotlinのfor文はイテレーター(in)を用いて反復処理を行う。

for (i in 1..10) {
  println(i)
}

example@ forのようにラベルを利用できる。break@exampleを呼び出すことで、example@ラベルがついたforを抜け出すことができるようになる。

コレクション

Listは変更不可能なコレクション。

val list = listOf('a', 'b', 'c')

MutableListは変更可能なコレクション。

val mutableList = mutableListOf('a', 'b', 'c')

mutableList += 'd'

varargを使うと動的にサイズを変更できる配列を関数の引数として宣言できる。

fun example(vararg list: String) {
  for (t in list) {
    println(t)
  }
}

example('a', 'b', 'c')
example('a', 'b', 'c', 'd')

スプレッド演算子*)を使うとコレクションを展開できる。

var list = mutableListOf('a', 'b', 'c')

var list2 = listOf('d', 'e', 'f')

var list3 = listOf(*list.toTypedArray(), *list2.toTypedArray())
println(list3)```
Set

Setは重複を防ぐことができる順序なしのコレクションである。

setOfmutableSetOfでSetを生成できる。

Map

Mapはキーと値のペアのコレクションである。

mapOfmutableMapで生成できる。

var map = mapOf(
  "name" to "Tanaka Taro",
  "age" to 14,
)

for (element in map) {
  println("${element.key} : ${element.value}")
}
配列

配列はarrayOfで生成できる。 Listは複数のオブジェクトを内部で保持するが、配列は参照型なのでアドレスを保持する。

なので、arrayOfで生成した値を出力するとアドレスが表示される。

var array = arrayOf('a', 'b', 'c')
println(array)

// [Ljava.lang.Character;@1e643faf
Sequence

Listと違ってmapfilterを利用した場合に、順序的ではなく垂直的に評価される。

つまり、Sequenceに対してmapfilterがチェーン呼出されていたら、最初の要素のみがmapfilterで処理された後に次の要素がmapfilterで処理されるといった流れを繰り返す。

sequenceOfを使って生成できる。

関数

funを使って宣言する。 引数がある場合は必ず型を指定する。戻り値がある場合は必ず型を定義する。戻り値がなく、戻り値の型を指定しない場合はUnit型になる。

関数名はcamelCaseを使用する。

fun main(args: Array<String>) {
    println(example(5))
}

fun example(a: Int): String {
    return "hello world ${a}";
}

名前付き引数を使用することもできる。引数名 = 値で引数を渡す。

fun main(args: Array<String>) {
    println(example(a = 5))
}

fun example(a: Int): String {
    return "hello world ${a}";
}

デフォルト引数も利用できる。

fun main(args: Array<String>) {
    println(example())
}

fun example(a: Int = 10): String {
    return "hello world ${a}";
}

クラス

クラスはclassを使って宣言する。

class Fruit {}

すべてのクラスはAnyを継承している。そしてクラスはデフォルトでfinal宣言になっているので、継承することはできないが、openを使用することで継承できるようになる。

public open class Fruit {
    public open fun print(text: String) {
        println(text)
    }
}

コンストラクタはconstructorを使用する。

class クラス名の横の()の間がプリマリーコンストラクタ。

fun main(args: Array<String>) {
    var user = User(text = "僕は田中太郎")
    println(user.text)
}

class User constructor(val text: String = "") {}

省略もできる。

fun main(args: Array<String>) {
    var user = User(text = "僕は田中太郎")
    println(user.text)
}

class User(val text: String = "") {}

プリマリーコンストラクタ以外に宣言されたconstructorセカンダリーコンストラクタで、this()を使用してプリマリーコンストラクタを継承する必要がある。

イニシャライザはinitを使ってブロック宣言する。コンストラクタよりも前に呼び出される。

プロパティはvarvalを使って宣言する。必ず初期化しないといけない。get()set()を用いてゲッターとセッターをカスタマイズすることができる。もちろん使わなくても良い。valで宣言したプロパティはset()を使うことはできない。ゲッターとセッターの対象の値にはfieldを使ってアクセスする。

fun main(args: Array<String>) {
    var user = User()
    println(user.id)
    println(user.name)
    user.name = "田中太郎"
    println(user.name)
}

class User {
    val id: String // 変える必要がないのでvalで宣言している
        get() {
            return "[${field}]"
        }

    var name :String // 変える可能性も考えてvarで宣言している
        get() {
            return "${field}さん"
        }
        set(value) {
            field = value
        }

    init {
        this.name = "anonymous"
        this.id = "ランダムな値"
    }

    constructor() {

    }
    constructor(name: String): this() {
        this.name = name;
        // valで宣言しているからidの値を変更することはできない
        // this.id = 'test';
    }
}

インナークラスはinnerを使って宣言する。メリットはPhone.Androidのように外部クラスを名前空間として利用できる。そして、内部クラスから外部クラスのプロパティにアクセスできるようになる。その逆はできない。

fun main(args: Array<String>) {
    var b = A().B()
    b.print()
}

class A {
    var name = "tanaka taro"
    inner class B {
        var middleName = "grape"

        fun print() {
            println("${name} ${middleName}")
        }
    }
}

クラスの代わりにobjectを利用してシングルトンクラスを生成できる。

e-words.jp

companion objectをクラス内部で使用することでクラスの中にオブジェクトを配置することもできる。

fun main(args: Array<String>) {
    Obj.print()
}
class Obj {
    companion object {
        const val TEXT = "Hello World"
        
        fun print() = println(TEXT)
    }
}

abstractを使って抽象クラスを宣言できる。抽象クラスを実装するクラスはoverrideを使って中身を定義できる。

fun main(args: Array<String>) {
    val user = User()
    println(user.name)
}

abstract class Human {
    abstract val name: String
}

class User : Human() {
    override val name = "Hello World"
}

データクラス

data classを使って宣言できる。

equals()hashCode()toString()が自動的に宣言したクラスに追加される。data classを使って宣言したクラスはopenを使用できないので継承できない。

fun main(args: Array<String>) {
    val a = DataClass(text = "Data Class Hello World")
    println(a.text)
}

data class DataClass constructor(val text: String = "") {}

シールドクラス

sealed classを使って宣言できる。列挙型のような使い方ができる。階層関係を表現できる。

whenと一緒に活用できる。

fun main(args: Array<String>) {
    var feeling = Feeling.Happy(50)
    Feel(feeling)
    var feeling2 = Feeling.Sad(20, "色々あったから")
    Feel(feeling2)
}

fun Feel(feeling: Feeling) {
    when (feeling) {
        is Feeling.Happy -> {
            println("あなたはハッピーです")
        }
        is Feeling.Sad -> {
            println("あなたは悲しいです")
        }
    }
}

sealed class Feeling {
    data class Happy(val score: Int) : Feeling()
    data class Sad(val score: Int, val reason: String) : Feeling()
}

インターフェース

interfaceを使って宣言できる。インターフェースを使う場合は:を使って実装クラスを定義できる。

デフォルト実装は保持するが、そのほかの実装を保持しない。

setterを宣言することはできない。getterのみ宣言できる。

fun main(args: Array<String>) {
    val value = B()
    value.print("Nice to meet you")
}

interface A {
    val text: String
    get() = "Hello World"

    fun print(newText: String) = if (newText != "") { println(newText) } else { println(text) }
}

class B : A {}

SAMインターフェース

メソッドを1つしか持たない抽象クラス。fun interfaceを使って宣言する。 デフォルト実装は利用できない。

インターフェースを呼び出す際にラムダを使って1つしかないメソッドの中身を実装できる。

fun main(args: Array<String>) {
    val sam = Sam { println (it) }
    sam.print("HELLO")
}

fun interface Sam {
    fun print(text: String): Unit
}

継承

継承元になるクラスを作成する際はopen classを使ってクラスを定義する。理由はクラスはデフォルトでfinalが付与されているから。そして継承元のクラスのメソッドを再定義する場合はoverride fun メソッド名を使用するが、継承元のクラス側でopen fun メソッド名のようにopenを付与しておかないといけない。

あと継承する際は継承先クラス名 : 継承元クラス名()のように継承元クラスのコンストラクタを呼び出してあげないといけない。

fun main(args: Array<String>) {
    val value = B()
    value.print("Taro")
}

open class A {
    open fun print(text: String): Unit {
        println(text)
    }
}

class B : A() {
    override fun print(text: String) {
        println("こんにちわ、${text}")
    }
}

抽象クラスを宣言する場合は、openはいらない。そのままabstract class クラス名abstract fun メソッド名を利用する。

fun main(args: Array<String>) {
    val value = B()
    value.print("Taro")
}

abstract class A {
    abstract fun print(text: String): Unit
}

class B : A() {
    override fun print(text: String) {
        println("こんにちわ、${text}")
    }
}

例外処理

例外はtry-catchを使う。すべての例外はThrowableを基本クラスとしている。 例外を明示的にスローする際はthrow Exception("message")を使用する。

fun main(args: Array<String>) {
    try {
        throw Exception("エラーが発生しました")
    } catch (e: Exception) {
        println(e.message)
    }
}

スマートキャスト

変数xがある特定の型もしくはインターフェースを含んでいるかを確認する場合はisを使用する。 強制的に型キャストする場合はasを使用する。

fun main(args: Array<String>) {
   val x: Any = 'z'

    if (x is Int) {
        // このスコープではxはInt型としてコンパイラに認識される
        println(x.toString())
    }

    // これは失敗する
    val y: String = x as String;
    println (y.length)
}

スコープ関数

変数xに対してx.let { it * 10 }だったり、x.run { this * 2 }みたいな使い方ができる。 ちなみにitthisはコンテキストオブジェクトと言われていて、xそのものを表す。

5種類スコープ関数が存在していて、それぞれでコンテキストオブジェクトがitだったりthisだったりと違う。ちなみに返り値もラムダ自体の実行結果が返り値になるのか、更新されたコンテキストオブジェクト自体になるのかなどさまざま。

  • let
    • it
    • ラムダの実行結果
    • 拡張関数
  • run
    • this
    • ラムダの実行結果
    • 拡張関数
  • with
    • this
    • ラムダの実行結果
    • 拡張関数じゃない
  • apply
    • this
    • コンテキストオブジェクト自体
    • 拡張関数
  • also
    • it
    • コンテキストオブジェクト自体
    • 拡張関数
fun main(args: Array<String>) {
    val text = "Hello World"
    val result = text.let {
        // itはtext変数を表す
        println(it.length)
        "${it}. Mr. Tanaka"
    }
    println(result)
}

ちなみに、thisはレシーバーで、itはラムダパラメーターという扱い。

無名関数

関数名をつけないで関数を宣言できる。

fun main(args: Array<String>) {
   val anonFun = fun(text: String) {
       println(text)
   }
    anonFun("Hello World")
}

ラムダ

ラムダを宣言するときはfunは不要。 ブロック内でreturnを使えない。なので、返したい値をそのまま最後に置いておく。

ちなみに、名前付き引数も使えない。

fun main(args: Array<String>) {
    val lambda = { text: String ->
        println (text)
        text
    }

    val result = lambda("Hello World")
    println (result)
}

関数の引数でラムダを受け取りたい場合は、最後に関数型の引数を定義してあげると呼び出し側でラムダを渡せる。

fun main(args: Array<String>) {
    withLambda() { text: String ->
        text
    }
}

fun withLambda(action: (String) -> String) {
    println("関数を実行します")
    println("ラムダを実行します")
    val result = action("ラムダ実行中")
    println(result)
    println("ラムダの実行が完了しました")
    println("関数を終了します")
}

クロージャ

外部のスコープにある変数にアクセスできるラムダをクロージャーという。 クロージャーは関数のインスタンスが再生成されるが、ラムダや無名関数の場合は再利用される。

fun main(args: Array<String>) {
    val outerText = "Hello World"
    withLambda() { text: String ->
        "${text} _ ${outerText}"
    }
}

fun withLambda(action: (String) -> String) {
    println("関数を実行します")
    println("ラムダを実行します")
    val result = action("ラムダ実行中")
    println(result)
    println("ラムダの実行が完了しました")
    println("関数を終了します")
}

ジェネリクス

ジェネリクス<T>は型パラメーターを作成できる機能。

fun main(args: Array<String>) {
    val instance = A("Text")
    println(instance.getValue())
}

class A<T>(private val value: T) {
    fun getValue(): T {
        return this.value
    }
}

outで共変(サブタイプの関係)を表せる。inで反変(スーパータイプの関係)を表せる。何もつけない場合は不変(サブタイプの関係性がない)を表せる。

どういうことかというと、in Tと型パラメーターを渡してTParentクラスを渡した場合、ChildクラスをTに渡したクラスを代入することができない。 しかし、Parentクラスの継承元のGrandParentクラスをTに渡したクラスは代入することができる。

これが反変という意味。

fun main(args: Array<String>) {
    // これはエラー
    val parentInstance: A<Parent> = A<Child>(Child())
    // これは成功
    val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent())
}

data class A<in T>(
    private val value: T,
)

open class GrandParent() {}

open class Parent: GrandParent() {}

class Child : Parent() {}

共変はその逆。

fun main(args: Array<String>) {
    // これは成功
    val parentInstance: A<Parent> = A<Child>(Child())
    // これはエラー
    val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent())
}

data class A<out T>(
    private val value: T,
)

open class GrandParent() {}

open class Parent: GrandParent() {}

class Child : Parent() {}

不変の場合はそもそもParentクラスとChildクラスにサブクラスの関係性があったとしても代入できない。その逆も然り。

fun main(args: Array<String>) {
    // これはエラー
    val parentInstance: A<Parent> = A<Child>(Child())
    // これもエラー
    val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent())
}

data class A<T>(
    private val value: T,
)

open class GrandParent() {}

open class Parent: GrandParent() {}

class Child : Parent() {}

アクセス修飾子

  • public
    • デフォルト
    • アクセスする場所に制限無し
  • internal
    • 同じモジュール(ファイル内)ならアクセスできる。
  • protected
    • 同じクラス、またはサブクラスからアクセスできる。
    • トップレベルからではアクセスできない。
  • private
    • 宣言したクラスが書かれたファイル内でだけアクセスできる。
    • 指定したクラスだけアクセスできる。
    • 外部にあるクラスからはアクセスできない。
    • 同じクラスの中だったらアクセスできる。

拡張関数

自分で定義した関数を既存のクラスに対して追加できる機能。

fun 拡張したい型.拡張関数名() {}で定義する

拡張関数内ではthisを通して追加先のクラスの関数やプロパティにアクセスできる。もちろん省略もできる。

既に定義されているメンバー関数と同じ名前の拡張関数を定義したときは、既に定義されているオリジナルのメンバー関数の方が優先される。

fun String.printSelf() {
    println(this.toString());
}
fun main(args: Array<String>) {
    val text = "Hello World"
    text.printSelf()
}

プロパティも拡張できる。val 拡張したい型名.新しいプロパティ名: 返したい型 get() { ... }を使ってゲッターを定義できる。セッターは定義できない。

val String.type: String
    get() {
        return "私は文字列です"
    }

fun main(args: Array<String>) {
    val text = "Hello World"
    println(text.type)
}

Null許容型型名?も拡張できる。

fun String?.printIfNotNull() {
    if (this == null) {
        println("僕はnullです")
        return
    }
    println(this.toString())
}

fun main(args: Array<String>) {
    val text: String? = null
    text.printIfNotNull()
}

分解宣言

分解宣言を使うとクラスで宣言したプロパティを複数の変数に分けて受け取ることができる。operator fun component1() = プロパティを使って実装する。

fun main(args: Array<String>) {
    val (a, b) = Disassembly(
        a = 1,
        b = 2
    )
    println(a)
    println(b)
}

class Disassembly(val a: Int, val b: Int) {
    operator fun component1() = a
    operator fun component2() = b
}

データクラスを使う場合はoperator fun component1()の形式はいらない。自動的に実装してくれる。

fun main(args: Array<String>) {
    val (a, b) = CustomPair<Int, Int>(1, 2)
    println(a)
    println(b)
}

public data class CustomPair<out A, out B>(
    public val a: A,
    public val b: B,
) {
    public override fun toString(): String = "($a, $b)"
}

標準クラスにPairTripleというクラスがある。それらもデータクラスで定義されていて、分解可能になっている。

fun main(args: Array<String>) {
    val (a, b) = Pair<Int, Int>(1, 2)
    println(a)
    println(b)
    val (c, d, e) = Triple<Int, Int, Int>(4, 5, 6)
    println(c)
    println(d)
    println(e)
}

遅延初期化

lazyを使うことで、変数やプロパティが実際にアクセスされた場合に初期化が行われるようにすることができる。

実行したらわかるが、リクエストを送信しますレスポンスを受け取りましたは、最初のfetchedResponse変数へのアクセス時のみ出力されて、それ以降の変数へのアクセス時には出力されないようになっている。

val fetchedResponse by lazy { fetchRequest() }

fun fetchRequest(): Map<String, String> {
    println("リクエストを送信します")
    val result =  mapOf("name" to "Tanaka Taro", "age" to "27")
    println("レスポンスを受け取りました")
    return result
}

fun main(args: Array<String>) {
    println("実行開始")
    println(fetchedResponse)
    println(fetchedResponse)
    println("実行終了")
}

lateinitを使えば、クラスのプロパティや変数の宣言後や作成後にその場で初期化せず、あとから任意のタイミングでそのプロパティや変数を初期化できる。注意点として、varで宣言しないといけない。

lateinit による変数の初期化 | まくまくKotlinノート

class LateClass {
    lateinit var text: String
}

fun main(args: Array<String>) {
    val instance = LateClass()
    instance.text = "HELLO WORLD"

    println(instance.text)
}

Type Alias

typealiasを使って既存の型の別名を定義できる。

typealias Response = Map<String, String>

関数型にもtypealiasを使える。

typealias Response = Map<String, String>
typealias Handler = (status: Int, message: String) -> Response

Delegated Properties

ReadWritePropertyインターフェース、もしくはReadOnlyPropertyインターフェースを実装したプロパティの移譲クラスを作成して、byを使って移譲クラスでオーバーライドしたgettersetterをプロパティのゲッターとセッターとして定義できる。これにより、何度もgettersetterを定義するという冗長な実装をしなくて済む。

プロパティをvalで宣言すると移譲クラスでオーバーライドしたゲッターのみ利用可能となる。

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

typealias Response = Map<String, String>
class FetcherDelegate(val url: String): ReadWriteProperty<API, Response> {
    override fun getValue(thisRef: API, property: KProperty<*>): Response {
        println("${url}に[GET]リクエストを送信中です")
        val response = mapOf("name" to "Tanaka Taro", "age" to "27")
        println("${url}から[GET]レスポンスを受信しました")
        return response
    }
    override fun setValue(thisRef: API, property: KProperty<*>, value: Map<String, String>) {
        println("${url}に[POST]リクエストを送信中です")
        println("${url}から[POST]リクエストを送信しました ${value}")
    }
}

class API() {
    var requestPath: Response by FetcherDelegate("http://example.com/users/1")
}
fun main(args: Array<String>) {
    val api = API()
    api.requestPath = mapOf("name" to "Suzuki Taro", "age" to "28")
    println(api.requestPath)
}