ティーポットは珈琲を淹れられない

ソフトウェアエンジニアK5のブログ

Kotlin文法 - 基本

2016年1月20日2020年1月14日

Kotlin Referenceの日本語訳。複数の記事に分けていて、この記事は Basic 章に相当する。

一気にざっと文法全体を確認したい場合は「Kotlin 文法まとめ」を用意したのでそちらをどうぞ。

英語をそのまま訳したものはどうしても分かりにくくなりがち。ここでは公式リファレンスそのままにこだわってなくて、内容を理解した上で適宜表現を変えたり補足したり省略したりしている。

記事執筆時には全体を日本語訳したものはなかったのだけど、今は 公式リファレンスをそのまま日本語訳したもの がある。

この記事は Qiita に書いた記事 を転載したものです。

複数記事全体の目次

複数の記事に分けているのでここに全体像のリンクを貼っておく。

Nullable について

この章より後で説明される Nullable というのが説明に出てくる。Kotlin では変数に null は入れられないので、null か参照を入れられる特別な Nullable というものが用意されている。

Int の参照か null が入れられる NullableInt? と記述する。その他の構文は後ほど。構文も含めて Swift での Optional と同様のもの。

基本型

Kotlin では全てがプロパティやメソッドを持つオブジェクト。実際には速度的な観点からビルトイン型もあるが、それらも利用者からは普通のクラスのように見える。

数値

数値の扱いは Java に近いが全く同じではない。数値は暗黙の型変換が行われず、記法も少し違う場合がある。

ビット数
Double 64
Float 32
Long 64
Int 32
Short 16
Byte 8

数値定数

  • 10進数: 123

    • Long なら後ろに L をつける: 123L
  • 16進数: 0x0F
  • 2進数: 0b00001011

(8進数はサポートされていない)

  • デフォルトでは Double: 123.5, 123.5e10
  • Float は後ろに f か F をつける: 123.5f

表現

Int? などのNullableやジェネリクスでは、数値は boxing される。

val a: Int = 10000 // これはJVMのプリミティブ型として格納される
print(a === a) // 'true'と表示される
val boxedA: Int? = a    // boxingされる
val anotherBoxedA: Int? = a // boxingされる
print(boxedA === anotherBoxedA) // !!!'false'と表示される!!!
// 内部の値の等価性は保持される
print(boxedA == anotherBoxedA) // 'true'と表示される

(注: ===は参照の等価性, ==は構造の等価性の比較。つまり==は Java でいう equals メソッドによる比較。)

明示的な型変換

小さい型は大きい型のサブ型ってわけじゃない。もしそうならこんな感じで問題が発生する。

// もし〜だったら的な仮想コード。コンパイルできない。
val a: Int? = 1 // Int (java.lang.Integer)にBoxingされる
val b: Long? = a // 暗黙変換で Long (java.lang.Long)にBoxingされる
print(a == b) // ちょwwwこいつ"false"とか言うwww
// Longのequals()がもう一方の型もLongかチェックするからなんですね〜

そんなわけで小さい型は大きい方に暗黙的に変換されない

// 1はIntだけど変数型がByteなのでコンパイル時にByte型の数値として扱われる
val b: Byte = 1 // OK, 数値は静的にチェックされる
// Byte型の値をInt型に格納しても暗黙型変換はされない
val i: Int = b // ERROR
val i: Int = b.toInt() // OK: 明示的に型変換

全ての数値型は次の型変換メソッドを持っている。

  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
  • toFloat(): Float
  • toDouble(): Double
  • toChar(): Char

暗黙の型変換がないことは型推論と算術演算子のオーバーロードによりほとんど気にならない。

// a + b では a.plus(b) が呼ばれる。LongがIntを受け取るplusメソッドを用意している。
val l = 1L + 3 // Long + Int => Long

演算子

数値に対して標準的な算術演算子が利用できる。演算子は実際には適切なクラスのメンバが呼び出される2。詳細は演算子オーバーロードの章で説明する。

ビット演算には特別な記号は割り当てられていないが、次のように挿入形式で使える関数がある。

// shlは算術左シフトを行う
val x = (1 shl 2) and 0x000FF000

Int と Long には以下のビット演算がある。

  • shl(bits) – 符号付左シフト (Java の <<)
  • shr(bits) – 符号付右シフト (Java の >>)
  • ushr(bits) – 符号なし右シフト (Java の >>>)
  • and(bits) – ビット AND
  • or(bits) – ビット OR
  • xor(bits) – ビット XOR
  • inv() – ビット反転

文字

文字型は Char、もうわかってると思うけど暗黙の型変換はない。

fun check(c: Char) {
  if (c == 1) { // ERROR: 暗黙の型変換
    // ...
  }
}

リテラルはシングルクォートで囲む ‘1’, ‘\n’, ‘\uFF00’ みたいに。明示的に Int 型に変換できる。

fun decimalDigitValue(c: Char): Int {
  if (c !in '0'..'9')
    throw IllegalArgumentException("Out of range")
  return c.toInt() - '0'.toInt() // 明示的に数値に変換
}

数値と同様に Nullable では Boxing される。

真偽値

Booleantruefalse を取る。Nullable では Boxing される。3つの組み込み演算子 _ ||, && , !_ がある。

配列

Array クラスで表される。get/set メソッドがあり([]でアクセスできる)、size を持ち、他に幾つかの便利メソッドを持つ。

class Array<T> private constructor() {
  val size: Int
  fun get(index: Int): T
  fun set(index: Int, value: T): Unit

  fun iterator(): Iterator<T>
  // ...
}
// [1, 2, 3]を作る
val list = arrayOf(1, 2, 3)
// nullで埋められたサイズ3の配列を作る
var arr: Array<Int?> = arrayOfNulls(3)
// Array<String>型で値が ["0", "1", "4", "9", "16"]の配列を作る
val asc = Array(5, { i -> (i * i).toString() })

注: Java と違って Array<String> 型を Array<Any> 型に代入はできない3。これは実行時エラーの発生を防いでくれる。(ただし Array<out Any> を利用できる。Type Projection の章を参照。)

Boxing のオーバーヘッドを生じないプリミティブ型の配列を表す特別なクラスを用意している。ByteArray, ShortArray, IntArray など。これらは Array を継承してはいないが、同じプロパティとメソッドを持っている。これらも対応するファクトリ関数を持っている。

// プリミティブ型の配列である IntArray を用いる
val x: IntArray = intArrayOf(1, 2, 3)  // 型を明示したが、推論してくれるので省略可能
x[0] = x[1] + x[2]

文字列

文字列は String 型で表され、immutable(中身が変更不可)である。構成要素は文字であり、s[i]という形でインデックスを指定してアクセスできる。for ループでイテレートできる。

for (c in str) {
  println(c)
}

文字列リテラル

// Javaの文字列と同じ感じのやつ
val s = "Hello, world!\n"

// トリプルクォートを使うとスケープが効かない生文字列になる
val text = """
  for (c in "foo")
    print(c)
"""

文字列テンプレート

$を使って文字列の中に変数の値や計算結果を埋め込める。

val i = 10
val s = "i = $i" // i = 10

val s = "abc"
val str = "$s.length is ${s.length}" // abc.length is 3

// $そのものを表現するには
val price = "${'$'}9.99" // $9.99

パッケージ

// 所属するパッケージ名を指定
package foo.bar

// フルネームはfoo.bar.baz
fun baz() {}

// フルネームはfoo.bar.Goo
class Goo {}

// ...

インポート

import foo.Bar // これでBarにアクセスできる
import foo.* // fooの下にあるものが全てアクセスできる
import foo.Bar // Barにアクセスできる
import bar.Bar as bBar // bBarは'bar.Bar'を表す

クラス以外にも以下を import できる。

  • トップレベルの関数やプロパティ
  • object 宣言の中の関数やプロパティ
  • enum 定数

Java と違って特別に”import static”というのはない。通常の import でいける。

制御構文

if

Kotlin では if は式であり戻り値を返せる。なので三項演算子は必要ないので存在しない。

// 伝統的な使い方
var max = a
if (a < b)
  max = b

// else付き
var max: Int
if (a > b)
  max = a
else
  max = b

// ifが式であることを利用した三項演算子的使い方
val max = if (a > b) a else b

分岐はブロックにできる。最後の行の式の評価結果がブロックの値になる。

val max = if (a > b) {
    print("Choose a")
    a
  }
  else {
    print("Choose b")
    b
  }

if の結果を利用するなら else 節が必要になる。

when

when は C 系言語でいう switch の置き換え。

when (x) {
  1 -> print("x == 1")
  2 -> print("x == 2")
  // どれにも当てはまらないならelseが実行される
  else -> { // ブロックも使える
    print("x is neither 1 nor 2")
  }
}

when も式として値を返せる。その場合 else 節は必須で、全ての分岐が値を返す必要がある。ブロックが複数行なら if と同様に最後に評価された値を返す。

when (x) {
  // 0と1で同じ処理なら、コンマで結合できる
  0, 1 -> print("x == 0 or x == 1")
  else -> print("otherwise")
}
when (x) {
  // 定数でなくてもマッチさせられる
  parseInt(s) -> print("s encodes x")
  else -> print("s does not encode x")
}
when (x) {
  // 範囲に含まれるか
  in 1..10 -> print("x is in the range")
  // 配列などのコレクションに含まれるか
  in validNumbers -> print("x is valid")
  // 否定も使える。範囲に含まれなければ実行される。
  !in 10..20 -> print("x is outside the range")
  else -> print("none of the above")
}
val hasPrefix = when(x) {
  // 型チェックもできる。チェック後は自動的にキャストされる。
  is String -> x.startsWith("prefix") // xはStringとして扱える
  else -> false
}

when に引数を与えないと、if の代わりに利用できる。各条件は単純に真偽値判定される。

when {
  x.isOdd() -> print("x is odd")
  x.isEven() -> print("x is even")
  else -> print("x is funny")
}

for

// まぁこれは説明いらないよね
for (item in collection)
  print(item)

// もちろんボディはブロックにできる
for (item: Int in ints) {
  // ...
}

for はイテレータを提供するものなら何でもイテレートできる。すなわち、

  • メンバーまたは拡張として iterator() を持ち、その戻り値の型が
  • メンバーまたは拡張として next() を持ち、かつ
  • メンバーまたは拡張として Boolean を返す hasNext() を持つ

これら3つ全ての関数は operator としてマークされている必要がある。

インデックス付きにしたいなら

// 最適化によってindicesは実際にはオブジェクトを生成しないのでパフォーマンス劣化はない
for (i in array.indices)
  print(array[i])

// withIndex()でインデックスと値のペアで回すこともできる
for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

while

while, do-while は普通。

while (x > 0) {
  x--
}

do {
  val y = retrieveData()
} while (y != null) // ここでyが見える!

リターンとジャンプ

3つのジャンプ命令がある。

  • return : デフォルトでは直近の(無名を含む)関数を抜ける。注:ラムダは違う。
  • break : 直近のループを抜ける
  • continue : 直近のループの次のステップに進む

break, continue とラベル

// ラベルを使って直近でなく指定ラベルのループを抜ける
loop@ for (i in 1..100) {
  for (j in 1..100) {
    if (...)
      break@loop
  }
}

return とラベル

return は直近の関数を抜ける。ラムダじゃない。

fun foo() {
  ints.forEach {
    if (it == 0) return // これはforEachでなくfooを抜ける
    print(it)
  }
}

もしラムダを抜けたいならラベルを使う。

fun foo() {
  ints.forEach lit@ {
    if (it == 0) return@lit
    print(it)
  }
}

この程度でラベルを定義する必要はなく、暗黙のラベル(ラムダが渡される関数の名前)を使った方が便利。

fun foo() {
  ints.forEach {
    if (it == 0) return@forEach // foo関数ではなくforEachに渡したラムダを抜ける(処理1回分)
    print(it)
  }
}

またはラムダじゃなく無名関数を渡す。

fun foo() {
  ints.forEach(fun(value: Int) {
    if (value == 0) return // foo関数ではなくforEachに渡した無名関数を抜ける(処理1回分)
    print(value)
  })
}

ラベルを使いつつ値を返したいならこう書く。

return@a 1

次の章へ

次は Kotlin 文法 - クラス、継承、プロパティ へ GO!


  1. コンパイラが最適化のために対応する命令に書き下すことはある。

  2. 元の文章では invariant と記載されている。これは共変性と反変性の説明によるところの invariant ということ。


© 2016-2020 K5