Kotlinの構文を覚えるためにざっくり記事にまとめてみた
久々の記事の投稿になります。私は普段、フロントエンド領域の開発がメインで最近ではVueを用いた開発を行なっています。 サーバーサイドの開発も携わった経験はありますがNode.js(TypeScript)のみで、今回初めてKotlinを用いたサーバーサイド開発に携わることとなりました。 Kotlinの基本構文を覚えるため、忘れてしまったときにパッと見れるようにするためのメモとして今回記事にしてみました。 基本的にはドキュメント等を読み込み、実際に動かしてみて記事にしておりますが、私はKotlin初心者なので間違えている部分もあるかもしれません。悪しからず。 また仕様の細部までというよりは、ざっくりとした内容になっており、私の慣れ親しんだJavaScriptと比較して書くことがあります。
Kotlinとはどういう言語か
IntelliJ IDEAで有名なJetBrains社が開発している静的型付けのオブジェクト指向プログラミング言語で、Java仮想マシン(JVM)上で実行されます。 GoogleがAndroidアプリケーションの開発に推奨する言語として、2017年に公式に採用したそうです。 他特徴として、Javaと互換性がありJava資産を使えるところや、Javaより簡潔に記述できnull安全といった特徴があるようです。 Kotlin/JSを使用して、JavaScriptと互換性のあるコードを書くこともできるそうですが、フロントエンドはReact or VueとTypeScriptを用いて開発したいですね。 Kotlin/JSでフロント開発するかはさておき、そういったこともできる言語となっています。 では実際にKotlinの基本となる構文を見ていきましょう。
変数、定数の宣言
Kotlinにはvarとvalの2つの宣言があります。varはJavaScriptでいうところのletにあたりvalはconstにあたるイメージでしょうか。 varで宣言すると再代入できますがvalで宣言したものは再代入できません。 varとvalはパッと見、よく似ているので見間違えそうです。
var a = 0;
val b = 0;
a = 10;
b = 10; // エラー
データ型
Kotlinでは変数、定数に対しデータ型を指定することができます。 また全てのデータがオブジェクトでありAny型を継承しています。 下記の例ではそれぞれ指定した型に適切な値を代入しています。
val int: Int = 10
val long: Long = 1000000000L
val short: Short = 100
val byte: Byte = 50
val float: Float = 10.5f
val double: Double = 10.123456789
val char: Char = 'A'
val boolean: Boolean = true
val string: String = "S.A.G.A佐賀!"
Kotlinは初期値があればデータ型を推論でき省略が可能です。初期値がない場合は指定しないとエラーとなります。
var a = 0;
var b: Double
var c // エラー
他にも型はありますが、基本的な型は以下になります。
型 | 概要 | 例 |
---|---|---|
Int | 32bit整数型 | val age = 42 |
Long | 64bit長整数型 | val population = 123456789L |
Short | 16bit短整数型 | val shortNumber = 32767 |
Byte | 8bitバイト型 | val byteValue = 127 |
Float | 32bit浮動小数点数型 | val pi = 3.14f |
Double | 64bit二重浮動小数点数型 | val a = 3.14 |
Char | 16bit文字型 | val letter = 'a' |
Boolean | 真偽値型 | val isTrue = true |
String | 文字列型 | val greeting = "S.A.G.A佐賀!" |
Unit | 戻り値がない型 | fun printMessage(): Unit { println("Hello, S.A.G.A佐賀!") } |
Any | すべての型の親型 | val anything: Any = "I can be anything!" |
Array | 同じ型の要素を含む固定サイズのコレクション | val numbers = arrayOf(1, 2, 3) |
List | 読み取り専用リスト型 | val readOnlyList = listOf(1, 2, 3) |
MutableList | 変更可能なリスト型 | val mutableList = mutableListOf(1, 2, 3) |
Set | 一意の要素を保持するセット型 | val uniqueSet = setOf(1, 2, 3) |
MutableSet | 変更可能なセット型 | val mutableSet = mutableSetOf(1, 2, 3) |
Map | キーと値のペアを保持するマップ型 | val mapExample = mapOf("one" to 1, "two" to 2) |
MutableMap | 変更可能なマップ型 | val mutableMap = mutableMapOf("one" to 1, "two" to 2) |
Nullable | null許容型 | val nullableString: String? = null |
型変換
型同士に互換性があれば型変換をすることができますが、型変換は明示的に変換する必要があります。 以下の例の場合、共に型エラーとなります。
val a: Float = 10.0 // エラー
val b: Double = 10 // エラー
この場合、サフィックスを使うことで回避することができます。
val a: Float = 10.0f
val b: Double = 10.0
また**toデータ型()**メソッドを用いることでも型変換することができます。 以下の例の場合、変数aは型推論によってInt型と推論されています。 変数bにはaを代入していますが.toLong()によりLong型に変換しています。
var a = 10
var b = a.toLong()
文字列リテラル
Kotlinには2種類の文字列リテラルがあります。
- エスケープ文字を含む文字列(Escaped String)
- 改行などの任意の文字列を含む文字列(Raw String)
エスケープ文字を含む文字列は「"」で囲うのですが任意の文字列の方は「"""」で囲う必要があります。
val msg1 = "S.A.G.A\n佐賀!"
val msg2 = """S.A.G.A
佐賀!"""
println(msg1)
println(msg2)
ただし上記の例だと実行結果が違います。msg2の方は改行の間にインデントがあるため、インデントまで反映されてしまいます。 この場合はtrimMarginやtrimIndentを使用することで実行結果が同じになります。 trimMarginは「|」を付与することで「|」以前をトリミングしてくれます。 trimIndentは左側にあるスペースを、一番インデントの少ない行に合わせてトリミングしてくれます。
val msg1 = "S.A.G.A\n佐賀!"
val msg2 = """S.A.G.A
|佐賀!""".trimMargin();
val msg3 = """
S.A.G.A
佐賀!
""".trimIndent();
// 実行結果は同じ
println(msg1)
println(msg2)
println(msg3)
また文字列リテラルではJavaScriptのテンプレートリテラルのように変数を使用することができます。
val address = "佐賀"
val msg = "S.A.G.A${address}!"
println(msg) // S.A.G.A佐賀!
数値リテラル
一番よく用いられるのは10進数かと思いますが0xではじめる16進数や0bではじめる2進数なども使用することができます。 リテラル中のアンダースコアは無視されるため、バイト区切りやビットの区切りの可読性向上に使用することができます。 下記の場合だとどちらも値は同じになります。
val a = 1_000
val b = 1000
Long型とFloat型にはサフィックスが必要でLong型の場合は末尾に「L」を、Float型の場合は末尾に「f」が必要です。 fの場合は大文字小文字どちらも使用することができますがLの場合は大文字のみしか使用することができません。
val a = 1000000000L
val b = 1000000000l // エラー
val c = 10.5f
val d = 10.5F
配列
配列は、複数の要素をまとめて扱う際に使用します。 arrayOf()を用いて作成し、どんな型の値でも入れることができます。
val a = arrayOf(1, 2, "さん", null)
例えばintArrayOf()を用いればint型の配列を作ることができます。 intArrayOf()を用いず、arrayOf()でもint型の配列を作ることはできますがintArrayOf()で作った方が処理が早いそうです。
val a: IntArray = intArrayOf(1, 2, 3)
val b: Array<Int> = arrayOf(1, 2, 3)
他の型の配列は以下の表になります。string型はarrayOf()でやる必要があるみたいです。 nullの場合はarrayOfNulls()を用いてnullの配列を作ることができます。
型 | 型名 | メソッド |
---|---|---|
Int | IntArray | intArrayOf() |
Long | LongArray | longArrayOf() |
Short | ShotArray | shortArrayOf() |
Byte | ByteArray | byteArrayOf() |
Float | FloatArray | floatArrayOf() |
Double | DoubleArray | doubleArrayOf() |
Boolean | BooleanArray | booleanArrayOf() |
Nullable | Array<Int?>等 | arrayOfNulls() |
コレクション
コレクションには大きく分けてList, Set, Mapの3種類があり、それぞれ読み取り専用で要素の変更(追加、削除、更新)ができません。 List, Set, Mapの特徴は以下になります。
コレクション | 特徴 |
---|---|
List | 順序を持つコレクションで重複可。 |
Set | 順序を持たないコレクションで重複不可。 |
Map | キーと値を利用して要素を扱う |
そして各コレクションは以下のように生成することができます。
val list: List<String> = listOf("A", "B", "C") // [A, B, C]
val set: Set<String> = setOf("A", "B", "C", "B", "C") // [A, B, C]
val map: Map<String, Any> = mapOf("name" to "田中", "age" to 32) // {name=田中, age=32}
変更可能なコレクションを作成するには、mutableコレクション名Of関数を使用します。
val list: MutableList<String> = mutableListOf("A", "B", "C")
list[0] = "a"
println(list) // [a, B, C]
演算子
ここではどの言語にもある基本的な四則演算子、==や===などの比較演算子には触れず、特徴的な演算子の一部を抜粋し取り扱うこととします。
セーフコール演算子(?.)
セーフコール演算子はNullable型メンバへのアクセスに使用します。 JavaScriptのオプショナルチェーンに近い感じでしょうか。
val a: String? = "佐賀"
println(a?.length) // 2
val b: String? = null
println(b?.length) // null
エルビス演算子(?:)
nullだった場合に返す値を設定することができます。こちらもJavaScriptのNull合体演算子(Nullish coalescing)に似ていますね。
val a: String? = null
println(a?.length ?: 0) // 0
!!演算子
Nullable型を非Nullable型にするときに使用できます。 しかし値がnullだった場合にぬるぽが発生してしまいますので使い時は限られてきそうです。
val a: String? = "佐賀"
println(a!!.length) // 2
is演算子
is演算子は、ある変数が特定の型に属しているかどうかをチェックするために使用されます。 Kotlinにはスマートキャストと呼ばれる機能があります。 is演算子を使用して変数の型をチェックし、trueであることを検出するとその後のコードブロックで変数の型を自動的に推論します。
val a = "佐賀"
if (a is String) println(a) // 佐賀
範囲演算子(..)
1..10のように記述をすると1~10の範囲を表現することができます。 inを使用するとその範囲に入っているかチェックすることができます。 また1~10の場合は1と10も含みます。 !inとすると下記の例の場合、真偽値が逆になります。
val a = 5
val b: IntRange = 1..10
println(a in b) // true
println(a !in b) // false
範囲演算子の基本的な型は以下になります。
型 | 例 |
---|---|
IntRange | val a = 1..10 |
LongRange | val a = 1L..1000L |
CharRange | val a = 'a'..'z' |
if式
Kotlinの条件分岐にはif式があります。 JavaScriptしかやってこなかった身としてはif式には驚きました。 式ですので以下のように代入することができます。
fun main() {
val a = 10
val msg = if (a > 5) {
"5よりも大きい"
} else {
"5よりも小さい"
}
println(msg) // 5よりも大きい
}
ただし、式として用いた場合、何らかの値を返す必要があるのでelseは省略できないことに注意です。 また{ }の中に複数行記述した場合は最後の行を戻り値とみなします。
fun main() {
val a = 10
val msg = if (a > 5) {
"5よりも大きい"
}
println(msg) // エラー
}
また処理が1行の場合は{ }を省略できます。
fun main() {
val a = 10
val msg = if (a > 5) "5よりも大きい" else "5よりも小さい"
println(msg)
}
when式
when式は、式の値に応じて処理を分岐させることができます。 式ですのでifと同様に代入することができます。
fun main() {
when (1) {
1 -> println("1です")
2 -> println("2です")
else -> println("どちらでもない")
}
}
また引数を取らずに書くこともできます。
fun main() {
val a = 3
when {
a <= 5 -> println("5以下")
else -> println("5以上")
}
}
forループ
forループの基本的な記述は他の言語とあまり変わりません。
fun main() {
val array = arrayOf(1, 2, 3)
for (arr in array) println(arr)
val map = mapOf("name" to "田中", "age" to 32)
for ((key, value) in map) println("${key}:${value}")
}
配列でインデックスを取得したい場合はwithIndex()を使用します。
fun main() {
val array = arrayOf(1, 2, 3)
for ((index, value) in array.withIndex()) println("${index}:${value}")
}
範囲演算子でループすることもできます。
fun main() {
for (i in 1..5) println(i) // 1 2 3 4 5
}
上記の例の場合1~5まで出力されてしまいますがuntilを用いると1~4でループできます。
fun main() {
for (i in 1 until 5) println(i) // 1 2 3 4
}
基本的にループするときはインクリメントしますがdownToを用いてデクリメントすることもできます。
fun main() {
for (i in 5 downTo 1) println(i) // 5 4 3 2 1
}
ループさせると基本的には1ずつ増減しますがstepを用いることで増減を指定することもできます。
fun main() {
for (i in 1..5 step 2) println(i) // 1 3 5
}
ループを中断するにはbreak, スキップはcontinueを用います。
fun main() {
for (i in 1..10) {
if (i % 3 === 0) continue
println(i)
if (i == 8) break
} // 1 2 4 5 7 8
}
ループが入れ子になっていた場合、breakを用いても外側のループは終了しません。 外側のループごと中断したい場合はlabel構文を用います。
fun main() {
outer@ for (i in 1..3) {
for (j in 1..3) {
if (i * j > 5) break@outer
println("${i * j}")
}
println(i)
} // 1 2 3 1 2 4
}
whileループ
whileは条件式を満たす間ループします。
fun main() {
var a = 1;
while (a < 2) {
println(a)
a++
}
}
do whileは条件式の判定が処理の後になります。
fun main() {
var a = 1;
do {
println(a)
a++
} while (a < 2)
}
関数
Kotlinでは基本的に関数の引数と戻り値に型は必須です。
fun main() {
fun sum (num1: Int, num2: Int): Int {
return num1 + num2
}
println(sum(2, 5))
}
戻り値がUnit型の場合や推論できる場合は戻り値の型を省略できます。 また戻り値が単一の値や式の場合は省略して書くことができます。
fun main() {
fun sum (num1: Int, num2: Int) = num1 + num2
println(sum(2, 5))
}
引数が渡されなかったときのためにデフォルト値を設定することもできます。
fun main() {
fun sum (num1: Int = 1, num2: Int = 1) = num1 + num2
println(sum(2, 5)) // 7
println(sum()) // 2
}
名前付き引数
名前付き引数を使用すると、どの引数にどの値を渡すか指定することができます。 定義時の引数の順番に縛りがないメリットがありますが、名前付き引数は実引数の後に書く必要があります。
fun sum (num1: Int = 1, num2: Int = 1) = num1 + num2
fun main() {
println(sum(num2 = 2)) // 3
println(sum(num2 = 2, 3)) // エラー
}
またひとつの引数のみにデフォルト値が入っている場合、下記のように記述するとエラーになります。
fun sum (num1: Int = 1, num2: Int) = num1 + num2
fun main() {
println(sum(2)) // エラー
}
そのため省略できる引数は後の方に記述する方が望ましいかと思います。
fun sum (num1: Int, num2: Int = 2) = num1 + num2
fun main() {
println(sum(2)) // 4
}
可変長引数
可変長引数を使うにはvarargキーワードを用います。可変長引数は内部的には配列とみなされるのでforループを使用することが可能です。 スプレッド演算子使用して可変長引数に使用することもできます。
fun sum (vararg numArr: Int): Int {
var result = 0
for (num in numArr) result += num
return result
}
fun main() {
val arr = intArrayOf(1, 2, 3, 4)
println(sum(*arr)) // 10
// 可変長引数の一部として配列を渡すことも可能
println(sum(5, *arr, 2)) // 17
}
Kotlinでは関数から戻り値を複数にすることも可能です。 Pair(2値)とTriple(3値)を使用します。下記例はPairを使用します。
fun sumAndProduct (vararg numArr: Int): Pair<Int, Int> {
var result1 = 0
var result2 = 1
for (num in numArr) {
result1 += num
result2 *= num
}
return Pair(result1, result2)
}
fun main() {
val arr = intArrayOf(1, 2, 3, 4)
// 分解宣言
val (sum, product) = sumAndProduct(*arr)
println(sum) // 10
println(product) // 24
}
高階関数
引数として関数をとったり戻り値として関数を返したりする関数を高階関数といいます。 関数の引数に関数を渡す場合は::演算子を用います。
fun <T> invokeOnEach(list: Array<T>, action: (T) -> Unit) {
for (element in list) action(element)
}
fun main () {
val names = arrayOf("松田", "田中", "江頭")
fun greet(name: String) {
println("Hello, ${name}!")
// Hello, 松田!
// Hello, 田中!
// Hello, 江頭!
}
invokeOnEach(names, ::greet)
}
ラムダ式
匿名関数を使用することで、高階関数に渡すためだけの関数宣言をせずに済みます。 匿名関数の書き方としてラムダ式を使うのが主流のようです。 ラムダ式の基本的な書き方は以下です。
{ 引数 -> 関数 }
前項の例をラムダ式に置き換えると以下のようになります。 高階関数の最後の引数になっている場合、関数の外に出すことができます。
fun <T> invokeOnEach(list: Array<T>, action: (T) -> Unit) {
for (element in list) action(element)
}
fun main() {
val names = arrayOf("松田", "田中", "江頭")
// 高階関数の最後の引数がラムダ式なら、関数の外に出すことができる
invokeOnEach(names) { name -> println("Hello, ${name}!") }
}
他にもラムダ式の省略形式があります。 高階関数の唯一の引数がラムダ式の場合は高階関数の()を省略することができ、ラムダ式の引数が単一の場合、その引数を暗黙的な引数itで受け取ることができます。 今回の例では()は省略することができませんが引数をitで受け取ることはできます。
fun <T> invokeOnEach(list: Array<T>, action: (T) -> Unit) {
for (element in list) action(element)
}
fun main() {
val names = arrayOf("松田", "田中", "江頭")
// ラムダ式の引数が単一の場合、その引数を暗黙的な引数itで受け取ることができる
invokeOnEach(names) { println("Hello, ${it}!") }
}
またラムダ式の中でreturnを使用するとラムダ式ではなく直上の関数を抜けてしまいます。 下記のサンプルコードの場合、途中でreturnされてしまうため最後の行のprintln("終了")は実行されません。
fun main() {
val names = arrayOf("松田", "田中", "江頭")
names.forEach {
if (it === "田中") return
println("${it}")
}
// 下記は実行されない
println("終了")
} // 松田
ラムダ式のみを抜けるようにするにはラベル構文を用います。
fun main() {
val names = arrayOf("松田", "田中", "江頭")
names.forEach loop@ {
if (it === "田中") return@loop
println("${it}")
}
println("終了")
} // 松田 江頭 終了
ラムダ式を引数にとる高階関数の名前をラベルに指定することもできます。 その場合次のようになります。
fun main() {
val names = arrayOf("松田", "田中", "江頭")
names.forEach {
if (it === "田中") return@forEach
println("${it}")
}
println("終了")
} // 松田 江頭 終了
クラス・オブジェクト
Kotlinではクラスを定義するときは以下のコードのように記述します。 インスタンス化は*クラス名()*のように記述します。
fun main() {
class User {
val name = "田中"
val age = 30
fun intro() {
println("私は${name}です。年齢は${age}歳です。")
}
}
val user = User()
user.intro() // 私は田中です。年齢は30歳です。
}
アクセス修飾子
クラスの変数やメソッドのスコープを定義するときにアクセス修飾子を使用します。
fun main() {
class User {
val name = "田中"
val age = 30
internal fun intro() {
println("私は${name}です。年齢は${age}歳です。")
}
}
val user = User()
user.intro() // 私は田中です。年齢は30歳です。
}
またアクセス修飾子には以下があります。
修飾子 | 概要 |
---|---|
public | 全てのクラスからアクセス可 |
protected | 定義されたクラスとそのサブクラスからのみアクセス可 |
internal | 同じモジュールのクラスからのみアクセス可 |
private | 定義されたクラスからのみアクセス可 |
プロパティ
Kotlinにはプロパティという機能があります。プロパティはただ単にクラスが持つ変数ではなく、それ自体がアクセサーを持ちます。 アクセサーは値の取得(getter)/ 設定(setter)を行なうメソッドです。 イミュータブルな値(val)はgetterのみ暗黙的に宣言され、ミュータブルな値(var)はgetter/setterの両方が暗黙的に宣言されます。 カスタマイズする場合はプロパティ直下に明示的にgetter/setterを宣言する必要があります。 引数は慣習的にvalueとし、fieldは予約語になっていてバッキングフィールドと呼びます。 バッキングフィールドは、プロパティの値を格納するために裏側で自動生成されるフィールドです。
fun main() {
class User {
val name = "田中"
var age = 30
set(value) {
if (value < 18) {
println("パチンコは18歳から")
} else {
field = value
}
}
internal fun intro() {
println("私は${name}です。年齢は${age}歳です。")
}
}
val user = User()
user.age = 17 // パチンコは18歳から
}
コンストラクタ
Kotlinのクラスにはプライマリコンストラクタとセカンダリコンストラクタの2種類があります。 プライマリコンストラクタは、クラスにひとつだけ記述できるコンストラクタで構文は以下です。
fun main() {
// class クラス名 constructor(引数: 型) {}
class User constructor(name: String, age: Int) {
var name: String
var age: Int
// 初期化処理
init {
// 引数とプロパティ名が同じ場合はthisで参照
this.name = name
this.age = age
}
fun intro() {
println("私は${name}です。年齢は${age}歳です。")
}
}
val user = User("田中", 30)
user.intro() // 私は田中です。年齢は30歳です。
}
constructorはプライマリコンストラクタがアノテーションやアクセス修飾子を持たない場合に省略できます。 また引数にvalやvarを宣言することでプロパティの宣言と初期化を同時に行なえアクセス修飾子も付与することができます。 上記のサンプルコードに当てると以下のようになります。
fun main() {
class User (internal var name: String, var age: Int) {
fun intro() {
println("私は${name}です。年齢は${age}歳です。")
}
}
val user = User("田中", 30)
user.intro() // 私は田中です。年齢は30歳です。
}
セカンダリコンストラクタは、ひとつのクラスに2つ以上のコンストラクタをするときに使用します。 プライマリコンストラクタ以降はセカンダリコンストラクタとして定義されます。
fun main() {
class User (var name: String, var age: Int) {
// 引数がひとつだけ渡されたとき
constructor(name: String):this(name, 26)
// 引数が渡されなかったとき
constructor()this("松田")
fun intro() {
println("私は${name}です。年齢は${age}歳です。")
}
}
val user1 = User("田中", 30)
val user2 = User("田中")
val user3 = User()
user1.intro() // 私は田中です。年齢は30歳です。
user2.intro() // 私は田中です。年齢は26歳です。
user3.intro() // 私は松田です。年齢は26歳です。
}
上記サンプルのように引数によって分岐させる場合は初期値を渡すことでも実現が可能です。
fun main() {
class User (var name: String = "松田", var age: Int = 26) {
fun intro() {
println("私は${name}です。年齢は${age}歳です。")
}
}
val user1 = User("田中", 30)
val user2 = User("田中")
val user3 = User()
user1.intro() // 私は田中です。年齢は30歳です。
user2.intro() // 私は田中です。年齢は26歳です。
user3.intro() // 私は松田です。年齢は26歳です。
}
継承
継承は既存のクラスが持っているプロパティやメソッドを別のクラスに引き継いで新しいクラスを定義することです。 Kotlinは単一継承のプログラミング言語で多重継承(複数のクラスを継承した派生のクラス定義)はできません。 下記は継承とオーバーライドしたサンプルになります。 UserProfileの引数nameは初期化していませんが、継承元であるUserNameの引数で初期化しています。
fun main() {
// openを付与することで継承可能
open class UserName (var name: String) {
// メソッドも同様でopenを付与することでオーバーライド可能
open fun intro() {
println("ユーザー名:${name}")
}
}
// UserNameクラスを継承
class UserProfile(name: String, var age: Int): UserName(name) {
// オーバーライドするにはoverrideをつける
override fun intro() {
println("私は${name}です。年齢は${age}歳です。")
// 継承元のintro()を実行
super.intro()
}
}
val userName = UserName("田中")
val userProfile = UserProfile("田中", 30)
userName.intro() // ユーザー名:田中
userProfile.intro() // 私は田中です。年齢は30歳です。 ユーザー名:田中
}
上記でオーバーライドしましたが、このオーバーライドは任意になります。 オーバーライドを強制するには抽象クラスと抽象メソッドを用います。 ただし抽象メソッドには、具体的な実装を行なうことはできません。
fun main() {
// abstractを付与
abstract class User (var name: String) {
abstract fun intro()
}
class UserName(name: String): User(name) {
override fun intro() {
println("ユーザー名:${name}")
}
}
val userName = UserName("田中")
userName.intro() // ユーザー名:田中
}
インターフェイス
Kotlinは多重継承ができず、単一継承で問題が出るケースがあります。 例えばAとBという2つのクラスがあり、Aではmethod1とmethod2というメソッド、Bではmethod2とmethod3というメソッドの実装を強制したいとします。 この場合、method2が共通なのでこれらをまとめると以下のようなコードになるかと思います。
fun main() {
abstract class S {
abstract fun method1()
abstract fun method2()
abstract fun method3()
}
class A: S() {
override fun method1() {}
override fun method2() {}
override fun method3() {} // 不要
}
class B: S() {
override fun method1() {} // 不要
override fun method2() {}
override fun method3() {}
}
}
しかし単一継承のため、そのクラスに不要なメソッドも強制されてしまいます。 この問題を解決できるのがインターフェイスです。 インターフェイスは厳密には継承ではなく実装になるようです。 インターフェイスは下記サンプルのように記述しますが、インターフェイスのメソッドが独自の実装を持つ場合は、オーバーライドは強制されませんので注意です。 また下記サンプルでは実装のみを行なっていますが、継承と実装は同時に行なうこともできます。
interface C1 {
fun method1()
}
interface C2 {
fun method2()
}
interface C3 {
fun method3()
}
fun main() {
class A: C1, C2 {
override fun method1() {}
override fun method2() {}
}
class B: C2, C3 {
override fun method2() {}
override fun method3() {}
}
}
インターフェイスを用いることで多重継承のようなことができるため、実装するインターフェイスが同名のメソッドかつデフォルトの実装が異なるメソッドを持つ場合があります。 このように名前が衝突した場合、実装クラスでどのように呼び出すかを明示する必要があります。
interface MyInterface1 {
fun method() = println("method1")
}
interface MyInterface2 {
fun method() = println("method2")
}
fun main() {
class MyClass: MyInterface1, MyInterface2 {
override fun method() {
super<MyInterface1>.method()
super<MyInterface2>.method()
}
}
}
インターフェイスではプロパティの定義もすることができます。
interface MyInterface {
var str: String
fun method() = println("${str}")
}
fun main() {
class MyClass: MyInterface {
override var str = "ABC"
}
val myClass = MyClass()
myClass.method()
}
しかし定義できるが状態は持つことができないので以下の注意が必要です。
- abstractであるかアクセサを持つ必要がある
- バッキングフィールドを持つことができない
クラスの型変換
型変換(キャスト)は型同士が継承・実装の関係にある場合に可能です。 継承先のクラスのインスタンスを継承元のクラスのインスタンスのデータとして扱うことをアップキャストといい、その逆をダウンキャストといいます。 継承先のクラスは継承元のクラスの引き継いでいるのでアップキャストは基本的にいつでも可能です。 しかしダウンキャストは継承元のクラスを継承先のクラスのように扱うため、持っていない機能を呼び出してしまう可能性があるため注意が必要です。 またダウンキャストはis演算子を用いた型のチェックが必要です。下記サンプルの場合はAnimalがDog型として使用できるかどうかを確認しメソッドを呼び出しています。
fun main() {
open class Animal {}
class Dog: Animal() {
fun sound() = println("ワンッ")
}
val animal: Animal = Dog() // アップキャスト
if (animal is Dog) { // ダウンキャスト
// 以降はDog型として扱われる(スマートキャスト)
animal.sound()
}
}
ネストクラス
classの中にclassを定義することをネストクラスといいます。 入れ子になっている外側のクラスのことをアウタークラスといいます。 下記サンプルの場合、Outerクラスからしかアクセスできないようにするのが望ましい場合、ネストクラスを使用するようです。
class Outer {
private class Nest {
fun print() = println("ネスト")
}
fun run() {
val nest = Nest()
nest.print()
}
}
fun main() {
val outer = Outer()
outer.run() // ネスト
}
上記のサンプルの場合だとネストクラスからアウタークラスのメンバにはアクセスができません。 アクセスしたい場合はinner修飾子を付与してインナークラスとして定義します。
class Outer(val name: String = "アウター") {
inner class Nest(val name: String = "ネスト") {
fun print() {
println("${name}")
// ラベル構文を使用することでOuterクラスのnameへアクセス
println("${this@Outer.name}")
}
}
fun run() {
val nest = Nest()
nest.print()
}
}
fun main() {
val outer = Outer()
outer.run() // ネスト アウター
}
データクラス
処理を持たないデータだけを保持するクラスのことをデータクラスといいます。 データクラスは、classの前にdataを付与することで定義できます。 データクラスは、必要な記述がプライマリコンストラクタのみ行なえるため、本体ブロックは省略することができます。
fun main() {
data class User(var name: String, var age: Int)
}
データクラスを定義する際は以下の条件を満たす必要があります。
- プライマリコンストラクタの引数が1つ以上
- 引数は全てval/varを付与しプロパティの宣言にする
- abstract, open, sealed, innerにできない
またデータクラスにはいくつかのメソッドが用意されていますが、いずれもプライマリコンストラクタで定義されたプロパティのみを対象とします。 以下はよく使用されるメソッドのサンプルです。
fun main() {
data class User(var name: String, var age: Int)
val user1 = User("田中", 30)
val user2 = User("田中", 30)
// equalsメソッド(同値性のチェック)
println(user1 == user2) // true
// toStringメソッド(文字列化)
println(user1) // User(name=田中, age=30)
// comnponentNメソッド(分割代入)
val (name, age) = user1
println(name) // 田中
// copyメソッド(複製)
val user3 = user1.copy("江頭") // 特定のプロパティの値を変更し複製することも可能
println(user3) // User(name=江頭, age=30)
}
オブジェクト式
再利用を目的としないクラスを定義する際に使用する機能がオブジェクト式です。 基本的な書き方は以下になります。
object { クラス本体 }
// クラスの継承やインターフェイスの実装がある場合
object: 親クラスorインターフェイス { クラス本体 }
オブジェクト式はリスナクラスでよく使用するようです。 また下記のように抽象メソッドを1つしか持たないものをSAMインターフェイスといいます。
btn.setOnClickListener(object: View.OnClickListener {
override fun onClick(view: View) {
ボタンがクリックされた時の処理
}
})
オブジェクト式で実装するインターフェイスがSAMインターフェイスの場合、ラムダ式で置き換えることができます。 これをSAM変換といいます。
btn.setOnClickListener({ view: View -> ボタンがクリックされた時の処理 })
ラムダ式の場合、引数の型が明らかであるとき型を省略でき、引数を使用しないのであれば引数そのものも省略可能です。 唯一の引数がラムダ式の場合()の外に出すことができるため、上記のサンプルを更に省略すると以下になります。
btn.setOnClickListener { ボタンがクリックされた時の処理 }
オブジェクト宣言
ひとつのインスタンスしか持たないようなクラスを用意したいときに、オブジェクト宣言を使用することで実現できます。 オブジェクト宣言の基本的な書き方は以下で、クラス宣言のように記述することができます。
object User {
var name = "田中"
var age = 29
fun intro() {
println(" 私は${name}です。年齢は${age}歳です。")
}
}
fun main() {
User.age = 30
User.intro() // 私は田中です。年齢は30歳です。
}
既存クラスを継承することもでき、その場合は親クラスのコンストラクタに値を渡す必要があります。
コンパニオンオブジェクト
Staticメンバ(インスタンス化せずに使えるプロパティやメソッド)を利用するには、クラス内部でのオブジェクト宣言であるコンパニオンオブジェクトを利用します。 コンパニオンオブジェクト内で定義されたメソッドなどは「クラス名.メソッド()」のように呼び出すことができます。
class User {
object user1 {
val name = "松田"
}
companion object {
val name = "江頭"
}
}
fun main() {
val user1 = User.user1.name
val user2 = User.name
println(user1)
println(user2)
}
コンパイル時定数
コンパイル時定数は、プログラムのコンパイル時にその値が確定し、実行時に変更できない定数のことです。 const修飾子を付与することで定義できます。
const val MAX_COUNT = 12
const修飾子の特徴として以下が上げられます。
- 使用できるのはトップレベルの変数か、オブジェクトのメンバ変数に限られます。
- プリミティブ型およびString型の変数に限られます。
- コンパイル時にその値が確定している必要があるため、計算結果や関数の戻り値などは使用できません。
ジェネリック型
ジェネリック型はいわゆる型引数になります。 以下が例です。
class Sample<T> (var value: T) {
fun getProp(): T {
return value
}
}
fun main() {
val sample1 = Sample<String>("ABC")
val sample2 = Sample<Int>(10)
println(sample1.getProp())
println(sample2.getProp())
}
また渡せる型を制限することもできます。
open class Parent() {}
class Child(): Parent() {
val name = "田中"
}
// Parentまたはその派生クラスのみ
class Sample<T: Parent> (var value: T) {
fun getProp(): T {
return value
}
}
fun main() {
val sample1 = Sample<Child>(Child())
println(sample1.getProp().name)
val sample2 = Sample<Int>(10) // エラー
}
ジェネリック関数
ジェネリック関数は引数と戻り値の型を関数呼び出し時に決める関数のことです。 関数名の前に型引数を記述することで定義できます。
fun <T> returnSize(arr: Array<T>): T = arr[arr.size - 1]
fun main() {
val arr1 = arrayOf(1, 2, 3)
val arr2 = arrayOf('A', 'B', 'C')
println(returnArr<Int>(arr1)) // Int型
println(returnArr(arr2)) // Char型
}
拡張関数
継承を用いず既存クラスにメソッドを追加できる機能を拡張関数といいます。 拡張関数を使用するとopenでないクラスにもメソッドの追加ができるようになります。 基本的な書き方は以下になります。
// fun 拡張するクラス名.メソッド名(引数:型): 戻り値の型 { 処理 }
fun String.addName(name: String): String {
return this + "と${name}"
}
fun main() {
val name = "江頭"
println(name.addName("松田")) // 江頭と松田
}
いかがだったでしょうか。今回は急遽Kotlinをやることとなったので私自身が覚えるための記事になっています。 全てを網羅しているわけでなく最低限理解していれば何とか業務をこなせるのではないかと思ったところのみ掲載しています。 今後業務を進めていく中で新たに得た知見や、漏れている基本構文があったらこの記事を更新したり、新たに記事を投稿したりするつもりです。