覚えたら書く

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

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は、これを少しでも防ぐ方向にあるように感じました。

Kotlin - インターフェース

Java で インターフェースの定義をするコードは以下のようになります。

public interface Printable {

    void print();

    void printDetail();
}

そしてこのインターフェースを実装した実装クラスのコードは以下のようになります

public class JavaPrinter implements Printable {

    @Override
    public void print() {
        System.out.println("Java summary");
    }

    @Override
    public void printDetail() {
        System.out.println("Java detail print");
    }
}

実装するインターフェースを指定する部分で implements キーワードを記述します。

以下コードで実行してみると

Printable printer = new JavaPrinter();

printer.print();
printer.printDetail();

結果は以下の通りです。当然の結果ですが。

Java summary
Java detail print


ちなみに先ほどの実装クラスのJavaPrinterは以下のように書く事もでき、@Override アノテーションの記述なしでもコンパイルが通ります。

public class JavaPrinter implements Printable {

    public void print() {
        System.out.println("Java summary");
    }

    public void printDetail() {
        System.out.println("Java detail print");
    }
}


Kotlinのインターフェース

KotlinとJavaは相互に呼び出し可能であることから、先ほどJavaで書いた Printable インターフェースを Kotlinのクラスとして実装可能です。

以下のようになります

class KotlinPrinter : Printable {
    override fun print() {
        println("Kotlin summay")
    }

    override fun printDetail() {
        println("Kotlin detail print")
    }
}

Kotlin ではクラス名の後ろに implements キーワードの代わりに : (コロン)を記述して、そのあとにインターフェース名を指定します。
ちなみに、Java では継承の場合は extends キーワードを使いますが、Kotlinの場合は継承の場合でも : (コロン)を使います。

以下は先ほどの Kotlin での実装クラスを実行するコードです

val printer : Printable = KotlinPrinter()
printer.print()
printer.printDetail()

実行結果は以下の通りです。

Kotlin summay
Kotlin detail print


ちなみに実装クラスを以下のようにしようとするとコンパイルエラーとなります。

// コンパイルエラー
class KotlinPrinter : Printable {
    fun print() {
        println("Kotlin summay")
    }

    fun printDetail() {
        println("Kotlin detail print")
    }
}

これは、override 修飾子 がないためです。
Kotlin の override 修飾子 は、Java の@Override アノテーションと同様に、スーパークラスやインターフェースをオーバライドするメソッドやプロパティを指定するために利用します。
しかし、Javaとは違って Kotlin の override 修飾子は省略できません。必須です。


Javaでのインターフェースしか書いていませんでしたが、Kotlinでのインターフェースの記述は以下のようになります。

interface Printable {

    fun print()

    fun printDetail()
}

ちなみに、インターフェースはデフォルトの実装を持つ事ができます。
Java 8とは異なって、Kotlinではメソッドに default というような特別なキーワードをつける必要はありません。

デフォルト実装をも持たせたインターフェースの例は以下の通りです。
デフォルト実装を持つメソッドを実装クラス側でオーバーライドしなければ、インターフェースに定義したデフォルトの処理が実行されます。

interface Printable {

    fun print() = println("Hello World")

    fun printDetail()
}


さらにここに以下のようなインターフェースを登場させて

interface Display {
    fun print() = println("Hello Display")
}

KotlinPrinterクラスに両方のインターフェースを実装させて、デフォルト実装をオーバーライドしないようにしようとするとコンパイルエラーとなります。

// コンパイルエラー
class KotlinPrint : Printable, Display {

    override fun printDetail() {
        println("Detail Print")
    }
}

複数のインターフェースから、複数の実装を継承してしまっているためです。
このようなケースでは、以下のように実装クラスで必ず対象のメソッドをオーバーライドする必要があります。

class KotlinPrint : Printable, Display {
    
    override fun print() {
        super<Printable>.print()
        super<Display>.print()
    }

    override fun printDetail() {
        println("Detail Print")
    }
}

ここで super<Printable>.print()super<Display>.print() の部分が継承したデフォルト実装を呼び出すコードになっています。
super.<親の型名>.{メソッド名} の記述が、継承元のメソッドを呼び出すことになります。


まとめ

Kotlin での インターフェースの定義方法と実装方法の基礎が分かりました。
まだ、この辺りだとJavaとの大きな違いは無いかなという印象です。

Kotlin - ローカル関数

例えば以下のような User クラスとそのUserのオブジェクトを登録する関数が存在するとします。

class User(val id: Int, val firstName: String, val lastName: String)

fun registerUser(user: User) {

    if (user.firstName.isEmpty()) {
        throw IllegalArgumentException("Invalid User id=${user.id} : firstName is Empty")
    }

    if (user.lastName.isEmpty()) {
        throw IllegalArgumentException("Invalid User id=${user.id} : lastName is Empty")
    }

    // Register to persistence area ...
}

登録関数の registerUser は実際の登録処理前に、Userの firstName と lastName が空文字列でないかのバリデーションを行い、
空であれば IllegalArgumentException をスローします。


一応実行させてみます

fun main() {
    registerUser(User(100, "", "Yamada"))
}

結果は以下の通りです

Exception in thread "main" java.lang.IllegalArgumentException: Invalid User id=100 : firstName is Empty
    at net.yyuki.sandbox.localfunc.LocalFuncKt.registerUser(LocalFunc.kt:10)
    at net.yyuki.sandbox.localfunc.LocalFuncKt.main(LocalFunc.kt:21)
    at net.yyuki.sandbox.localfunc.LocalFuncKt.main(LocalFunc.kt)


firstName と lastName のチェックを行っている処理は重複していて、DRY原則に従うのであれば、関数化して重複を取り除く必要があるでしょう。

こういった場合に、バリデーションの処理部分をローカル関数として配置する事で重複を取り除く事ができます。
validation というローカル関数を、 registerUser関数の中に配置する事でバリデーション処理の重複を取り除く事ができました。

class User(val id: Int, val firstName: String, val lastName: String)

fun registerUser(user: User) {

    fun validation(user: User, value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Invalid User id=${user.id} : $fieldName is Empty")
        }
    }

    validation(user, user.firstName, "firstName")
    validation(user, user.lastName, "lastName")

    // Register to persistence area ...
}

先ほどと同様の呼び出しで実行してみると結果は以下の通りで、予定通りに例外がスローされています

Exception in thread "main" java.lang.IllegalArgumentException: Invalid User id=100 : firstName is Empty
    at net.yyuki.sandbox.localfunc.LocalFuncKt$registerUser$1.invoke(LocalFunc.kt:11)
    at net.yyuki.sandbox.localfunc.LocalFuncKt.registerUser(LocalFunc.kt:15)
    at net.yyuki.sandbox.localfunc.LocalFuncKt.main(LocalFunc.kt:22)
    at net.yyuki.sandbox.localfunc.LocalFuncKt.main(LocalFunc.kt)


ローカル関数は、外側の関数の全ての引数や変数にアクセスする事ができるため、validation 関数に Userのオブジェクトを渡す必要はありません。
そのため、以下のように書き換え可能です。

class User(val id: Int, val firstName: String, val lastName: String)

fun registerUser(user: User) {

    fun validation(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Invalid User id=${user.id} : $fieldName is Empty")
        }
    }

    validation(user.firstName, "firstName")
    validation(user.lastName, "lastName")

    // Register to persistence area ...
}

ローカル関数へ Userのオブジェクトを渡さなくなったので、だいぶすっきりしました。

さらに、validation 関数をUserクラスの拡張関数へと移動する事もできます

class User(val id: Int, val firstName: String, val lastName: String)

private fun User.validation() {
    fun validation(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Invalid User id=$id : $fieldName is Empty")
        }
    }

    validation(firstName, "firstName")
    validation(lastName, "lastName")
}

fun registerUser(user: User) {
    user.validation()

    // Register to persistence area ...
}

拡張関数にすることで、余分な修飾もなくUserオブジェクトのメンバにアクセスできます。

Kotlin - トリプルクォート文字列

現在のJavaにはなくてKotlinにはあるものの一つに トリプルクォート文字列(triple-quoted string) があります。

固定文字列は、"(ダブルクォート)で囲って記述しますが、
この際に、その文字列内に ダブルクォート を入れたり 改行を入れたりしようとすると 必ずエスケープが必要になってきます。

例えば以下のようなクラスを定義したとして

public class Person {
    private final long id;
    private final String name;
    private final int age;

    public Person(long id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

以下のJSON文字列から上記クラスのオブジェクトへと変換するものとします

{
    "id" : 10001,
    "name" : "KotorinTaro",
    "age" : 25
}

JSON文字列からこのオブジェクトに変換する処理をJavaで書こうとすると以下のようになります。
(今回の例では、変換に Gsonライブラリを使用しています)


JSON文字列からオブジェクトへの変換のコード(一部抜粋)

import com.google.gson.Gson;

// JSON文字列の定義
String json = "{ \"id\": 10001, \"name\": \"KotorinTaro\", \"age\": 25 }";

Gson gson = new Gson();
Person person = gson.fromJson(json, Person.class);

System.out.println(json + "\n -> " + person);

実行結果は以下の通りです

{ "id": 10001, "name": "KotorinTaro", "age": 25 }
 -> Person{id=10001, name='KotorinTaro', age=25}


一応、問題なく変換できているようですが、
そもそも元のJSON文字列の定義がエスケープだらけでJSON文字列として正しいのかよく分かりません。

String json = "{ \"id\": 10001, \"name\": \"KotorinTaro\", \"age\": 25 }";

同じ階層に3項目しかないJSONなので、まだマシと言えばマシですがかなり分かりにくいです。


普通のアプリで、わざわざJSONを文字列リテラルで記述することなんか無いでしょう・・・と思いたくなりますが、
たとえばユニットテストのインプットとしてJSON文字列などを使いたい(外部からJSONの電文が渡された想定の)場合などに、
JSON文字列を直接記述したくなります。

が、JSONを素直に記述することもできないので、JSONファイルを用意してその中にJSONを書いて、
ユニットテスト時に一旦そのファイルから読み取った文字列をテストケースに渡す。
といったことになりがちです。


トリプルクォート文字列

Kotlinのトリプルクォート文字列は以下のような記述をします

val str = """なんかの文字列"""


たとえば改行やダブルクォート等を含んだ文字列でもエスケープが必要ありません

◾️サンプルコード1

val str = """|"My Name is Kotorin Taro."|"""

println(str)

◾️実行結果

|"My Name is Kotorin Taro."|


◾️サンプルコード2

val str = """
     for (ch in "something") {
        println(ch)
    }
    """

    println(str)

◾️実行結果

    for (ch in "something") {
        println(ch)
    }


文字列の先行する空白行(左のマージン)を取り除いた状態にしたい場合は、マージンの終わりをマークするマージンプレフィックス(規定のマージンプレフィックスは | (パイプ))と trimMargin関数を使用します。

◾️サンプルコード3

val str = """
    |for (ch in "something") {
    |    println(ch)
    |}
    """.trimMargin()

println(str)

◾️実行結果

for (ch in "something") {
    println(ch)
}


当初のJSON文字列をオブジェクト化するサンプルをKotlinで以下のように書く事ができます

import com.google.gson.Gson

val json = """
    |{
    |    "id" : 10001,
    |    "name" : "KotlinTaro",
    |    "age" : 25
    |}
    """.trimMargin()

val person = Gson().fromJson(json, Person::class.java)

println("$json \n-> $person")

実行結果は以下の通りです

{
    "id" : 10001,
    "name" : "KotlinTaro",
    "age" : 25
} 
-> Person{id=10001, name='KotlinTaro', age=25}


まとめ

Kotlin の トリプルクォート文字列 を利用する事で、通常であればエスケープを必要とするような文字列も素直に記述できる事が分かりました。
ここには書きませんでしたが、正規表現の記述の場合にも トリプルクォート文字列 が有効に利用できます。