あらすじ
毎朝、Kotlin Referenceの読み合わせをする勉強会をやっています。
現在、クラスと継承のところまで読み合わせをしたのですが、いくつか腑に落ちなかった点がありました。
クラスと継承 - Kotlin Programming Language
そこで、腑に落ちなかった以下の点について掘り下げてみました。
- プロパティ
- インターフェース
といっても、上記はKotlin Referenceで説明されていましたので、Referenceの内容も交えて少し掘り下げてみました。
プロパティ
基本的には以下ページの内容を引用しています。
プロパティとフィールド - Kotlin Programming Language
プロパティの宣言
Kotlinのクラスは、プロパティを持つことができます。
これらは、 var キーワードを使用して、ミュータブル(可変)として宣言することもでき、 val キーワードを使用するとイミュータブル(読み取り専用)にすることもできます。
public class Address {
public var name: String = ...
public var street: String = ...
public var city: String = ...
public var state: String? = ...
public var zip: String = ...
}
プロパティを使うにはJavaでのフィールドでやるように、ただ単純に名前で参照するだけで良いです:
fun copyAddress(address: Address): Address {
val result = Address() // 'new' キーワードは Kotlin にありません
result.name = address.name // アクセサが呼ばれる
result.street = address.street
// ...
return result
}
ゲッターとセッター
プロパティを宣言するための完全な構文は次のとおりです。
var <propertyName>: <PropertyType> [= <property_initializer>]
[<getter>]
[<setter>]
イニシャライザ、ゲッターとセッターは必須ではありません。
イニシャライザか基本クラスのメンバーからオーバライドされることが推測される場合は、プロパティの型も必須ではありません。
例:
var allByDefault: Int? // エラー:明示的なイニシャライザが必要、デフォルトのゲッターとセッターは暗黙
var initialized = 1 // これは Int 型を持ち、ゲッターとセッターも持つ
読み取り専用のプロパティ宣言の完全な構文は、ミュータブルのものと比べて2点異なります。
var の代わりに val で始まるのと、セッターを認めないことでです:
val simple: Int? // Int 型を持ち、デフォルトゲッターを持つ。コンストラクタ内で初期化が必要
val inferredType = 1 // Int 型を持ち、デフォルトゲッターを持つ
カスタムアクセサは普通の関数ととても似ていて、プロパティの中に宣言することができます。
ここでは、カスタムゲッターの例を示します:
val isEmpty: Boolean
get() = this.size == 0
カスタムセッターは次のようになります:
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 文字列をパースして他のプロパティへ値を代入する
}
慣例により、セッターの引数名は value ですが、別の名前が良いならそちらを選択することもできます。
アクセサの可視性を変更したり、アノテーションを付ける必要がありますが、デフォルトの実装を変更する必要がない場合は、その本体を定義せずにアクセサを定義することができます:
var setterVisibility: String = "abc"
private set // セッターはプライベートでデフォルトの実装を持つ
var setterWithAnnotation: Any? = null
@Inject set // セッターに Inject でアノテーションを付ける
バッキングフィールド (Backing Fields)
Kotlinのクラスは、フィールドを持つことができません。
しかし、カスタムアクセサを使用するときにバッキングフィールドが必要になることがあります。
この目的のために、Kotlinは自動バッキングフィールドを提供します。
これにより、 field 識別子を使用してアクセスすることができます。
var counter = 0 // イニシャライザの value はバッキングフィールドへ直に書き込まれる
set(value) {
if (value >= 0)
field = value
}
バッキングフィールドとは、フィールドの実体のことです。
KotlinとJavaのコードを比較するとわかりやすいかと思います。
// Kotlin
class Human {
val age = 20
get() {
println("Age is: $field")
return field
}
}
// Java
public final class Human {
private final int age = 20;
public final int getAge() {
String var1 = "Age is: " + this.age;
System.out.println(var1);
return this.age;
}
}
引用:https://android.benigumo.com/20190517/backing-fields-properties-in-kotlin/
バッキングプロパティ
「暗黙のバッキングフィールド」にそぐわないことをやりたい場合には、 バッキングプロパティ (backing property) を持つように必ずフォールバックさせることもできます:
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null)
_table = HashMap() // 型パラメータが推論される
return _table ?: throw AssertionError("他スレッドによってnullをセットされた")
}
全ての点において、これはちょうどJavaと同じです。
なぜなら、privateプロパティへデフォルトゲッターとセッターでのアクセスが、関数呼び出しのオーバヘッドが無いように最適化されているためです。
まとめると、ゲッターやセッターの外でフィールドに直接アクセスしたいときなどに使います。(field 識別子に名前をつけるイメージ)
private なフィールドに"_"を付けて明示します。
こちらもKotlinとJavaのコードを比較するとわかりやすいかと思います。
// Kotlin
class Human {
private val _age: Int = 20
val age: Int
get() {
return _age
}
val printAge = {
println("Age is: $_age")
}
}
// Java
public final class Human {
private final int _age = 20;
@NotNull
private final Function0 printAge = (Function0)(new Function0() {
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
String var1 = "Age is: " + Human.this._age;
System.out.println(var1);
}
});
public final int getAge() {
return this._age;
}
@NotNull
public final Function0 getPrintAge() {
return this.printAge;
}
}
引用:https://android.benigumo.com/20190517/backing-fields-properties-in-kotlin/
プロパティのオーバーライド
メソッドのオーバーライドとルールは同じです。
オーバーライドするフィールドの可視性修飾子をopenにし、オーバーライド先のフィールドにoverrideを付けます。
open class A {
open var stringRepresentation: String = ""
get() = field // バッキングフィールド
set(value) {
field = value.toUpperCase()
}
}
class B : A() {
override var stringRepresentation: String = ""
get() = field.reversed()
set(value) {
field = value.toLowerCase()
}
}
val a = A()
a.stringRepresentation = "abcde" // ABCDE
val b = B()
b.stringRepresentation = "abcde" // edcba
ちなみに、オーバーライドする際に型を変更することはできません。
(たとえ型が親子関係であっても)
open class A {
open var stringRepresentation: CharSequence = ""
get() = field // バッキングフィールド
set(value) {
field = value
}
}
class B : A() {
// var-property type is 'String', which is not a type of overridden
override var stringRepresentation: String = ""
get() = field
set(value) {
field = value
}
}
インターフェース
基本的には以下ページの内容を引用しています。
インタフェース - Kotlin Programming Language
Kotlinでのインタフェースは、Java 8と非常によく似ています。
インタフェースは抽象メソッドの宣言と同様に、メソッドの実装を含めることができます。抽象クラスと違って、インタフェースは状態を持てません。
インタフェースはプロパティを持つことができますが、これらは abstract であること、またはアクセサの実装を提供することが必要です。
インタフェースは、 interface キーワードを使用して定義されます。
interface MyInterface {
fun bar()
fun foo() {
// 本体は任意
}
}
ちなみに、Java 8におけるインターフェースについては以下を参考にしてください。
多重継承の問題についても考察されています。
Java8のインタフェース実装から多重継承とMix-inを考える | ギークを目指して
→どうやら、Java 8とKotlinで多重継承の解決方法は同じのようです。
インタフェースの実装
クラスやオブジェクトは、1つまたは複数のインターフェイスを実装することができます:
class Child : MyInterface {
override fun bar() {
// 本体
}
}
インターフェイス内のプロパティ
インターフェイス内にプロパティを宣言することができます。
インタフェースで宣言されたプロパティは、 abstract にすることも、アクセサの実装を提供することもできます。
インタフェース内で宣言されたプロパティはバッキングフィールドを持つことはできず、それ故にインタフェース内で宣言されたアクセサはそれらを参照できません。
interface MyInterface {
val property: Int // abstract
val propertyWithImplementation: String
get() = "foo"
fun foo() {
print(property)
}
}
class Child : MyInterface {
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 {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}
}
インタフェース A と B は、両方とも関数 foo() と bar() を宣言しています。
両方とも foo() を実装していますが、 B のみが bar() を実装しています。
( bar() は A では abstract としてマークされていません。これは関数が本体を持たないときのインタフェースのデフォルトだからです。)
さて、もし具体クラス C を A から得れば、 bar() をオーバライドし、実装を提供しなければならないことは明らかです。
そしてもし D を A と B から得れば、 bar() をオーバライドする必要はありません。
なぜなら1つの実装を継承したからです。
しかし foo() の実装を2つ継承してしまったため、コンパイラはどっちを選んだら良いかわかりません。
したがって foo() のオーバライドが強制され、何が欲しいのかを明示する必要があります。