覚えたら書く

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の可視性の方がより理にかなったものになっていると言えます