Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

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

インターフェース

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 はトップレベルでは使えない。

example.kt
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もできるね。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした