Kotlinプログラミングを読んで気になった箇所メモ ①
Kotlinプログラミングを読んで気になった箇所メモ ②の続き
6章 ~ 13章 まで(9章はskip)
6章 null安全と例外
6.2 Kotlinの明示的なnull許容型
Int
や、String
等の型はnullの代入を許さない。(null非許容型)
Int?
, String?
のように、型の末尾に ?
を加えることでnullを許容する型になる。(null許容型)
6.4 null安全
null許容型として定義された値に対して関数を呼び出すとコンパイルエラーになる。
nullの可能性があるのでコンパイラが認識してエラーにする。
val name: String? = "mike"
name.capitalize() // コンパイルエラー
nullを許容しない型で使うのが一番だが、null許容型を使う状況が出てくる。
null許容型から関数を呼び出す方法は4つある。
1. セーフコール演算子 ?.
セーフコール演算子 ?.
を付けることで、コンパイラはnull値かどうかをチェックする。
非null の場合はそのまま呼び出すが、
null の場合は呼び出しをスキップする(nullを返却するわけではないので注意)
val name: String? = null
name?.capitalize() // capitalize()はコールされない
2. 二重感嘆符演算子 !!.
注意が必要な演算子。
二重感嘆符演算子 !!.
はnull許容型 -> 非null許容型 キャストする。
対象がnullだった場合はKotlinNullPointerExceptionが発生するので、nullではないことが確信がある場合のみ使うこと。
val name: String? = null
name!!.capitalize() // KotlinNullPointerException
3. ifでチェックする(スマートキャスト)
値がnullかどうかをチェックするだけ。
コンパイラがnullでは無いことを理解してくれるので、 !!.
でキャストする必要がない。
val name: String? = null
if (name != null) {
name.capitalize() // String? -> String型へのキャストが不要(スマートキャスト)
}
4. null合体演算子 ?:
三項演算子のようなもの。
?:
の左辺がnullだった場合、 右辺を実行する。
val name: String? = null
println(name?.capitalize() ?: "John") // John
7章 文字列
7.5 もっと知りたい? 文字列の文字を順番に処理する
StringのforEach関数
を呼び出せば、文字列を一文字ずつ処理することができる。
"Hello,John!".forEach {
println(it) // itの型はChar
}
/*
出力結果
H
e
l
l
o
,
J
o
h
n
!
*/
8章 数
8.4 文字列を数値型に変換する
Stringが持っているtoInt
, toDouble
, toBigDecimal
等の関数はフォーマットが不適切な場合例外がでてしまう。
val num = "5.91".toInt() // java.lang.NumberFormatException
文字列を数値型に安全に変換する関数として、toIntOrNull
,toDoubleOrNull
等が提供しているので、こっちを利用するほうが良い。
val num = "5.91".toIntOrNull() ?: 0
println(num) // 0
9章 標準関数
10章 リストとセット
10.1 リスト
リスト型も型推論は可能。
だが、String型リストを作りたい場合は型パラメータ(のこと)明示すること。
Listはジェネリック型なので、明示しないと何でも入ってしまう。
// 型指定無しだとListに何でも入る。
val list = listOf("太郎", "次郎", "三郎", 1, true)
// Stringのみを受け付けるList
val names: List<String> = listOf("太郎", "次郎", "三郎")
リストの要素をアクセスする
リストの要素にアクセスする際インデックスを指定は ArrayIndexOutOfBoundsExceptionが発生する可能性がある。
代わりにgetOrElseか、getOrNullを使ったほうが良い。
val list = listOf("太郎", "次郎", "三郎")
println(list[3]) // java.lang.ArrayIndexOutOfBoundsException
println(list.getOrElse(3) {"Unknown"}) // Unknown
println(list.getOrNull(3) ?: "Unknown") // Unknown
リストの内容を変える
listOf
はRead-Onlyのリストなので、add
やremove
等のリストの内容を書き換えることができない。
(var
にすればリスト自体の代入は可能)
書き換える場合は mutableListOf
を使う。
val mutableList = mutableListOf("太郎", "次郎", "三郎") // ミュータブルリスト(変更可能なリスト)
mutableList.remove("太郎")
mutableList.add("士郎")
println(mutableList) // [次郎, 三郎, 士郎]
val readOnlyList = mutableList.toList() // MutableList -> List(読み取り専用) に変換する
readOnlyList.toMutableList() // List -> MutableList の変換も可能
list -> mutableListに変換できるということは、
コードの書きようによってはイミュータブルなリストも変更可能にしてしまうので注意。
10.9 もっと知りたい? 各種の配列型
KotlinとJavaの間の相互運用性をサポートする目的で、Javaの配列を扱うために、Kotlinには Array がある。
例えば、以下のようなJavaメソッドをもっていて、それをKotlinから呼び出したい時。
public class JavaMethods {
// 引数に配列のintを受け取る関数
public static void displaySum(int[] nums) {
System.out.println(IntStream.of(nums).sum());
}
}
val nums = intArrayOf(1,2,3,4,5) // 型はIntArray
JavaMethods.displaySum(nums); // 15
// ListからArray変換も可能
// JavaMethods.displaySum(listOf(1,2,3,4,5).toIntArray())
Arrayはコンパイルされると、Javaのプリミティブ配列になる。
原則として Javaコードとの相互変換等のやむを得ない場合のみ使うこと。
基本はいろんな機能が備わったListのようなコレクション型を使うべき。
11章 マップ
11.1 マップの作成
// to関数を使って左辺と右辺の値を1個のPairに変換する
println(mapOf("Taro" to 15, "Jiro" to 13, "Saburo" to 10)) // {Taro=15, Jiro=13, Saburo=10}
// Pair を使って作成する場合
println(mapOf(Pair("Taro", 15), Pair("Jiro", 13), Pair("Saburo", 10))) // {Taro=15, Jiro=13, Saburo=10}
11.2 マップの値をアクセスする
いくつか方法がある
val map = mapOf("Taro" to 15, "Jiro" to 13, "Saburo" to 10)
// map["{key}"] でアクセス
println(map["Taro"]) // 15
// ない場合、nullを返却
println(map["Siro"]) // null
// getValue ない場合、Exceptionが発生するので注意
println(map.getValue("Siro")) // java.util.NoSuchElementException
// getOrElse ない場合、関数を実行する
println(map.getOrElse("Siro") {"No such name"}) // No such name
// getOrDefault ない場合、デフォルト値
println(map.getOrDefault("Siro", 0)) // 0
12章クラス定義
12.5 クラスプロパティ
getter,setter(varのみ) は自動で生成される。オーバーライドも可能
class Motorcycle {
// クラスプロパティ
val brand = "Honda"
val model = "CB250R"
var color = "black"
get() = field.capitalize()
// インスタンスメソッド
fun showSpec() {
println("Brand: $brand, Model: $model, Color: $color")
}
}
fun main(args: Array<String>) {
val cb250r = Motorcycle()
cb250r.showSpec() // Brand: Honda, Model: CB250R, Color: Black
cb250r.color = "red"
cb250r.showSpec() // Brand: Honda, Model: CB250R, Color: Red
}
12.9 もっと知りたい? 競合に対する防御を固める
MotercycleにReleaseInfo(発売情報)クラスのプロパティを持っているとする。
発売情報は確定しない場合があるのでnullableとする。(設計の良し悪しは置いておく)
MoterCycleがReleseInfo(発売情報)を持っていればその情報を表示するメソッド(showRelease()
)をつくるとする。
以下のようにnullチェックだとコンパイルエラーになる
class ReleaseInfo(var date: String, var isRelease: Boolean)
class Motorcycle {
var releaseInfo: ReleaseInfo? = ReleaseInfo("2020-10-01", false)
fun showRelease() {
if (releaseInfo != null) {
// コンパイルエラー ReleaseInfoへのスマートキャストが不可能
println("${releaseInfo.date}, ${if (releaseInfo.isRelease) "Sale now!" else "coming soon"}")
}
}
}
これは、他から releseInfo
の状態を書き換える(競合状態)可能性があるため、コンパイルエラーになる。
具体的には「 relaseInfo
がnullチェックをパスしても、次の処理のprintlnするまでに、MotercycleのreleseInfo
プロパティが書き換えられる可能性がある。」ためnullにならないことを保証できないため。
この問題を解決する方法としては標準関数を使うのが有効。
fun showRelease() {
releaseInfo?.also {
println("${it.date}, ${if (it.isRelease) "Sale now!" else "coming soon"}")
}
}
第13章 初期化
13.1 コンストラクタ
Javaみたいな一時的な変数用意して代入するやり方。
// プライマリコンストラクタを定義
class Motorcycle(_brand: String, _model: String, _color: String) {
// 一時的な変数をMotercycleプロパティに代入
val brand = _brand
val model = _model
var color = _color
..
}
fun main(args: Array<String>) {
val cb250r = Motorcycle("Honda", "CB250R", "black")
}
プライマリコンストラクタのなかでプロパティを定義する
「既定のsetter/getterを使うプロパティ」ならば、コンストラクタの中にプロパティを書くこともできる。
一時的な変数の定義が不要でコードが削減できるので、このやり方で書くのが望ましい。
class Motorcycle(val brand: String, val model: String, _color: String) {
// colorはgetterをカスタムしているので、コンストラクタでプロパティ定義はできない
var color = _color
get() = field.capitalize()
..
}
デフォルトの引数を指定することもできる
class Motorcycle(val brand: String = "Honda", val model: String = "CBR250", _color: String = "black") {
..
}
セカンダリコンストラクタ
Javaでいうオーバーロードのようなもの。
プライマリコンストラクタ or 別のセカンダリコンストラクタを呼び出す必要がある。
class Motorcycle(val brand: String, val model: String, _color: String) {
var color = _color
get() = field.capitalize()
// セカンダリコンストラクタ
constructor(_color: String) : this("KAWASAKI", "Ninja250", _color)
...
}
13.2 初期化ブロック
init{}
で囲む。
プロパティの設定だけでなく、処理を実装することも可能。
例えばコンストラクタへの引数が有効化どうかの検証等が実行できる。
class Motorcycle(val brand: String, val model: String, _color: String) {
var color = _color
get() = field.capitalize()
init {
require(brand.isNotBlank(), { "ブランドを空にすることはできません。" })
}
}
13.4 初期化の順序
以下の順番で実行される。
Javaバイトコードに逆コンパイルするとわかりやすい。
- プライマリコンストラクタのインラインプロパティ
- クラスレベルで要求されたプロパティの代入(配置によっては3にもなる)
- initブロックでのプロパティ代入と関数コール(配置によっては2にもなる)
- セカンダリコンストラクタでのプロパティ代入と関数コール
// 1. プライマリコンストラクタのインラインプロパティ
class Motorcycle(val brand: String) {
// 2. クラスレベルで要求されたプロパティの代入
val model: String = "CBR250"
var color: String
get() = field.capitalize()
// 3. initブロックでのプロパティ代入と関数コール
init {
require(brand.isNotBlank(), { "ブランドを空にすることはできません。" })
color = "black"
}
constructor(brand: String, _color: String) : this(brand) {
// 4.セカンダリコンストラクタでのプロパティ代入と関数コール
color = _color
}
...
}
13.5 初期化の遅延
クラスの「null非許容」型のプロパティはインスタンスが構築されるときは、すべてnull
ではない値で初期化されることになる。
ただ、このルールを曲げることができる。
Android開発向けっぽい?
lateinit
varプロパティ宣言時に、lateinitキーワードを付与することで、インスタンス生成時に初期化しないようになる。
初期化しないまま参照すると UninitializedPropertyAccessExceptionが発生するので注意。
必要に応じてisInitializedつけよう。
class Motorcycle {
lateinit var color: String
fun printColor() {
if (this::color.isInitialized) println("Color: $color") else println("Colorを決めてください")
}
}
遅延初期化(lazy initialization)
変数の初期化を、最初のアクセスされるまで待たせることもできる。
Kotlinではデリゲート(委譲)という機構を使って実装される。
以下の例では遅延初期化する意味はあまりないが、
初期化時に大きなファイルを読み込む等の重い処理を行う場合や、プロパティへアクセスがすぐに必要がない場合に使うと良いかもしれない。
class Motorcycle {
// 遅延初期化 初期化時にselectColor()を実行する
val color by lazy { selectColor() }
private fun selectColor() = listOf<String>("red", "white, blue").shuffled()
.first()
.capitalize()
fun printColor() {
println("Color: $color")
}
}
fun main(args: Array<String>) {
val m = Motorcycle()
m.printColor() // 初めてcolorが参照されるので、このタイミングでselectColor()が実行される
}