はじめに
前回は、はじめてのKotlinアプリケーションを作成しました。今回からKotlinの文法について説明していきます。
第2章:Kotlinの文法
2.1 変数
2.1.1 変更可能(ミュータブル)な変数:var
Kotlinで変数を宣言するには、varキーワードを使用します。varは変更可能(ミュータブル)な変数です。Javaの(finalではない)通常の変数の宣言です。
// varは変更可能(ミュータブル)な変数
var user: String = "tamito0201" // Kotlinでは文の終わりにセミコロンは不要です。
2.1.2 変更不可能(イミュータブル)な変数:val
定数を宣言するには、valキーワードを使用します。 valで宣言された変数は、一度初期化されると、値を変更することはできません。Javaでいうfinalと同じです(※1)。
// valは変更不可能(イミュータブル)な変数
val CONNECT_TIMEOUT: Int = 3000
valは、変更不可にすべき変数を、プログラマが間違って変更してしまうことを防ぐ目的として使用されます。このため、変更可能な変数varを使う必要がないときは、必ずvalを使用すべきです。コードを書くときはvalを使用するクセをつけましょう。
統合開発環境であるIntelliJ IDEAは、コードを分析して、varの代わりにvalを使うべきケースを検出するすぐれものです。
「Variable is never modified and can be declared immutable using ‘val’ more…」といいうメッセージで、変数は変更されないのでimmutable(イミュータブル)なvalとして宣言すべきと提案してくれます。これを変更するには「Alt」+「Return」(Windowsの場合)を押すと、
valに変更してくれます。
さきほどvalは変更不可能な変数と説明しましたが、例外があります。valで宣言された変数の参照自体イミュータブル(変更不可能)ですが、それが指すオブジェクトはミュータブルになりえます。以下のようなListの場合。listが指すオブジェクトの値は変更可能です。
val list = arrayListOf("promari", "programming")
list.add("lesson")
println(list) // [promari, programming, lesson]
- ※1 : Java10からはvarによる型を省略して変数宣言が可能です。
2.1.3 型推論(type inference)
Kotlinでは型推論(type inference)が可能なので、宣言時に値が代入される変数については、型を省略して変数を定義することもできます。型推論とはプログラマが型を明記しなくても代入式などから、コンパイラが文脈から推論して付くべき型を自動で推論してくれる機能です。
val CONNECT_TIMEOUT = 3000
のように書くこともできます。どういういうことがと申しますと、右辺はint型のデータであることが分かっているため、わざわざ変数名にIntと明示しなくても、コンパイラが自動的に型を推論してくれるのです。
型推論を持っている言語は、Kotlin以外に、Haskell、ML、Vala、C#、Scala、Objective Caml、D言語などがあります。
2.1.4 演習
変数(radius)と定数(PI)を用いて円の面積を計算するプログラムを作成しましょう。
2.2 関数
2.2.1 関数の宣言
Kotlinでのシンプルな関数は以下のように記述できます。
fun addition(a: Int, b: Int): Int {
return a + b
}
関数の宣言はfunキーワードで始まり、関数名(この場合は)がadditionが続きます。そして関数additionは、Int型の引数aと引数bを取り、aとbの和を計算したInt型の結果を返す関数です。
引数という言葉は厳密には、関数を定義する際に用いられる変数を仮引数(parameters または formal parameters)、関数を呼び出す際に渡す値を実引数(argument または actual parameters)と呼びます。
Kotlinでは、関数は簡略して記述する事ができます。関数の中身が式1つだけなら波括弧は省略できます。
fun addition(a: Int, b: Int): Int = a + b
もし戻り値の型が推論可能ならさらに戻り値の型も省略できます。
fun addition(a: Int, b: Int) = a + b
波括弧に本体が入る形で関数が記述されていた場合は、その関数は「ブロック本体(block body)を持つ」といいます。もし式を直接返すのであれば「式本体(expression body)を持つ」といいます。
- TIPS
IntelliJ IDEAではブロック本体(block body)と式本体(expression body)を変換する機能が用意されています。使ってみましょう。
今関数additionはブロック本体(block body)を持っていますよね。
関数本体のところにマウスのフォーカスを充てると豆電球マークが出ます。
豆電球をクリックすると、「式の本体に変換する」というメニューが現れるのでクリック(または「Alt」+「Enter」キーでも同じことができます)すると、
式本体を持つ関数に変換できました。
さらに戻り値の型が推論可能ならそれすらも省略できます。
どうでしょう。他の言語を学習した方なら、最初は慣れないので読みづらいと感じるかもしれません。これも慣れです。慣れてくると他の言語での記述方法が冗長のように感じてくるものです。
2.2.2 デフォルト値
Kotlinでは、関数にデフォルト値を設定しておくことで、引数を省略することが可能です。
fun addition(a: Int = 1, b: Int = 2) = a + b
fun addition2() = addition(8, 16) // 8 + 16 = 24 が出力される
fun addition3() = addition(4) // 4 + 2 = 6 が出力される。
fun addition4() = addition() // 1 + 2 = 3 が出力される。
2.2.3 演習
苗字と名前という仮引数をとる関数を用いて、苗字と名前を連結した文字列を返却する式本体を持つ関数を作成してください。
※文字列の連携方法はまだ学習していないですが、ググってプログラムを作成してくださいね。今から調べる癖を身に付けていきましょう。
2.3 データ型
2.3.1 基本型
Kotlinは、他の言語と同じようにInt、Double、Boolean、Charなどの型が用意されています。ただし、Javaと違ってプリミティブ型は用意されていません。Int型やBoolean型などすべてがオブジェクトのように動作します。Javaがint、doubleなどに対応するInteger、Doubleなどのラッパー型があるのとは対照的ですね。
ただし、オブジェクトだからといってパフォーマンスの懸念はありません。Kotlinは、パフォーマンスを向上させるために、String以外は、実行時にInt、Char、Booleanなどのオブジェクトをプリミティブ型に最適化してくれるからです。
2.3.1.1 数値型(Numbers)
Kotlinの数値型はJavaに似ていて、整数型と浮動小数点型に分類できます。
- 整数型(Integers)
型 | 説明 |
---|---|
Byte | 8ビット符号付き整数 |
Short | 16ビット符号付き整数 |
Int | 32ビット符号付き整数 |
Long | 64ビット符号付き整数 |
- 浮動小数点型(Floating Point Numbers)
型 | 説明 |
---|---|
Float | 32ビット浮動小数点数 |
Double | 64ビット浮動小数点数 |
以下に数値型の例を示します。
val aByte: Byte = 11
val aShort: Short = 111
val aInt = 1000 // 10進数(型推論の場合)
val aLong = 1000L // サフィックスに'L'を付けると、Long型。これでコンパイラは型推論でLong型と推論します。
val aDouble = 126.78 // 浮動小数点はデフォルトでDouble型
val aFlout = 126.78f // サフィックスに'f'または'F'を付けると浮動小数点型になります。
val aHex = 0x0AFF // プレフィックスに'0x'または'0X'を付けると16進数
val aBinary = 0b1100 // プレフィックスに'0b'または'0B'を付けると2進数
2.3.1.2 論理型(Booleans)
論理型は他の言語と同じ様にtrueとfalseの2つの値を取ります。
val aBoolean = true
val bBoolean = false
2.3.1.3 文字型(Characters)
文字はChar型を使用します。ただし、注意点があり、Javaとは異なり、Char型は数値としては扱うことはできません。Char型を表すにはシングルクォートを使います。
val aChar = 'A'
val dChar = '9'
val lChar = '\n'
val uChar = '\uFF00' // UNICODE
2.3.1.4 文字列型(Strings)
文字列はStringクラスを使って表現されます。文字列型は、Javaと同様にイミュータブルな変数として表されます。
val name = "tamito0201"
配列のように[index]を使用して、String内の特定のインデックスにある文字にアクセスできます。
val name = "tamito0201"
val charIndex = name[0] // 't'
val lastChar = name[name.length - 1] // '1'
// 配列なのでループで回すこともできます。
for (chr in name) {
printn(chr)
}
// 二重引用符(ダブルクォート)で宣言された文字列は、 '\ n'(改行)、 '\ t'(タブ)などのようにエスケープ文字を含むことができます。
val aEscapedStr = "Hello tamito0201\nHow are you?"
// Kotlinではトリプルクォートを使ってエスケープがない複数行にまたがる文字列を格納できる。
val aMultilineStr = """
ここにかいた文字列は
改行も含めて、そのままの文字列が出力されます。
インデントも。これ便利ですね。
"""
// $を使って文字列の中に変数の値や計算結果を埋め込めます。
val i = 100
avl str = "i = $i, i ÷ 10 = ${i / 10}" // "i = 10, i ÷ 10 = 10"
2.3.2 型変換
// 整数の数値代入
val b: Byte = 1 // IntをByteと見なしてくれる
val l: Long = 100 // OK
val i: Int = 1L // NG。明示的にLongを指定すると代入できない。
val f: Floug = 3.0 // NG。浮動小数点はデフォルトでDouble型なのでNG。暗黙的な型変換はKotlinには存在しない。
val d: Double = 3 // NG。整数を浮動小数点とはみなさない。
// 小さな型から大きな型への暗黙的キャストはKotlinではできない
val a: Byte = 1
val b: Int = a // NG。
// 型は以下のように明示的に変換します。
// IntからLongの型変換
val aInt = 100
val aLong = aInt.toLong()
// 大きな型から小さな型への変換
val doubleValue = 123.45
val intValue = doubleValue.toInt() // 123
// Kotlinのすべての型は、Stringに変換するためのtoString()というヘルパー関数をサポートしています。
val aInt = 1111
aInt.toString() // "1000"
// 文字列から数値型への変換
val str = "1111"
val intValue = str.toInt()
// 文字列から数値への変換が不可能な場合
// str = "1111AAA"
str.toInt() // Throws java.lang.NumberFormatException
2.3.3 ビット演算
Kotlinでは論理演算子、ビット演算子はJavaのように特別な文字(&など)で表現されるわけではありません。関数によりその機能が提供されています。
Kotlin | Java | 説明 |
---|---|---|
shl | << | 左シフト |
shr | >> | 符号付右シフト |
ushr | >>> | 符号なし右シフト |
and | & | 論理積 |
or | \ | 論理和 |
xor | ^ | 排他的論理和 |
inv() | ~ | ビット反転 |
2.4 配列
Kotlinの配列は、他の言語に見られるような配列はありません。その代わりにArrayクラスを使います。そのため、Kotlinの配列は、変更可能であり、要素に対して読み取り操作と書き込み操作の両方を実行できます。
2.4.1 配列を生成する
配列を生成するには、Arrayクラスのコンストラクタを使用する方法と、Kotlinの標準ライブラリを使用して配列を生成する方法があります。
2.4.1.1 コンストラクタを使用して生成
まず、Arrayクラスのコンストラクタを使用して生成する方法を説明します。
Arrayクラスのコンストラクタは引数を2つ取ります。1つ目が配列の長さ。もう1つが、配列初期化のための関数を取ります。
Array<型名>(要素の数, {初期化処理})
Kotlinでは型推論が可能なので、型名を省略できます。
// [0, 2, 4]の値で初期化
val doubleArray = Array(3, {it * 2})
// Array<String>を["0", "2", "4"]の値で初期化
val strDoubleArray = Array(3, { i -> (i * 2).toString() })
2.4.1.2 標準ライブラリを使用して生成
もう一方の、Kotlinの標準ライブラリを使用して配列を生成する方法です。
メソッド | 説明 |
---|---|
arrayOf | Arrayインスタンスを簡易に生成 |
arrayOfNulls | 指定されたサイズの指定されたタイプのオブジェクトの配列を、null値で初期化して返します。 |
listOf | java.util.Listを生成。要素を変更できません。 |
arrayListOf | java.util.ArrayListインスタンスを生成。Javaと同様に追加、削除などが可能。 |
linkedListOf | java.util.LinkedListインスタンスを生成。Javaと同様に追加、削除などが可能。 |
// [1, 2, 3, 4, 5]を作成
val list = arrayOf(1, 2, 3, 4, 5)
// ["a", "b", "c"]を作成
val strList = arrayOf("a", "b", "c")
// nullで初期化されたサイズ5の配列を作成する
val nullList: Array<Int?> = arrayOfNulls(5)
// [1, 2, 3, 4, 5]のjava.util.Listを生成。要素を変更できない。
val list = listOf(1, 2, 3, 4, 5)
// [1, 2, 3, 4, 5]のjava.util.ArrayListを生成。要素変更可能。
val arrayList = arrayListOf(1, 2, 3, 4, 5)
// [1, 2, 3, 4, 5]のjava.util.LinkedListを生成。要素変更可能。
val linkedList = linkedListOf(1, 2, 3, 4, 5)
2.4.2 プリミティブ配列
arrayOfメソッドをプリミティブ値と共に使用することができますが、Kotlinはプリミティブ値を対応するオブジェクトラッパークラスにオートボクシングするため、パフォーマンスに影響を及ぼします。このオーバヘッドを回避するために、Kotlinはプリミティブ配列をサポートしており、プリミティブ型のint, char, boolean, long, short, byte, float, doubleの型には、専用のarrayOfメソッドが用意されています。
intArrayOf(), charArrayOf(), booleanArrayOf(), longArrayOf(), shortArrayOf(), byteArrayOf(), floatArrayOf, doubleArrayOf関数を使用してプリミティブ配列を作成できます。
// プリミティブ型の配列はオーバーヘッド無しでボクシングができる
var intArray = intArrayOf(1, 2, 3)
// [0, 2, 4]の値で初期化(オーバヘッド無し)
val doubleArray = IntArray(3, {it * 2})
2.4.3 要素の取得、代入
Kotlinにはset()とget()関数があり、それぞれ配列の特定の要素を直接変更したりアクセスしたりできます。ただし、通常は直接インデックスを指定して要素の取得、代入します
val strArray = arrayOf("a", "b", "c")
// 取得
println(strArray[0])
println(strArray[1])
println(strArray[2])
println(strArray.get(0))
println(strArray.get(1))
println(strArray.get(2))
// 代入
strArray[0] = "d"
strArray[1] = "e"
strArray[2] = "f"
strArray.set(0, "d")
strArray.set(1, "3")
strArray.set(2, "f")
2.5 コレクション
KotlinはJavaとは異なり、イミュータブル(変更不可能:immutable)とミュータブル(変更可能:mutable)のインタフェースが明確に分かれています。
ミュータブル(変更可能:mutable)なコレクションは要素を変更したり、追加や削除が可能ですが、イミュータブル(変更不可能:immutable)なコレクションは追加や削除ができません。
KotlinにはJavaと同様にコレクション系オブジェクト(List、Set、Mapなど)があります。ただし、上述したようにリードオンリーでイミュータブルでアクセスするのか、ミュータブルで編集用にアクセスするのかによってインタフェースが使い分けられます。
具体的には、要素を参照するだけであれば、Kotlinではイミュータブル(変更不可能:immutable)なCollection インタフェースを使用し、要素の追加や削除が必要な場合は ミュータブル(変更可能:mutable)なMutableCollection インタフェースを使用します。
Kotlinでは、List、Set、Mapはイミュータブル(変更不可能:immutable)であるCollection インタフェースを実装しています。一方で、MutableList、MutableSet、MutableMapはミュータブル(変更可能:mutable)であるMutableCollectionインタフェースを実装しており、要素の編集が可能です。
- List、Set、Mapは読み取り専用。add等の更新系のメソッドは備えていない。
- Mutableが頭に着いたMutableList、MutableSet、MutableMapは編集可能。add等の更新系のメソッドを備えている。
クラス図が読み取れる方は、以下のような継承関係となっております。
全体的なインタフェースの関係は以下のようなクラス図となります。
2.5.1 コレクションオブジェクト生成のための関数
コレクションオブジェクトには、インスタンスを生成するための、xxxOf() という形のユーティリティ関数が用意されています。
- List (immutable): listOf()・・・順序を持つコレクション。重複可
- MutableList (mutable): mutableListOf()
- ArrayList (mutable): arrayListOf()
- MutableList (mutable): mutableListOf()
- Set (immutable): setOf()・・・順序を持たないコレクション。重複不可
- MutableSet (mutable): mutableSetof()
- HashSet (mutable): hashSetOf()
- LinkedHashSet (mutable): linkedSetOf()
- SortedSet (mutable): sortedSetOf()
- MutableSet (mutable): mutableSetof()
- Map (immutable): mapOf()・・・キーバリューで値を保持するコレクション
- MutableMap (mutable): mutableMapOf()
- HashMap (mutable): hashMapOf()
- LinkedHashMap (mutable): linkedMapOf()
- SortedMap (mutable): sortedMapOf()
- MutableMap (mutable): mutableMapOf()
// 読み取り専用
val immutablelist = listOf(1, 2, 3, 4, 5)
// 書き込みも可能
val mutablelist = mutableListOf(1, 2, 3, 4, 5)
nullを覗いたListを生成することもできます。
val list = listOf(null, "a", "b", "c", null);
val notNullList = listOfNotNull(null, "a", "b", "c", null)
println(list) // [null, "a", "b", "c", null]を出力
println(notNullList) // ["a", "b", "c"]を出力
2.5.1.1 イミュータブル(変更不可能)なコレクション
// List<Int> を作る。Listは編集不可。
val immutablelist = listOf(1, 2, 3, 4, 5)
immutablelist[0] = 10 // ERROR!! 変更できない。
// Map<Int, String> を作る。Mapは編集不可
val immutablelistMap = mapOf(1 to "A", 2 to "B")
immutablelistMap[0] = "hoge" // ERROR!! 変更できない
ただし、注意が必要なのは、Kotlinのイミュータブル(変更不可能)なコレクションは、読み取り専用ではありますが、完全な変更不可能なオブジェクトではありません。なぜなら、ミュータブル(変更可能)なコレクションは、上述したクラス図の関係で、イミュータブル(変更不可能)なコレクションを継承しているため、そのインスタンスはイミュータブル(変更不可能)なコレクションに代入できてしまいます。
val mutableList = mutableListOf(1, 2, 3)
val immutableList: List<Int> = mutableList // mutableを読み込み専用に代入
mutableList.add(4)
println(immutableList)
これには注意が必要です。
2.5.1.2 ミュータブル(変更可能)なコレクション
// MutableList<Int> を作る。編集可能
val mutablelist = mutableListOf(1, 2, 3, 4, 5)
mutablelist[0] = 5
val mutableMap = mutableMapOf(1 to "A", 2 to "B")
mutableMap[0] = "hoge"
2.5.2 コレクションオブジェクトの変換
イミュータブル(変更不可能)なコレクションとミュータブル(変更可能)なコレクション間の変換には、toXXXメソッドを使用します。
// MutableList => List
val mutableList1 = mutableListOf(1, 2, 3)
val list1: List<Int> = mutableList1.toList()
// List => MutableList
val list2: List<Int> = listOf(1, 2, 3)
val mutableList2: MutableList<Int> = list2.toMutableList()
インタフェース | 説明 | 初期化メソッド | ミュータブル化 | イミュータブル化 |
---|---|---|---|---|
List | 変更不可の順序付きリスト | listOf | toMutableList() | – |
MutableList | 変更可の順序付きリスト | mutableListOf | – | toList() |
Set | 変更不可の集合 | setOf | toMutableSet() | – |
MutableSet | 変更可の集合 | mutableSetOf | – | toSet() |
Map | 変更不可のキーと値のコレクション | mapOf | toMutableMap() | – |
MutableMap | 変更可のキーと値のコレクション | mutableMapOf | – | toMap() |
2.5.3 Java コレクションのインスタンスの生成
Javaのコレクションのインスタンスを作る関数も用意されています。
// ArrayListを生成
val arrayList = arrayListOf(1, 2, 3)
// HashSetを生成
val hashSet = hashSetOf(1, 2, 3)
// HashMapを生成
val hashMap = hashMapOf(1 to 10, 2 to 20, 3 to 30)
javaコレクションとは相互変換が可能です。
- java.util.Iterable ⇔ kotlin.collections.Iterable
- java.util.Collection ⇔ kotlin.collections.Collection
- java.util.List ⇔ kotlin.collections.List
- java.util.Set ⇔ kotlin.collections.Set
- java.util.Map ⇔ kotlin.collections.Map
2.6 制御構文
2.6.1 if
Kotlinではif文は式です。Javaと違って値を返することができます。従って、三項演算子は存在しません。例を見ると、その意味が分かるかと思います。
// 通常のif文使い方
val max: Int
if (a > b)
max = a
// elseも今まで通り
val max: Int
if (a > b)
max = a
else
max = b
// if文は式。値を返せますので、三項演算子は存在しません。
val max = if (a > b) a else b
// ifの分岐はブロックにすることができ、ブロックの最後の行の評価結果が値となります。
val max = if (a > b) {
print("selected a")
a // a > b の場合 a が maxに代入される。
} else {
print("selected b")
b // b > a の場合 b が maxに代入される。
}
2.6.2 when
whenはC言語のような言語におけるswitch文の置き換えです。ただし、break文は必要ありません。
// オーソドックスな使い方
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // ブロックも使えます
print("x is neither 1 nor 2")
}
}
// Kotlinはwhenキーワードを使用して複数の条件を指定でき、非常に強力です。
when (obj) {
1 -> { res = "One" } // if obj == 1
"Hello" -> res = "Greeting" // if obj == "Hello"
is Long -> "Long" // if obj is of type Long
!is String -> "Not String" // if obj is not of type String
else -> {
// どれも当てはまらない場合に実行される
}
}
// 範囲やコレクションに含まれるかも指定できます。
when (x) {
// 複数の値をカンマで区切ることもできます。
0, 1 -> print("x == 0 or x == 1")
// 定数でなくてもOK
parseInt(s) -> print("sはxをエンコードする")
// xは範囲内か
in 2..10 -> print("xは範囲内")
// コレクションに含まれたら、xは有効
in validNumbers -> print("xは有効")
// xは範囲外か
!in 10..20 -> print("xは範囲外")
else -> print("どれにも該当しません。")
}
// whenもifと同様に式なので、値を返すことができます。
val hasPrefix = when(x) {
// 型チェックの結果xがStringだったら、"prefix"という文字が含まれているかどうか判定
is String -> x.startsWith("prefix")
else -> false
}
// 引数がなければ if-else if文の代わりに使えます。
val result = when {
value == 100 -> "100"
value in 10..99 -> "in 10..99"
value !in 10..99 -> "!in 10..99"
value as Int -> "Int"
else -> "どれにも該当しません。"
}
2.6.3 for
Kotlinのfor文は、Javaでいうforeach for (item in items) の書き方しか用意されていません。
val collection = listOf(1, 2, 3, 4, 5)
// collection内のアイテムをループ
for (item in collection)
println(item)
// 上と同様
for (item: Int in collection)
println(item)
// 範囲でループ(※1)
for (i in 1..10)
println(i)
// downTo, stepの指定(※2)
for (i in 100 downTo 1 step 2)
println(i)
- ※1 範囲の内容を理解してから読み進めて下さい。
もし配列やリストをインデックス付きで繰り返し処理したい場合、indicesを使います。
// indicesを使用して、インデックスでループ
for (i in array.indices)
print(array[i])
別の方法として、ライブラリ関数のwithIndexを使用することもできます。
// withIndexを使用して、インデックスと値のペアでループ
for ((index, value) in array.withIndex())
println("$indexの要素は$value")
// マップをキーと値のペアでループ。
for ((key, value) in map)
println("key=$key, value=$value")
規程のメソッドが実装されていればイテレータを備えるクラスを自作できます。(※2)
fun main(args: Array<String>) {
for(item in Iterator()) {
println(item)
}
}
class IteratorFunctions {
operator fun hasNext(): Boolean = Math.random() < 0.7
operator fun next(): String = "当たりました!"
}
class Iterator {
operator fun iterator() = IteratorFunctions()
}
- ※2 関数やクラスの内容を理解してから読み進めて下さい。
2.6.4 while
while と do..while は他の言語と同じように動作します。
while (x > 0) {
x--
}
do {
val y = retrieveData()
} while (y != null) // y はここで可視(visible)
Kotlinはループ中の従来の breakとcontinue演算子をサポートしています。
2.7 範囲
2.7.1 Range構文
Kotlinでは、Range構文という便利な機能があります。Range構文を利用して、開始値と終了値を指定することでシーケンスのリストを簡単に作成することができます。
例えば、1..5のRange構文は一連の値1, 2, 3, 4, 5を作成します。同様に’A’..’D’というRange構文は、A, B, C, Dという文字列を作成します。
実際のコードで説明します。
// iを1から4まで出力
for (i in 1..4) print(i) // "1234"と出力されます。
// chをAからEまで出力
for (ch in 'A'..'E') print(ch) // "ABCDE"と出力されます。
// 逆順でも繰り返しが可能です。
for (i in 4 downTo 1) print(i) // "4321"と出力されます。
// 繰り返しのステップも指定できます。
for (i in 1..10 step 2) print(i) // "13579"と出力されます。
// 逆ステップ
for (i in 10 downTo 1 step 2) print(i) // "108642"と出力されます。
// if文でも使用できます。
if (i in 1..5) { // 1 <= i && i <= 5 と等価です。
println(i)
}
特定の要素が範囲内に存在するか確認することもできます。
val oneToFive = 1..5
println("3 in oneToFive: ${3 in oneToFive}") // 3 in oneToFive: true
println("11 in oneToFive: ${11 in oneToFive}") // 11 in oneToFive: false
関数rangeTp()とdownTo()もRange構文と同じように使うことができます。rangeTo()は昇順でdownTo()は降順です。
val oneToFive = 1.rangeTo(5)
val sixToThree = 5.downTo(3)
for(x in oneToFive) print(x) // "12345"と出力されます。
for(n in sixToThree) print(n) // "543"と出力されます。
Range構文を使うことでリストを簡単に出力することができます。
// 1から10までのリスト
val oneToTen = (1..10).toList()
print(oneToTen) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]と出力されます。
// 10から1までのリスト
val tenToOne = (1..10).reversed().toList()
print(tenToOne) // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
// 10から1までのリスト
val tenToOne = (10 downTo 1).toList()
print(tenToOne) // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
// 1から10まで2刻みのリスト
val oneToTenStep2 = (1..10 step 2).toList()
print(oneToTenStep2) // [1, 3, 5, 7, 9]
2.7 Null安全
2.7.1 null許容型
KotlinとJavaの型システムの違いで、重要な違いとなるのがKotlinがnull許容型を明示的にサポートしていることでしょう。null許容型とは、プログラム内にて、どの変数がnullになり得るかを表現する方法です。Kotlinの型システムはnullになり得る型と、なり得ない型を区別します。これにより、Javaでnull参照するときに発生するNullPointerExceptionを回避するのに非常に役に立ちます。
Kotlinでは、デフォルトでnullを許容しません。nullを許容したい場合はNullableという特別な型を使います。型の後ろに?を付けるとNullableになります。
val num: Int = null // コンパイルエラー nullになり得ない
val num: Int? = null // Nullableのため、nullを許容する。
// Nullableでないため、String型はnullを許容しません。
var name: String = "tamito0201"
var name = null // コンパイルエラー
val len = name.length // nameはnullではないため、NullPointerExceptionは発生しません。
Nullableであればnullを入れられますが、nullを参照しようとコードを書くとコンパイルエラーとなります。Kotlinを始めとするモダンなプログラミング言語では、NullPointerExceptionの解決策として、実行時のエラーからコンパイル時エラーへと変換し、実行時に例外がスローされる可能性を減らしてくれます。
var name: String? = "promari"
name = null;
val len = name.length // コンパイルエラー
「Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type kotlin.String?」
つまり、
「nullになる可能性がある値を安全に操作するためには、?.演算子を使うか、もしくは!!演算子を使って非null型に変換してください。そうでないと、nullの可能性があるStringやIntについての”length()”関数を呼び出しできません」
とコンパイルエラーとなります。
// Nullableな変数のプロパティやメソッドを呼び出すにはnullチェックし、
// nullが代入されないことが保証されているのであれば、 Nullableでない型に代入してくれます。
var name: String? = "promari"
// name = null; でnameはnullの可能性がある
val len = if (name != null) name.length else -1
// if文の条件式でも、&&または||の事前にnullチェックを行っているのであれば、後続の判定ではnullにはなり得ないので、
// コンパイルエラーにならず安全に使用することができます。
val name: String? = "promari"
if (name != null && name.length > 0) println("私の名前は ${name.length} 文字です") // 私の名前は 7 文字です
2.7.1 安全呼び出し演算子(safe-call operator)
kotlinが優れている機能の一つに安全呼び出し演算子(safe-call operator)があります。安全呼び出し演算子はnullチェックとメソッド呼び出しを一つの演算子に結合したものです。
以下の式は、if (s != null) s.toUpperCase() else nullに相当します。?を使うとnullでなければ実行し、そうでなければnullを返すという判定処理を1文でかけるのです。
val len = s?.length
さらにlengthはnullがありえるので、チェインして以下のように書けます。
val lenLong = s?.length?.toLong()
2.7.2 エルビス演算子
Kotlinには、nullの代わりにデフォルト値を返す便利な演算子があります。それはエルビス演算子(Elvis operator)とよび、?:と表記されます。(null合体演算子(null-coalescing oerator)と呼ぶこともあります)
val lenLongEl = s?.length ?: -1
エルビス演算子は、2つの値を取り、前の値がnullでなければ結果はその値(s.length)となり、nullであれば結果は後ろの値(-1)を返します。
エルビス演算子を使うと以下の関数も1行で書くことができます。
fun Person.countryName() = company?.address?.country ?: "Unknown"
実用的な例を示しておきます。
fun printShippiinLabel(person: Person) {
val address = person.company?.address
?: throw IllegalArgumentExceptioon("No address") // 住所が空なら例外をスロー
with (address) {
printn(streetAddress)
printn("$zipCode $city, $country")
}
}
2.7.3 安全キャスト
Kotlinにおける型キャストのための演算子はas演算子です。
// yをStringにキャストしますが、キャストできない場合は例外ClassCastExceptionをスローします。
val x: String = y as String
上記はnullでも例外をスローしますが、nullを許容するにはNullableとします。
val x: String? = y as String?
ただし、yがnullでなく、Stringにキャストしようとする場合、やはり例外ClassCastExceptionをスローします。
これを防ぐために安全キャスト演算子(as?)を使用します。
val x: String? = y as? String
as?と使用すると、失敗時に例外をなげずにnullを返します。なので、キャスト結果はNullableとなります。
安全キャスト演算子はエルビス演算子と組み合わせると非常に便利に使えます。
// 型をチェックして一致しなければfalseを返す
val otherPerson = o as? Person ?: return false
2.7.5 安全呼び出し
Javaでは以下のようなnullチェックはよくみかけますよね。
Person person = // nullの可能性あり
// person.run(); // NullPointerExceptionの可能性あり
if (person != null) {
person.run(); // Nullチェック済
}
上記をKotlinで書くと以下になります。
val person; Person? = // nullの可能性あり
// person.run // コンパイルエラー
if (person != null) `
person.run() // Nullチェック済
}
Kotlinではnull許容型の関数は直接呼び出すことができませんでした。Kotlinではもっと便利な構文が用意されています。
val person: Person? = // nullの可能性あり
person?.run()
?.に続けて関数の呼び出しやプロパティにアクセスできます。上記の例だと、personがnullだった場合は、ただnullが返却されます。これをKotlinでは「安全呼び出し」と呼びます。
2.7.4 非null表明
安全呼び出し(.?をつける) 以外にnull許容型を扱う方法があります。
val person: Person = // nullの可能性あり
val name = person.name // コンパイルエラー
val name = person!!.name // コンパイルOK。ただし、NullPointerExcetionの可能性あり
!!演算子は、null許容型を非null許容型に強制的に変換します。この演算子はなるべく使わずにコーディングしましょう。
あえて使うのであれば、「値がnullでないことは把握している。もしそれが間違っていたとしたら例外は覚悟している」と伝えていることになります。
一つ注意しないといけないのが、!!を用いて例外が起きた場合、スタックトレースは例外がスローされた場所の式ではなく行番号を表します。したがって、同じ行で複数の!!を使うことは避けてくださいね。
person.company!!.address!!.country
2.9 等価性
今回は、Kotlinにおける「==」と「===」演算子、「.equals」メソッドの違いについて説明します。Kotlinには2種類の等価性があります。構造の等価性と参照の等価性です。
2.9.1 「==」演算子(構造の等価性)
構造の等価性は、両方の値が同じ(または等しい)かどうかを評価する「==」演算子を使用します。Kotlinの「==」演算子はJavaの「==」とは違い、データまたは変数のみを比較しますが、Javaや他の言語では、「==」演算子は参照を比較するために使用されます。Javaでは、equals()メソッドがKotlinの「==」演算子と同等です。
Kotlinの「==」演算子の否定形は、「!=」演算子です。両者の値が等しくない場合に使用されます。
2.9.2 「===」演算子(参照の等価性)
参照の等価性は、2つの変数またはオブジェクトの参照が同じかどうかを評価する「===」演算子を使用します。両方のオブジェクトまたは変数が同じオブジェクトを指している場合にのみ当てはまります。 Kotlinの「===」の否定されたものは「!==」です。これは、両方の値が互いに等しくない場合に比較するために使用されます。
実行時にプリミティブ型として表される値(例えば、Int)の場合、「===」等価検査は「==」検査と同等です
2.9.3 .equalsメソッド
equals(other:Any?)メソッドはAnyクラスに実装されていますので、拡張クラスでオーバーライドすることができます。 .equalsメソッドは「==」演算子と同じように変数やオブジェクトの内容も比較しますが、FloatとDoubleの比較では動作が異なります。
==と.equalsの違いは、FloatとDoubleの比較の場合です。.equalsは、浮動小数点演算に関するIEEE 754規格と一致しません。
2.9.4 「==」と「===」、「.equals」の違い
以下の2つのプリミティブ型Int変数を比較してみましょう。
val int1 = 10
val int2 = 10
println(int1 == int2) // true
println(int1.equals(int2)) // true
println(int1 === int2) // true
プリミティブデータ型は「===」演算子の場合にも値をチェックするだけなので、すべてがtrueを出力します。 それでは、以下のラッパークラスではどうなるでしょうか。
val first = Integer(10)
val second = Integer(10)
println(first == second) //true
println(first.equals(second)) //true
println(first === second) //false
上記の場合、「==」と「.equals」演算子は値のみを比較するのでtrueを出力しますが、「===」演算子は異なるオブジェクトの参照を比較するのでfalseを出力します。
それでは以下のクラスオブジェクトの場合はどうでしょう。
class Employee (val name: String)
val emp1 = Employee(“Suneet”)
val emp2 = Employee(“Suneet”)
println(emp1 == emp2) //false
println(emp1.equals(emp2)) //false
println(emp1 === emp2) //false
println(emp1.name == emp2.name) //true
println(emp1.name.equals(emp2.name)) //true
println(emp1.name === emp2.name) //true
この場合の理由は明らかで、Empoyeeはプリミティブデータ型またはラッパークラスではないため、3つすべてが参照を比較し、3つすべてのチェックでfalseが返されます。 しかし、文字列比較の場合、等しい文字列の内容をチェックするだけであれば、すべての場合にtrueを返します。
しかし、コンテンツの比較はデータクラスの場合にのみ機能します。 通常のクラスの場合、たとえ内容が同じであってもコンパイラは両方のオブジェクトを異なるオブジェクトと見なしますが、データクラス(※1)の場合はデータを比較し、内容が同一であればtrueを返します。
data class Employee (val name: String)
val emp1 = Employee("Suneet")
val emp2 = Employee("Suneet")
println(emp1 == emp2) //true
println(emp1.equals(emp2)) //true
println(emp1 === emp2) //false
println(emp1.name == emp2.name) //true
println(emp1.name.equals(emp2.name)) //true
println(emp1.name === emp2.name) //true
※1 データクラスを説明してから理解してください。今は記憶にとどめて置いてください。
float値を負のゼロと正のゼロで比較しましょう。
val negZero = -0.0f
val posZero = 0.0f
println(negZero == posZero) //true
println(negZero.equals(posZero)) //false
println(negZero === posZero) //true
単精度の浮動小数点と倍精度の浮動小数点の比較の場合、.equalsはIEEE 754浮動小数点演算の標準と一致しません。「-0.0」と「0.0」を比較した場合はfalseを返しますが、「==」と「===」はtrueを返します。詳細は、Floating Point Numbers Comparisonを参照してください。
2.10 パッケージとインポート
2.10.1 パッケージ
Kotlinではクラス、オブジェクト、インタフェース、または関数の完全修飾名(FQN)を指定するために使用され、名前空間でパッケージを区切ることができます。
パッケージを定義するには、Javaと同様に、packageキーワードを用います。
// 所属するパッケージ名を指定する
package com.kotlin.project
class Employee
fun name():String = "test"
上記の例では、クラスEmployeeは完全修飾名com.kotlin.project.Employeeを持ち、関数name()は完全修飾名com.kotlin.project.nameを持ちます。
2.10.2 インポート
宣言されたパッケージの外部でクラス、オブジェクト、インタフェース、および関数を使用できるようにするには、必要なクラス、オブジェクト、インタフェース、または関数をインポートする必要があります。
インポートもJavaと同様に、importキーワードを用います。Kotlinでは、インポートするクラスと関数を区別しません。importキーワードによりどのような種類の宣言でもインポートすることが可能です。
// これで com.kotlin.project.Employeeにアクセスできる
import com.kotlin.project.Employee
com.kotlin.projectパッケージのトップレベルにある関数getValue()をインポートする場合は、以下のように記述します。
import com.kotlin.project.getValue
kotlinはパッケージのトップレベルにクラスだけでなく関数を置くことができるため、import構文により関数を指定することができます。
同じパッケージからのインポートがたくさんある場合は、各インポートを個別に指定しないようにするために、*演算子を使用してパッケージ全体を一度にインポートできます。こちらもJavaと同様です。
import com.kotlin.project.*
2つの異なるパッケージがそれぞれ同じ名前を使用している場合は、asキーワードを使用して名前をエイリアスすることができます。 これは、java.io.Pathやcom.packt.myproject.Pathなど、複数のライブラリで共通の名前が使用されている場合に特に便利です。
import com.kotlin.project.Foo
import com.kotlin.otherproject.Foo as Foo2
fun doubleFoo() {
val foo1 = Foo()
val foo2 = Foo2()
}
2.10.3 パッケージ構造
Javaでは、パッケージ構造に一致したファイル構造とディレクトリ構造にクラスを配置します。一方、Kotlinでは、複数のクラスを各々のファイルに置くことが可能で、ファイル名も自由に決めることができます。Kotlinは、ディスク上のファイルのレイアウトにも制約がなく、ファイルを置くためのディレクトリ構成も自由に決めることができます。
出典:Kotlinイン・アクション
しかし、混乱を招くため、Javaのディレクトリ構造に従って、パッケージ通りに配置するのがよい方法です。
2.11 enum
今回は、Kotlinにおける列挙型enumについて説明します。enumは単なる定数の集まりではありません。プロパティを持つことができますし、インターフェイスを実装することもできます。
2.11.1 基本的なenum
Kotlinの列挙型定数enumの基本を見てみましょう。
以下は、色の種類を表す3つの定数を持つenumです。
enum class Color {
RED,
BLACK,
YELLOW
}
各enumの定数はオブジェクトです。定数はカンマで区切られます。
Kotlinのenumは、Javaの場合と同じように、コンストラクタを持つことができます。 定数は特定の値をコンストラクタに渡すことで初期化できます。
enum class Color(val color: String) {
RED("aka"),
BLACK("kuro"),
YELLOW("kiiro")
}
特定の色にアクセスするには以下のようにします。
val color = Color.RED.color
2.11.2 匿名クラスとしてのenum
enumを匿名クラスとして作成することで、特定の定数の動作を定義できます。 定数はenumで定義されている抽象関数をオーバーライド(※1)する必要があります。 たとえば、色の印象を取得する例は以下になります。
enum class Color {
RED {
override fun getColorImpression() = "fire"
},
BLACK {
override fun getColorImpression() = "evil"
},
YELLOW {
override fun getColorImpression() = "sun"
};
abstract fun getColorImpression(): String
}
上記により、次の例で無名定数クラスのオーバーライドされたメソッドを呼び出すことができます。
val colorImpression = Color.RED.getColorImpression()
※1 オーバライドを説明してから理解してください。今は記憶にとどめて置いてください。
2.11.3 インタフェースを実装するenum
今、さまざまな色のカラーコードを定義するColorCodeインターフェース(※2)があるとしましょう。
interface ColorCode {
fun getColorCode(): Int
}
それでは、enumがこのインターフェースをどのように実装できるかを見てみたいと思います。
enum class Color : ColorCode {
RED {
override fun getColorCode() = 0xff0000
},
BLACK {
override fun getColorCode() = 0x000000
},
YELLOW {
override fun getColorCode() = 0xffff00
}
}
色のカラーコードにアクセスするには、2.11.2と同じ方法で使用することができます。
val colorCode = Color.YELLOW.getColorCode()
※2 インタフェースを説明してから理解してください。今は記憶にとどめて置いてください。
2.11.4 enumの構文
whenを使ってenumの値を選択することができます。
fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "Youk"
Color.GREEN -> "Gave
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
それぞれの値をカンマで区切ることで、1つの分岐に複数の値をまとめることができます。
fun getWarmth(color Color) =
when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
enumの名前を直接使用するために、*演算子を使用して定数部分を明示的にインポートすることもできます。
import colors.Color.*
fun getWarmth(color Color) =
when(color) {
RED, ORANGE, YELLOW -> "warm"
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}
文字列名でenum定数を取得するには、staticなvalueOf()を使用します。
val color = Color.valueOf(name.toUpperCase())
すべての列挙型定数をループ処理するには、staticなvalues()関数を使用します。
for (color in Color.values()) {
println(color)
}
列挙型にstaticな関数を追加すると、コンパニオンオブジェクト(※3)を使用できます。
companion object {
fun getColorByName(name: String) = valueOf(name.toUpperCase())
}
上記により、この関数を呼び出すことができます。
val color = Color.getColorByName("aka")
※3 コンパニオンオブジェクトを説明してから理解してください。今は記憶にとどめて置いてください。
2.12 スマートキャスト(Smart cast)
今回はKotlinのスマートキャストの機能について説明します。Kotlinは、Flow-Sensitive Typeと呼ばれる型システムを持っています。制御フローによってある変数や式の型が変わる型システムの総称のことで、Kotlinではスマートキャスト(Smart cast)と呼んでいます。
2.12.1 スマートキャストの使用方法
通常、安全なキャストなしでnull許容型のStringにアクセスしようとすると、コンパイルエラーが発生します。
var message: String? = "Hello!"
print(message.length) // コンパイルエラー
上記の式を解くために、スマートキャスト(Smart cast)を使います。
var message: String? = "Hello!"
if(message != null) { // スマートキャスト
print(message.length) // プロパティにアクセス可能
}
}
ifの条件式でnullチェックをすることにより、その中ではnot-nullableな型として扱うことができます。
Javaでは、キャストを行う場合、instanceof演算子を使用し、ダウンキャストを行います。
// Java
Animal animal = new Cat();
if (animal instanceof Cat) {
Cat cat = (Cat)animal; // ダウンキャスト
cat.catMethod(); // `Cat` のメソッドを使う処理
}
一度instanceofでanimalがCatであると確認したにも関わらず、その後 Cat へのダウンキャストが必要になります。直前の if 文で animal が Cat であることを確認しているのに、明示的にキャストしなければならないのは冗長です。
Kotlinではスマートキャスト(Smart cast)により、animalは自動的にCatにキャストされます。
//Kotlin
val animal = Cat()
if (animal is Cat) { // Smart Cast
animal.catMethod() // `Cat` のメソッドを使う処理
}
when文を使うことで、更にスマートキャストを活用できます。
val list: List<Any> = listOf('a', 3, "abc", true)
for (e in list) {
val result: Any? = when(e) {
is Int -> e + 3
is String -> e.toUpperCase()
is Char -> e
is Boolean -> e.not()
else -> null
}
println(result)
}
2.13 例外処理
今回はKotlinの例外処理について説明します。Kotlin の例外処理はJava の例外処理とよく似ています。
2.13.1 Kotlinにおける例外
すべての例外はThrowableクラスから派生しています。 例外をスローするためにはthrowキーワードを使います。例えば、値が 0~100の範囲に収まっていない場合は、IllegalArgumentExceptionをスローする処理は以下のように記述できます。
fun validation(percentage :Int) {
if (percentage !in 0..100) {
throw IllegalArgumentException("A percentage must be between 0 and 100 [parameter: $percentage]")
}
}
Javaと違って、例外のインスタンスを生成するのに new キーワードは不要です。
Javaと違って、Kotlinでは throw構文は式となっているため、他の式の一部として利用する事ができます。以下は、if の条件に当てはまる場合はその値を返し、そうでなければ例外がスローされます
fun validation(value :Int) =
if (value in 0..100) {
value
} else {
throw IllegalArgumentException("A percentage must be between 0 and 100 [parameter: $value]")
}
2.13.2 try-catch-finally
Javaと同じく、例外を処理するためにcatch節, finally節 を伴ったtry構文を使用します。
fun readLineCount(reader: BufferedReader): Int { // この関数でスローされる可能性のある例外をここで明示する必要はない。
try {
val str = reader.readLine() // IOException の可能性がある
return str.toInt()
} catch (e: NumberFormatException) { // 例ギアの方は右側に記載する
return 0
} finally {
reader.close()
}
}
Javaと違いKotlinではthrows節が存在しません。また、Javaでは、IOException は検査例外(checked exception)であるため、明示的な記述が必要ですが、Kotlin では検査例外と非検査例外を区別していません。ある関数からスローされる例外を指定する必要はなく、使用する側では、どの例外についても、処理してもいいし、しなくてもかまいません。
2.13.3 式としてのtry
Kotlin のtryキーワードは、ifやwhenと同じように式として使用され、変数にその値を割り当てる事ができます。
fun printReadLineCount(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
0
}
println("read value = $number")
}
この関数を以下のように呼び出すと、
val reader = BufferedReader(StringReader("not a number")
printReadLineCount(reader))
結果は以下のようになります。
read value = 0
引き続きKotlinの文法を説明していきますので、お楽しみに。