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
0x
や0b
をつかって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は重複を防ぐことができる順序なしのコレクションである。
setOf
とmutableSetOf
でSetを生成できる。
Map
Mapはキーと値のペアのコレクションである。
mapOf
とmutableMap
で生成できる。
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と違ってmap
やfilter
を利用した場合に、順序的ではなく垂直的に評価される。
つまり、Sequenceに対してmap
とfilter
がチェーン呼出されていたら、最初の要素のみがmap
とfilter
で処理された後に次の要素がmap
とfilter
で処理されるといった流れを繰り返す。
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
を使ってブロック宣言する。コンストラクタよりも前に呼び出される。
プロパティはvar
とval
を使って宣言する。必ず初期化しないといけない。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
を利用してシングルトンクラスを生成できる。
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 }
みたいな使い方ができる。
ちなみにit
とthis
はコンテキストオブジェクトと言われていて、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
と型パラメーターを渡してT
にParent
クラスを渡した場合、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)" }
標準クラスにPair
とTriple
というクラスがある。それらもデータクラスで定義されていて、分解可能になっている。
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
を使って移譲クラスでオーバーライドしたgetter
とsetter
をプロパティのゲッターとセッターとして定義できる。これにより、何度もgetter
とsetter
を定義するという冗長な実装をしなくて済む。
プロパティを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) }