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

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

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

2016年1月22日2020年1月14日

Kotlin 文法 - クラス、継承、プロパティの続き。

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

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

インターフェース

Kotlin の interface は Java8 のそれにとてもよく似ている。抽象メソッドだけでなく実装も持てる。ただし抽象クラスと違って状態を持てない。

interface MyInterface {
    fun bar()
    fun foo() {
      // デフォルト実装を持てる
    }
}

// classとobjectは1つまたは複数のinterfaceを実装できる
class Child : MyInterface {
   override fun bar() {
      // オーバーライドする内容
   }
}

インターフェースのプロパティ

プロパティは持てるが、abstract であるかまたはアクセサの実装を提供する必要がある。ようするにバッキングフィールドを持てない。なので初期値を与えたりアクセサ内で field にアクセスできない。

interface MyInterface {
    // バッキングフィールドはないので値は与えられない
    val property: Int // abstract

    // getterの実装を与えることはできる
    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        // 値はなくてもオーバーライドされる前提でプロパティにアクセスできる
        print(property)
    }
}

class Child : MyInterface {
    // abstractになってるpropetryをオーバーライド
    override val property: Int = 29
}

オーバーライド競合の解決

これ、クラスのとこでも説明したよね?

interface A {
  fun foo() { print("A") }
  fun bar()
}

interface B {
  fun foo() { print("B") }
  fun bar() { print("bar") }
}

class C : A {
  // Aのbar()をオーバーライド
  override fun bar() { print("bar") }

  // foo()の方はAのデフォルト実装を利用
}

class D : A, B {
  // AもBもfoo()の実装を持ってるんで、どっち使うかわからん。
  // なのでオーバーライド必須。
  override fun foo() {
    super<A>.foo()    // Aのfoo()を呼び出す
    super<B>.foo()    // Bのfoo()を呼び出す
  }

  // bar()の方はBしか実装がないから、オーバーライドしなけりゃそっち使うよ。
}

アクセス修飾子

public, protected, private, internal の4つがある。デフォルトは public で Java とは違う。

パッケージ

トップレベルでは以下のようになる。protected はトップレベルでは使えない。

package foo

// privateだとこのファイル内だけでしか見えない
private fun foo() {}

// publicなのでこのプロパティはどこからでも見える
public var bar: Int = 5
    private set    // setterはこのファイル内からしか見えない

// internalは同じモジュール内なら見える
internal val baz = 6

クラスとインターフェース

クラス内では

  • private はそのクラス内だけでしか見えない
  • protected はそのクラスとサブクラスからしか見えない
  • internal は同じモジュール内でそのクラスが見えているなら見える
  • public そのクラスが見えているなら見える

protected の意味は Java と違って C++や C#と一緒。また Java と違って内部クラスの private メンバをその外側のクラスから見ることはできない。

open class Outer {
    private val a = 1
    protected val b = 2
    internal val c = 3
    val d = 4  // 何も指定しなければpublic

    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a は見えない
    // b, c, d は見える
    // Nested と e は見える
}

class Unrelated(o: Outer) {
    // o.a, o.b は見えない
    // o.c は見える。 o.d も同じモジュールなので見える
    // Outer.Nested は見えない。なので Nested::e も見えない。
}

コンストラクタ

コンストラクタも何も書かなければデフォルトで public なので、前にも書いたけどアクセス制限したいならこう書く。

class C private constructor(a: Int) { ... }

モジュール

さっきから internal は同じモジュール内ならって言っているけど、モジュールってのはこういう意味ね1

  • IntelliJ IDEA モジュール
  • Maven または Gradle プロジェクト
  • Ant タスクの1つの発動でコンパイルされるファイルのセット

拡張

継承したりデコレータパターンのような仕組みを使わずに、クラスを拡張することができる2

拡張関数

// MutableList<Int>にswapを付け足す
fun MutableList<Int>.swap(index1: Int, index2: Int) {
  val tmp = this[index1]
  this[index1] = this[index2]
  this[index2] = tmp
}
val list = mutableListOf(1, 2, 3)
// こんな感じで使える
list.swap(0, 2) // swapメソッドの中のthisはlistを指す
// ジェネリクス関数としても追加できる
fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
  val tmp = this[index1]
  this[index1] = this[index2]
  this[index2] = tmp
}

ジェネリクスについてはまた後ほど。

拡張は静的に解決される

拡張は実際にクラスを書き換えるわけじゃない。クラスに新しいメソッドが追加されるわけじゃないんだ。拡張は静的に呼び出されるってことを声を大にして言いたい!!

open class C

class D: C() // Cを継承

// 拡張でCにfooメソッドを付け足す
fun C.foo() = "c"
// 拡張でDにfooメソッドを付け足す
fun D.foo() = "d"

// Cクラスのインスタンスを受け取る
fun printFoo(c: C) {
    // 拡張の場合、cはCクラスなのでC.foo()を呼び出すように静的に解決される。
    // オーバーライドされたメソッドのように動的に解決されるわけじゃない。
    println(c.foo())
}

// Cの子供のDを渡す。Dに拡張で足したD.foo()が呼び出される・・・よね?
// 残念でした〜!!C.foo()が呼びだされまーす!!
printFoo(D()) // "c"が表示される

もし同じ宣言のメンバがあった場合、メンバが必ず勝つ

class C {
    fun foo() { println("正社員") }
}

// Cクラスのメンバーを派遣で置き替えたいんだけど・・・
// ダメです!!常に正社員が優先です!!
fun C.foo() { println("派遣") }

val c = C()
c.foo()    // "正社員"って表示される。拡張で上書きはできない。

拡張相手が Nullable の場合

Nullable でも拡張できる。こうすると null に対しても呼び出せる。

// Nullableに拡張関数を追加
fun Any?.toString(): String {
    // thisはnullの可能性がある!!
    if (this == null) return "null"
    // nullチェックした後ならもうnullじゃないことがコンパイラには分かるから、
    // thisは勝手にnullでない参照に置き換わる。
    return toString() // これはthis.toString()と同じだけど、thisは既にAny?でなくAny
}

拡張プロパティ

プロパティも拡張できる。

// List<T>にlastIndexプロパティを付け足す
val <T> List<T>.lastIndex: Int
  get() = size - 1

でも関数と同様に実際にクラスにメンバを挿入するわけじゃないから、バッキングフィールドにはアクセスできない。なので拡張プロパティの初期化はできない。明示的に getter/setter を提供することでしか定義できない。

val Foo.bar = 1 // error: 拡張プロパティの初期化はできない!!

コンパニオンオブジェクトの拡張

いやそもそもコンパニオンオブジェクトってなんやねん?ってのは後で説明する。ここではそいつも拡張できるってことを覚えといて。

class MyClass {
  companion object { }  // "Companion"って呼んでね♥︎
}

// MyClassのコンパニオンオブジェクトにfoo()を付け足す
fun MyClass.Companion.foo() {
  // ...
}

// コンパニオンオブジェクトの他のメンバと同じように呼び出せる。
// 呼び出すときはCompanionって付けなくてもいい。貴方に影ながら付き添います。
MyClass.foo()

拡張のスコープ

大抵の場合はパッケージのトップレベルで定義するよね?

package foo.bar

fun Baz.goo() { ... }

他のパッケージから使うには import しないとダメよ。

package com.example.usage

import foo.bar.goo // "goo"って名前の全ての拡張をインポートする
                   // または
import foo.bar.*   // "foo.bar"の全てをインポートする
// Baz.gooだけを狙い撃ちでインポートってのはできないっぽい

fun usage(baz: Baz) {
  baz.goo()
)

モチベーション

なんでこんな機能用意したかっていうと、Java だとなんちゃら Utils ってクラスが一杯あるでしょ?例えば java.util.colletions って有名な奴使うと、

// Java
Collections.swap(
    list,
    Collections.binarySearch(list, Collections.max(otherList)),
    Collections.max(list)
    );

まぁ static インポートして、

// Java
swap(list, binarySearch(list, max(otherList)), max(list))

でも IDE の強力なサポートを受けられないよね?もしこう書けたらもっと良くない?

// Java(もしこうなら)
list.swap(list.binarySearch(otherList.max()), list.max())

でも List の中にありうる全ての実装を入れておくなんてしたくないよね?そんなとき拡張が役に立つよ!

次の章へ

次はKotlin 文法 - データクラス, ジェネリクスへ GO!


  1. ええと、パッケージってわけじゃないのね・・・。1コンパイル単位ってこと?よくわからん。Swift の Dynamic Framework 内ってのに似てる?

  2. C#や Gosu のようにって原文にある。これできる言語は他にも色々。Swift もできるね。


© 2016-2020 K5