覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - コンストラクタ

今日も相変わらず 「Kotlinイン・アクション」 を読みながらの写経です。

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


Java のクラスは1つ以上のコンストラクを持ちます。
Kotlin も似ていますが プライマリ(primary)コンストラクタとセカンダリ(secondary)コンストラクタを区別している点が異なります。
また、初期化ブロック(initializer block)に追加の初期化用ロジックを記述する事ができます。


プライマリコンストラクタ

以下はKotlinのクラス宣言になります。

class Person(val fullName: String, val age: Int)

このクラスには波括弧なしで、丸括弧に宣言があるだけとなっています。
このような丸括弧で囲まれたコードブロックは、プライマリコンストラクタ と呼ばれます。

プライマリコンストラクタは以下の2つの役割を持ちます

  • コンストラクタの引数を指定する
  • 引数で初期化されるプロパティを定義する

当然ですがコンストラクラタにより以下のようにインスタンスを生成できます

val person = Person("Kotlin Taro", 20)

先ほどのプライマリコンストラクの内容をもっと明示的に記述すると以下のようになります

class Person constructor(_fullName: String, _age: Int) {
    val fullName: String
    val age: Int
    init {
        fullName = _fullName
        age = _age
    }
}

constructor というキーワードは、プライマリコンストラクタもしくはセカンダリコンストラクタの宣言のはじめに付けられます。
init は、初期化ブロック(initializer block)を実装するためのキーワードとなります。
初期化ブロックは、クラスが生成された時に実行される初期化コードを含んでいて、プライマリコンストラクタと一緒に利用されることが意図されています。
プライマリコンストラクタの構文では、初期化コードを含めることができないためです。


初期化ブロックを用いた例を上述しましたが、上記例であれば初期化ブロックに初期化用のコードを書く必要はありません。
fullName, age プロパティの宣言と組み合わせる事ができるためです。
プライマリコンストラクタに対してのアノテーションや可視性修飾子が付かないのであれば constructorキーワードを省略可能です。
これらの内容を適用すると以下のように書く事ができます。

class Person(_fullName: String, _age: Int) {
    val fullName = _fullName
    val age = _age
}

クラス本体で val キーワードでプロパティを宣言していますが、プロパティが対応するコンストラクタ引数で初期化されるのであれば、
コンストラクタの引数の前に val キーワードを付ける事で、コードをシンプルにする事が可能です。
それが以下になります

class Person(val fullName: String, val age: Int)

最初に書いたクラス宣言と同じ記述となりました。これがもっとも簡潔な構文になります。


コンストラクタ引数は、関数の引数と同じようにデフォルト値を設定する事が可能です。

// age プロパティにデフォルト値を設定した
class Person(val fullName: String, val age: Int = 10)
val person1 = Person("Kotlin Taro", 20)
println("${person1.fullName} is ${person1.age} years old.")    //  -> Kotlin Taro is 20 years old.

// デフォルト値が利用される
val person2 = Person("Kotlin Jiro")
println("${person2.fullName} is ${person2.age} years old.")    //  -> Kotlin Jiro is 10 years old.


ちなみに、コンストラクタ引数の全てがデフォルト値を持つ場合、コンパイラは引数を持たずに全てデフォルト値を使用するコンストラクタを追加で生成します。
こうすることで、引数なしコンストラクタでのインスタンス化を行うようなライブラリでKotlinを利用しやすくなります。

// コンストラクの引数の全てにデフォルト値を持つ
class Person(val fullName: String = "Unknown", val age: Int = 10)

fun main() {
    val person = Person()  //  デフォルトコンストラクタの実行
    println("${person.fullName} is ${person.age} years old.")    // -> Unknown is 10 years old.
}


クラスがスーパークラスを継承している場合、プライマリコンストラクタはスーパークラスも初期化する必要があります。
基底クラスのリストで、スーパークラスの参照に続けてそのコンストラクタ引数を与える事で、初期化できます。

open class Animal(val name: String)

class Cat(name: String) : Animal(name)


コンストラクタを宣言しないクラスでは、何も引数をとらないデフォルトコンストラクタが生成されます

open class Shape

// デフォルトコンストラクタが生成されている
val shape = Shape()

コンストラクタを宣言していないクラス継承し、なおかつコンストラクタを持たないクラスの場合は、
スーパークラスが引数を持たなかったとしても、スーパークラスのコンストラクタを明示的に呼び出す必要があります。

class Circle : Shape()

ちなみに、インターフェースを実装する場合は、インターフェースはコンストラクタを持たないため、
親の型のリスト内のインターフェース名の後ろに丸括弧は必要ありません。


あるクラスを他のコードからインスタンス化できないように保証するには、コンストラクタを private にするひつようがあります。
以下はプライマリコンストラクタを private にしたものです。

class SecreteClazz private constructor() {}

このクラスは、privateなコンストラクタしか持たないため、このコードの外側からはインスタンス化することはできません。


セカンダリコンストラクタ

Java に比べて、Kotlin では複数のコンストラクタを持つクラスは一般的ではありません。
なぜならば、Javaでオーバーロードによって複数のコンストラクタを必要とする状況は、Kotlin では引数のデフォルト値と名前付き引数によってカバーされるからです。


それでも、複数のコンストラクタが必要となる状況は残されていて、複数の異なる方法でクラスを初期化するコンストラクタを持つフレームワークのクラスを拡張するような状況です。
Javaで宣言された2つのコンストラクタを持つ View クラスを想像した場合に、Kotlinでは以下のように宣言されます

open class View {
    constructor(ctx: Context) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) {
        // initialize code
    }
}

このクラスには、プライマリコンストラクタの宣言はありません(クラス宣言のクラス名の後ろに括弧がないことがそれを意味しています)
セカンダリコンストラクタは、constructor キーワードを使って導入し、必要な数だけ宣言可能です。

このクラスを拡張する場合、同じコンストラクタを宣言可能です。
以下では super() キーワードを使って、対応するスーパークラスのコンストラクタを呼び出しています。

class MyView : View {
    constructor(ctx: Context) : super(ctx) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) : super(ctx, attr) {
        // initialize code
    }
}

Javaと同じく this() キーワードを利用することも可能です

class MyView : View {
    constructor(ctx: Context) : this(ctx, DEFAULT_ATTRIBUTE) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) : super(ctx, attr) {
        // initialize code
    }
}


クラスにプライマリコンストラクタがなければ、それぞれのセカンダリコンストラクタが基底クラスを初期化するか、
基底クラスを初期化するための別のコンストラクタを呼び出す必要があります。

Javaとの互換性が、セカンダリコンストラクタが登場する主なユースケースとなります。


まとめ

Java にはなかった、プライマリコンストラクタとセカンダリコンストラクタがKotlinには存在している事がわかりました。
主に使うのはプライマリコンストラクタなのだと思いますが、両方の使い分け等について把握しておく必要があるかなと感じました。

Kotlin - シールドクラス

明示していない場合も少なくないですが、現時点で私が書いているKotlinのエントリは、ほぼ 「Kotlinイン・アクション」 を読みながらの写経です。

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


式を表現する Expr インターフェース とそれを実装した数字を表現する Num クラス, 値の和を表す Sum クラスを定義し、
when 式によって、サブクラスごとの処理を分岐させて、計算結果を導き出す eval 関数を定義すると以下のようになります。

import java.lang.IllegalArgumentException

interface Expr

class Num(val value: Int) : Expr

class Sum(val left: Expr, val right: Expr) : Expr


fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> 
            throw IllegalArgumentException("Unknown expression")
    }


実行するコードは以下のようになります。

fun main() {
    // 10 + 10
    val answer1 = eval(Sum(Num(10), Num(10)))
    println(answer1)  // -> 20

    // 1 + 20 + 30
    val answer2 = eval(Sum(Num(1), Sum(Num(20), Num(30))))
    println(answer2)  // -> 51
}


when で分岐にマッチしなかった場合の処理を指定するための else分岐 を用意する必要があります。
Kotlinのコンパイラでは、 when構文を使って式を評価するときに、デフォルトの選択肢のチェックが行われます。

上記の例では、Num と Sum 以外の場合意味のある値を返却する事ができないため、例外をスローしています。

常にデフォルトの分岐が必要になるのは不便であり、かつ、新しい実装クラスを増やした場合にwhenの分岐にそれが加わっているかどうかはコンパイラはチェックしてくれません。

このような問題を解決する策として シールドクラス が存在しています。
スーパークラスに sealed修飾子を設定すると生成可能なサブクラスを制限する事ができます。

シールドクラスを継承したサブクラスとして宣言するには、シールドクラスでネストされたクラスか、同じファイル内で宣言されたクラスとしてのみです。

先ほどの例はシールドクラスを利用する事で以下のように記述できます

sealed class Expr

class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()


fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
    }

when 式内でシールドクラスの全てのサブクラスを処理するのであれば、デフォルトの分岐を追加する必要がありません。


シールドクラスを継承したサブクラスを追加した場合は、when 式の分岐の中にも追記して、全てのサブクラスを網羅する必要があります。
以下の例では、 Subtract というクラスを追加しています

sealed class Expr

class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
class Subtract(val left: Expr, val right: Expr) : Expr()  // 新しく増やしたサブクラス

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        is Subtract -> eval(e.left) - eval(e.right)  // この行を追加しないとコンパイルエラーになる
    }


まとめ

簡単な例しか取り扱っていないため、シールドクラスの便利さを表現しきれていませんが、
今回のエントリでいうと when 式での分岐において余計なデフォルト処理の記述が不要となり、
かつ、シールドクラスの全てのサブクラスに対するケースの網羅をコンパイラが強制してくれる事がわかりました。

Kotlin - 内部クラスとネストされたクラス

Java では クラスの中にクラスを宣言する事が可能です。
あるクラスの一部機能を切り出して、クラス化してその外側のクラスからしか利用しない といったケースで使う事があります。

例えば以下のように記述できます。

public class Sample {

    private final String name;

    public Sample(String name) {
        this.name = name;
    }

    private class InnerClazz {

        // 外側のクラスのインスタンス変数にアクセスできる
        // 明確に記述しなくても外側のクラスのインスタンスにアクセスできてしまう
        public void helloOuterName() {
            System.out.println(Sample.this.name);
        }
    }
}

しかし、この内部クラスとて宣言した InnerClazz は、エンクロージングクラス( = 内部クラスからみて外側にあるクラス)のインスタンスにアクセスできてしまいます。
上記のコード例からも private なメンバへアクセス可能です。

このような状況を意図しない場合には、基本的に内側のクラスに static 修飾子を付与してネストしたクラスという扱いにする事で、
エンクロージングクラスへの暗黙的な参照は無くなります。

以下コードは、コンパイルが通らなくなります。

public class Sample {

    private final String name;

    public Sample(String name) {
        this.name = name;
    }

    private static class InnerClazz {

        // コンパイルが通らない
        public void helloOuterName() {
            System.out.println(Sample.this.name);
        }
    }
}

コンパイルが通るようにするためには、例えば以下のようなコードにする必要があります。

public class Sample {

    private final String name;

    public Sample(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    private static class InnerClazz {

        private final Sample sample;

        // 明示的にインスタンスを渡す必要がある
        private InnerClazz(Sample sample) {
            this.sample = sample;
        }
        
        public void helloOuterName() {
            System.out.println(sample.getName());
        }
    }
}

一般的に、ほとんどのケースでクラスの中にクラスを宣言する場合、 static 修飾子を付与したネストクラスが必要とされるように感じます。


Kotlinの場合

Kotlin の場合は、明示的な修飾子を付与しない内部クラスは、Javaにおける static 修飾子が付与されたネストされたクラスと同等です。
そのため、暗黙的に内側のクラスは、外側のクラスへの参照を持っていません。

class KotlinSample(val name : String) {

    private class InnterClazz(val sample : KotlinSample) {
        fun helloOuterName() {
            println(sample.name)
        }
    }
}


逆に、内部クラスが外側のクラスへの参照を持った状態にするには、内部クラスに innrer 修飾子を付与する必要があります。
この内部クラスから外部クラスへアクセスするためには、 this@外部クラス名 という記述をする必要があります。

以下がサンプルコードになります。

class KotlinSample(val name : String) {

    private inner class InnterClazz() {
        fun helloOuterName() {
            println(this@KotlinSample.name)
        }
    }
}

JavaとKotlinのネストされたクラスと内部クラスの対応関係は以下の通りです

OutAクラスの内部で宣言されたInBクラス Javaでの宣言 Kotlinでの宣言
ネストされたクラス(外部クラスの参照を保持しない) static class InB class InB
内部クラス(外部クラスへの参照を保持する) class InB inner class InB


まとめ

Javaであるクラスの中に別のクラスを記述する場合、外側へのクラスの暗黙的な参照を必要とするケースはほとんどなく、
基本的に内側のクラスに static 修飾子をつけることになります。
Kotlin では、内側のクラスに何の修飾子もつけない状態が、これと同等になっているのは理にかなっているように感じました。

Kotlin - 可視性

可視性のデフォルトはpublic

Kotlin の可視性修飾子はJavaと似ていて、public, protected, preivate の修飾子 を持っています。
しかし、デフォルトの可視性(修飾子の指定を省略した場合の可視性)が、Kotlin では public になっています。

class Dog(val name : String)

public class Dog(val name : String)

は、いずれのクラスも public の可視性

また、Javaのデフォルトの可視性である パッケージプライベート はKotlin には存在していません。
Kotlin におけるパッケージというものが、可視性の制御には利用されず、名前空間にコードをまとめるためだけに使用されます。


internalによるカプセル化

Kotlin には、 internal というJavaには存在しない修飾子が提供されています。
これは "モジュール内で参照可能" という意味で、モジュール(module)とは、Kotlin のファイルが一体としてコンパイルされるまとまりのことです。
Intellij IDEAのモジュールやEclipseのプロジェクト、MavenやGradleのプロジェクトで一度にコンパイルされるファイルのまとまりがそれに該当します。

internal 可視性の利点は、モジュールの実装の詳細について本物のカプセル化を実現できる事です。

Javaでは、外部のコードが同じパッケージ名でクラスを定義すれば元のコードのパッケージプライベートな宣言を参照することができてしまいます。
これにより実装のカプセル化を簡単に破る事ができてしまいます。


Javaで例えば以下のようなインターフェースと内部実装のクラスを作成したとします。

package net.yyuki.sandbox.scope;

//  インターフェース
public interface Printable {
    void print();
}
package net.yyuki.sandbox.scope.internal;

import net.yyuki.sandbox.scope.Printable;

//  実装クラスはパッケージプライベート
final class SecretePrinter implements Printable {

    @Override
    public void print() {
        System.out.println("Secrete Logic print.");
    }
}

インターフェースのパッケージは、net.yyuki.sandbox.scope, 実装クラスのパッケージは net.yyuki.sandbox.scope.internal としています。
そのため、実装クラスが属するパッケージクラスに直接アクセするようなコードは書けません

package net.yyuki.sandbox.client;

import net.yyuki.sandbox.scope.Printable;

public class Main {
    public static void main(String[] args) {
        Printable p = null;
        Printable printer = new SecretePrinter();  //  可視性の問題でこんなコードは書けない
    }
}

これで可視性の制御が一見できているように見えますが、
上記のPrintableとSecretePrinterをまとめた jar を作成して公開した場合に、利用者側が以下のように net.yyuki.sandbox.scope.internal という実装クラスを隠蔽するためのパッケージと同名のパッケージを作成してクラスを配置してしまうと、
いとも簡単にパッケージプライベートの可視性のクラスにアクセスできてしまいます。

// 実装クラスと同名のパッケージを宣言している
package net.yyuki.sandbox.scope.internal;

import net.yyuki.sandbox.scope.Printable;

public class JavaMain {
    public static void main(String[] args) {
        Printable printer = new SecretePrinter();  // 何の問題なもなくパッケージプライベートの実装にアクセスできる
        printer.print();
    }
}


このように外部からカプセル化を破られるのを防ぐために internal という可視性が導入されたものと思われます。
Java でも OSGiとかをやった経験があるとこの辺のモジュラリティの話になって、internal といった可視性の重要性が理解しやすいように感じます。


Kotlinでinternalで宣言したクラスに拡張関数を追加しようとした際に、publicな関数として公開されてしまうようなコードはコンパイルエラーとなります。

internal class Cat(val name: String, val age: Int)

// これはコンパイルエラー
fun Cat.walk() {
    println("cat walk")
}

//  これはOK
internal fun Cat.dash() {
    println("cat dash")
}


また、他にもJavaと違いKotlinでは、クラス、関数、プロパティを含むトップレベルの宣言で private を指定できます。
この宣言をした場合、それらが宣言されているファイルの中からのみ参照可能となります。

Javaでは以下のようにトップレベルのクラスで private の修飾子をつけることは不可能です

package net.yyuki.sandbox.scope;

//  コンパイルエラー
private class Cat {
}


Kotlinだと以下のような記述が可能です

package net.yyuki.sandbox.scope

private class Cat {
    fun print() {
        println("nyan nyan")
    }
}

fun main() {
    val cat = Cat()
    cat.print()
}

以下は、Kotlinの可視性修飾子に関する一覧となります

可視性修飾子 クラスメンバ トップレベルで宣言した場合
public(デフォルト) どこからでも参照可能 どこからでも参照可能
internal モジュール内からのみ参照可能 モジュール内からのみ参照可能
protectrd サブクラスから参照可能 -
private クラス内からのみ参照可能 ファイル内からのみ参照可能


protected の挙動

protected 修飾子については、Javaでは同一パッケージ内からでもprotected なメンバにアクセスできますが、Kotlinではそのアクセスを許容していません。
Kotlinでは protected なメンバは、そのクラスとサブクラスからのみ参照可能となっています。

また、拡張関数はprivate や protected なメンバに対してアクセスする事ができません。

internal open class Cat(val name: String, val age: Int) {
    private fun printName() = println("My name is $name")

    protected fun printAge() = println("$age years old.")
}

internal fun Cat.walk() {
    
    printName()  // private なメンバにアクセスしようとしてコンパイルエラー

    printAge()  // protected なメンバにアクセスしようとしてコンパイルエラー
}


まとめ

Java と Kotlinの可視性はかなり似ていますが、Kotlinの可視性の方がより理にかなったものになっていると言えます

Kotlin - 継承

Javaのクラスは明示的に final キーワードを付与しない限り、どのようなクラスからでも継承してそのサブクラスを作成可能です。
メソッドもオーバーライドできます。

◾️簡単に継承できるというサンプル

public class Person {

    protected final String name;

    public Person(String name) {
        this.name = name;
    }

    public void myName() {
        System.out.println("My Name is Unknown");
    }
}

public class Tom extends Person {

    public Tom() {
        super("Tom");
    }

    @Override
    public void myName() {
        System.out.println("My name is Tom!!");
    }
}

これは便利なように見えて、壊れやすい基底クラス(fragile base class)という問題を起こしやすくなります。
基底クラス(親クラス)のコードが変更された時に、サブクラスで意図しない挙動を引き起こすことがあるというものです。

サブクラスは基底クラスの作成者が予期していない方法でメソッドをオーバライドしてしまうというリスクが存在します。

Javaプログラミングスタイルの名著と呼ばれている「Effective Java」では、
"継承のために設計及び文書化する。でなければ継承を禁止する" と記述されています。
これは、サブクラスによってオーバライドされる事を意図しない全てのクラスやメソッドは明示的に final と指定するべきである ということを意味しています。

とは書かれていますが、Javaの言語仕様上はデフォルトが継承可能な状態なので、なかなかこの状態は守られることは少ないように感じます。
そして、壊れゆくクラス達が多数出現するのを目にします。


Kotlinではデフォルトは final

Kotlin では、「Effective Java」に記述された思想を受け継いでいます。

Java のクラスやメソッドがデフォルトでサブクラス化やオーバーライドが可能なのに対して、
Kotlin の場合、デフォルトがfinalの状態になっています。

あるクラスを継承したサブクラスの作成を許可するためには、そのクラスにopen 修飾子を付ける必要があります。
+ オーバーライドを許可する全てのメソッドやプロパティにも open 修飾子を付ける必要があります。

例えば以下のようなクラスを定義しても継承もメソッドのオーバーライドもできません。

class KotlinPerson(val name : String) {
    fun myName() {
        println("My name is unknown")
    }
}


継承させたい場合は open の付与が必要です

open class KotlinPerson(val name : String) {
    open fun myName() {
        println("My name is unknown")
    }
}


こうすると以下のような継承したサブクラスの作成やメソッドのオーバーライドができます

class Mike(name: String) : KotlinPerson(name) {

    override fun myName() {
        println("My name is Mike")
    }
}

ちなみに、基底クラスやインターフェースのメンバをオーバーライドした場合、オーバーライドしたメンバはデフォルトで open となっています。
自身の実装をさらなるサブクラスでオーバーライド禁止にしたい場合は、そのメンバに final を指定する必要があります。


抽象クラス

Kotlin には Java と同様に abstract キーワードを指定して以下のような抽象クラスを宣言することが可能です。

abstract class Animal {

    // 抽象メソッド。サブクラスでのオーバーライド必須
    abstract fun walk()

    // 非抽象メソッドはデフォルト open ではないが openと付与することもできる
    open fun eat() {
    }

    // 非抽象メソッド。open指定してないのでサブクラスでオーバーライドできる
    fun smell() {
        println("Smell the smell")
    }
}


継承したサブクラスは以下のように定義できます

class Cat : Animal() {
    // 抽象メソッドなのでオーバーライド必須
    override fun walk() {
        println("Cat is walk")
    }

    // 親クラスでopenにしてあったのでオーバーライド可能
    override fun eat() {
        super.eat()
        println("The cat is eating deliciously")
    }
}


Kotlin におけるクラスでのアクセス修飾子の意味は以下の通りです

  • final
    • 対応するメンバはオーバーライドできない
    • クラスのデフォルト値
  • open
    • 対応するメンバはオーバライドできる
    • これを付与する事で継承ができるようになる。明示的な指定が必要。
  • abstract
    • 対応するメンバをオーバーライドしなければならない
    • 抽象クラスのみで使用可能。抽象メンバ(抽象メソッド)は実装を持つ事ができない
  • override
    • 対応するメンバの内容をオーバーライドする
    • finalと指定しなければオーバーライドしたメンバはデフォルトで open となる


まとめ

簡単にですが、Kotlinの継承に関わる内容を記述しました。といっても、相変わらす「Kotlinイン・アクション」の写経にすぎませんが。。。
個人的には、Kotlinではデフォルトでクラス(実装クラス)がデフォルトで継承できないというのが良い点だなと思います。
Javaだと共通化という名目で、実装クラスを継承してやたら複雑になってしまったアプリケーションを見る事が少なくありません。
Kotlinは、これを少しでも防ぐ方向にあるように感じました。