Kotlin

Kotlin文法 - クラス、継承、プロパティ

はじめに

Kotlin文法 - 基本の続き。

Kotlin ReferenceのClasses and Objects章Classes and Inheritance, Properties and Fieldsの大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。

objectについて

まだ説明されてないのに object についての記述が出てくるので簡単に。Kotlinでは簡単に無名クラスのオブジェクトやシングルトンを作る仕組みがある。詳細はオブジェクトを参照。

// 無名クラスのオブジェクトを作る
val ab = object : A(1), B {
  override val y = 15
}

// シングルトン
object DataProviderManager {
  fun registerDataProvider(provider: DataProvider) {
    // ...
  }

  val allDataProviders: Collection<DataProvider>
    get() = // ...
}

クラスと継承

クラス

コンストラクタ

// 中身がないクラスは括弧もいらずこう書ける
// コンストラクタを定義しない場合、自動で引数なしのコンストラクタが作られる
class Empty

// 1つのプライマリコンストラクタと複数のセカンダリコンストラクタを持てる。
// プライマリコンストラクタの宣言はクラスヘッダに書く
class Customer(name: String) {
    // 初期化処理はinitブロックの中に書く
    init {
        // プライマリコンストラクタの引数がこの中で使える
        logger.info("Customer initialized with value ${name}")
    }

    // プライマリコンストラクタの引数がプロパティの初期化でも使える
    val customerKey = name.toUpperCase()

    // セカンダリコンストラクタ
    constructor(firstName: String, lastName: String)
            // プライマリがある場合、セカンダリは必ずプライマリを呼び出す
            : this("$firstName $lastName") {
        logger.info("firstName = $firstName, lastName = $lastName")
    }
}

// 上のは省略記法でプライマリコンストラクタの正規の書き方はconstructorを使う。
class Person constructor(firstName: String) {
    // ...
}

// アノテーションとかアクセス指定をしたいならconstructor使う正規の書き方をする。
class Student public @Inject constructor(name: String) {
    // ... 
}
// プライマリコンストラクタなしでセカンダリだけってのもあり。
class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}
// コンストラクタなしだと自動で引数なしのコンストラクタが作られる。
// コンストラクタを公開したくないなら、こんな感じでアクセス指定する。
class DontCreateMe private constructor () {
}

プライマリコンストラクタの引数をそのままプロパティにしたい場合、次のように簡潔に書ける。

// コンストラクタの引数はval, varを指定できる。指定したものは自動でプロパティ化される。
class Person(val firstName: String, val lastName: String, var age: Int) {
  // ...
}

val person = Person("Yotsuba", "Koiwai", 5)
// コンストラクタでval, var指定したものはプロパティとしてアクセスできる。
println("name: ${person.lastName} ${person.firstName}, age: ${person.age}")
// 年齢はvarなので変更可能(苗字も将来変わるかもよってツッコミたい)
person.age++

NOTE: JVM上ではプライマリコンストラクタの引数が全てデフォルト値を持つ場合、デフォルト値を利用する引数なしの追加コンストラクタをコンパイラが自動生成する。このことでJacksonやJPAなどのパラメータなしコンストラクタを使ってオブジェクトを生成するライブラリが使いやすくなる。

インスタンス生成

コンストラクタは関数を呼ぶようにして使える。new キーワードはいらない。

val invoice = Invoice()

val customer = Customer("Joe Smith")

クラスメンバ

クラスは以下のものを持てる

  • コンストラクタと初期化ブロック
  • 関数
  • プロパティ
  • ネストされた内部クラス
  • オブジェクト宣言

継承

全てのクラスは Any を継承する。

class Example // 何も指定しなければ暗黙的にAnyを継承する

Anyjava.lang.Object ではない。equals(), hashCode(), toString() 以外のメソッドを持たない。詳細はjava interoperabilityを参照。

// open付けないと継承できない。デフォルトではJavaでのfinal classになる。
open class Base(p: Int)

// コロンで継承。プライマリコンストラクタがあるならその場で基底クラスを初期化する。
class Derived(p: Int) : Base(p)
// プライマリコンストラクタがない場合
class MyView : View {
    // 各セカンダリコンストラクタは親クラスのコンストラクタをsuperで呼び出す
    constructor(ctx: Context) : super(ctx) {
    }

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
    }
}

メンバのオーバーライド

open class Base {
  // デフォルトでfinal扱い。open付けないとオーバーライドできない。
  open fun v() {}
  fun nv() {}
}

class Derived() : Base() {
  // オーバーライドする側ではoverrideを付ける。付けないとコンパイラに文句言われる。
  override fun v() {}
}

open class AnotherDerived() : Base() {
  // オーバーライドするとデフォルトでopen扱い。
  // それ以上オーバーライドさせないならfinalを付ける。
  final override fun v() {}
}

継承したりオーバーライドしたりがデフォルトで禁止って、じゃあそうなってた場合、どうやってライブラリのハックすんのよって?

  • ハックなんてさせるべきじゃないってのがベストプラクティスだ。
  • 似たアプローチのC++, C#はうまく使われてる。
  • どうしてもハックしたいならJavaでやってKotlinから呼び出せば?Aspectフレームワークが使えるよ。

オーバーライドのルール

open class A {
  open fun f() { print("A") }
  fun a() { print("a") }
}

interface B {
  fun f() { print("B") } // インターフェースのメンバはデフォルトでopen
  fun b() { print("b") }
}

class C() : A(), B {
  // AもBも同じ f() を持ってて、どっちを継承するのかわからない。
  // こういう場合 f() のオーバーライドは必須。
  override fun f() {
    super<A>.f() // A.f()を呼ぶ
    super<B>.f() // B.f()を呼ぶ
  }
}

抽象クラス

クラスやそのメンバに abstract を付けると抽象クラスや抽象メソッドになる。abstract をつけると明示しなくても open 扱いになる。

open class Base {
  open fun f() {}
}

abstract class Derived : Base() {
  // abstractでないメソッドをabstractとしてオーバーライドすることもできる
  override abstract fun f()
}

staticメンバはない

JavaやC#と違ってKotlinのクラスにはstaticメンバはない。大抵は代わりにパッケージレベルの関数を使うのが推奨。

他にオブジェクト宣言やコンパニオンオブジェクトを使う方法があるけど、これについては後ほど。

シールドクラス

シールドクラスは継承を制限されたクラスで、enumクラスの拡張として利用できる。enumクラスの各定数は1つのインスタンスとしてしか存在できないが、シールドクラスのサブクラスは状態を含む複数のインスタンスを持てる。

// Exprはその内部クラスしか継承できない
sealed class Expr {
    class Const(val number: Double) : Expr()
    class Sum(val e1: Expr, val e2: Expr) : Expr()
    object NotANumber : Expr()
}

こんな感じでwhenと一緒に使うと便利1

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // 全てのケースをカバーしてるのでelseはいらない
    // Expr継承できるのは内部クラスだけなので、上で定義されてる分だけしかないことを
    // コンパイラは知っている。
}

プロパティとフィールド

プロパティ宣言

public class Address { 
  public var name: String = ...
  public var street: String = ...
  public var city: String = ...
  public var state: String? = ...
  public var zip: String = ...
}

fun copyAddress(address: Address): Address {
  val result = Address() // newはいらない
  result.name = address.name // ドットでsetter/getterにアクセスできる
  result.street = address.street
  // ...
  return result
}

getter/setter

プロパティの完全なシンタックスは以下。

var <propertyName>: <PropertyType> [= <property_initializer>]
  [<getter>]
  [<setter>]

property_initializerとgetter, setterはオプション。PropertyTypeも型推論できるならオプション。

var allByDefault: Int? // error: 明示的な初期化が必要。デフォルトgetter/setter利用。
var initialized = 1 // 型推論によりInt型、デフォルトのgetter/setter利用。

// カスタムgetter/setterを定義
var stringRepresentation: String
  get() = this.toString()
  // setterの引数は慣習でvalueだけど、好みで他の名前でもいい
  set(value) {
    setDataFromString(value) //文字列をパースして他のプロパティに代入
  }
var setterVisibility: String = "abc" // Nullableじゃないので初期化必須
  private set // setterはprivateでデフォルト実装を利用

var setterWithAnnotation: Any?
  @Inject set // setterにアノテーションを付ける。実装はデフォルトを利用。

不変値の場合は val を使う。setterは定義できない。

val simple: Int? // Int?型, デフォルトのgetter, コンストラクタでの初期化が必要
val inferredType = 1 // Int型、デフォルトのgetter

// カスタムgetterを定義
val isEmpty: Boolean
  get() = this.size == 0

アクセス制限やアノテーションを付ける。実装はデフォルトのものを利用できる。

// アクセス制限する
var setterVisibility: String = "abc"
  private set // デフォルトのsetterを使うけどアクセスはprivateに限定

// アノテーションをつける
var setterWithAnnotation: Any?
  @Inject set // 実装はデフォルトのものを利用

バッキングフィールド

Kotlinのクラスはフィールドを持てない(つまりプロパティで定義しているのはgetterやsetterとしてのメソッドであって、純粋な値なわけではないってことかと)。

でもカスタムgetter/setterでプロパティ値の実体(バッキングフィールド)にアクセスしたいときがある。この用途のためにKotlinは field でアクセスできる自動的なバッキングフィールドを提供している。

var counter = 0 // 初期値は直接バッキングフィールドに書き込まれる
  // カスタムsetterを用意
  set(value) {
    if (value >= 0)
      field = value // fieldを通してバッキングフィールドに値を格納する
  }

コンパイラはgetter/setterがfieldを使っているかデフォルト実装だった場合だけ、バッキングフィールドを生成する。

// これはバッキングフィールドを持たない
val isEmpty: Boolean
  get() = this.size == 0 // 他のプロパティの値を使って計算したものを返す

バッキングプロパティ

バッキングフィールドじゃうまくいかないってときは、Javaでやるみたいにprivateなプロパティをバッキングプロパティにすればいい。

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
  get() {
    if (_table == null)
      _table = HashMap() // Type parameters are inferred
    return _table ?: throw AssertionError("Set to null by another thread")
  }

コンパイル時定数

頭に const 付けるとコンパイル時定数になる。次を満たしている必要がある。

  • トップレベルか、object のメンバー
  • String かプリミティブ型で初期化される
  • カスタムgetterがない
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

// アノテーション内でも使える
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

遅延初期化プロパティ

プロパティは普通はコンストラクタで初期化されているべきだが、DIとかUnitテストとかのセットアップで後から初期化したいことがある。

public class MyTest {
    // lateinitを付けると遅延初期化プロパティになる。varにしか使えない。
    // カスタムgetter/setterは持てない。Nullableやプリミティブ型であってはいけない。
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        // ここで初期化する
        subject = TestSubject()
    }

    @Test fun test() {
       // Nullableじゃないのでnullの可能性を考えずに普通に使える
       // 初期化前にアクセスするとそれ用の例外が発生する
        subject.method()  // dereference directly
    }
}

プロパティのオーバーライド

メソッドのオーバーライドとルールは同じ。

// openつけないと継承できない
open class Person(name: String) {
    open val name: String // openつけないとoverrideできない
    init { this.name = name }
}

class UpperCaseNamePerson(name: String) : Person(name) {
    // プロパティをoverrideしてgetterを変更
    override val name: String
        get() { name.toUpperCase() }
}

デリゲートされたプロパティ

これは後ほど解説する。プロパティのgetter/setterの動作を他のクラスに移譲する仕組み。プロパティのgetter/setterでよくある動作を共通化できる。

import kotlin.properties.getValue

class User(val map: Map<String, Any?>) {
    val name: String by map    // mapに移譲
    val age: Int     by map    // mapに移譲
}

fun main(args: Array<String>) {
    val user = User(mapOf(
        "name" to "John Doe",
        "age"  to 25
    ))
    println(user.name) // Prints "John Doe"
    println(user.age)  // Prints 25
}

次の章へ

次はKotlin文法 - インターフェース、アクセス修飾子、拡張 へGO!


  1. Swiftのenumの方が良くできてるけど、Javaとの相互運用のためにenumクラスとは別に分けたのだと思う。つまりenumクラスはJavaのenumに、シールドクラスはJavaのclassになるのだろう。