LoginSignup
1
0

More than 3 years have passed since last update.

Kotlin Reference を見ていて腑に落ちなかった点を掘り下げてみる

Last updated at Posted at 2019-08-23

あらすじ

毎朝、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() のオーバライドが強制され、何が欲しいのかを明示する必要があります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0