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

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

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

2016年1月21日2020年1月14日

Kotlin 文法 - 基本の続き。

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

この記事は Qiita の記事 と同じものです。

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 になるのだろう。


© 2016-2020 K5