覚えたら書く

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

Kotlin - オブジェクト式

Kotlin の object キーワードは、無名オブジェクト(anonymous object)の宣言のためにも使用できます。
無名オブジェクト は Javaにおける無名内部クラスを置き換えるものとなります。

Javaで無名内部クラスの典型的な利用シーンというと、イベントリストが必要となるケースがあります。

例えば以下のようなJavaのコードがあったとした場合(import 等もろもろ省略)

JWindow window = new JWindow();
window.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseClicked(MouseEvent e) {
        System.out.println("Mouse Clicked");
    }

    @Override
    public void mousePressed(MouseEvent e) {
        System.out.println("Mouse Pressed");
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        System.out.println("Mouse Released");
    }
});

上記コードは Kotlin ではオブジェクト式で以下のように記述できます。

val window = JWindow()
window.addMouseListener(
    object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            println("Mouse Clicked")
        }

        override fun mousePressed(e: MouseEvent) {
            println("Mouse Pressed")
        }

        override fun mouseReleased(e: MouseEvent) {
            println("Mouse Released")
        }
    }
)

オブジェクトの名前を省略しているという点をを除いて、オブジェクト宣言と同じです。
オブジェクト式はクラスを宣言し、そのクラスのインスンスを生成しています。


Javaの無名クラスと同様にオブジェクト式の中のコードは、それが生成された関数内の変数を参照することができます。
しかし、Javaとは違って、その変数がfinalでなければならないという制約はありません。
そのため、オブジェクト式の中から変数の値を変更することも可能です。

以下のようなコードを書くことも可能です

var count = 0
val window = JWindow()
window.addMouseListener(
    object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            println("Mouse Clicked")
            count++
        }

        override fun mousePressed(e: MouseEvent) {
            println("Mouse Pressed")
            count++
        }

        override fun mouseReleased(e: MouseEvent) {
            println("Mouse Released")
            count++
        }
    }
)


まとめ

オブジェクト式により Java の無名内部クラスを置き換えることができることがわかりました。

Kotolin - コンパニオンオブジェクト 2

Kotlin のコンパニオンオブジェクトは、クラス内に宣言された通常のオブジェクトであり、
名前を付けたり、インターフェースを実装したり、拡張関数やプロパティを持つこともできます。


コンパニオンオブジェクトに名前を付ける

前回のエントリですでにやってやってますが、コンパニオンオブジェクトに名前を付けることが可能です。

import com.google.gson.Gson

class Person(val name: String, val age: Int) {
    // 名前付きのコンパニオンオブジェクトの宣言
    companion object Decoder {
        fun fromJSON(jsonStr: String): Person {
            return Gson().fromJson(jsonStr, Person::class.java)
        }
    }
}

呼び出し側のコードは以下の通りで、コンパニオンオブジェクトの名前を明示することも省略することも可能です。

// コンパニオンオブジェクトの名前を指定して関数を呼び出せる
val p1 = Person.Decoder.fromJSON("""{"name": "Kotlin Taro", "age": 32}""")
println("${p1.name} is ${p1.age} years old.")  // -> Kotlin Taro is 32 years old.

// コンパニオンオブジェクトの名前を省略することもできる
val p2 = Person.fromJSON("""{"name": "Java Taro", "age": 39}""")
println("${p2.name} is ${p2.age} years old.")  // -> Java Taro is 39 years old.


コンパニオンオブジェクトでインターフェースを実装する

他のオブジェクト宣言と同じくコンパニオンオブジェクトはインターフェースを実装できます。
インターフェースを実装しているオブジェクトのインスタンスとして、そのオブジェクトを含むクラスの名前を使用できます。

JSONFactory というインターフェースを介してオブジェクトが生成されるものとする場合、
以下のようなサンプルコードを書くことができます。

import com.google.gson.Gson

interface JSONFactory<T> {
    fun fromJSON(jsonStr: String): T
}

class Person(val name: String, val age: Int) {
    // 名前付きのコンパニオンオブジェクトの宣言
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonStr: String): Person {
            return Gson().fromJson(jsonStr, Person::class.java)
        }
    }
}


JSONFactoryを引数に取る関数を定義し多とした場合、
その関数を呼び出す場合には、JSONFactoryをコンパニオンオブジェクトが実装しているので Person オブジェクトを引数に渡すことが可能です。

fun <T> loadFromJSON(jsonStr: String, factory: JSONFactory<T>): T {
    return factory.fromJSON(jsonStr)
}

fun main() {
    // JSONFactoryインターフェースの
    val p1 = loadFromJSON("""{"name": "Java Taro", "age": 45}""", Person) // コンパニオンオブジェクトのインスタンスを関数に渡す
    println("${p1.name} is ${p1.age} years old.")
}


コンパニオンオブジェクトの拡張関数

クラスがコンパニオンオブジェクトを持っていれば、コンパニオンオブジェクトに対して拡張関数を定義することが実現できます。
例えば ClazzA がコンパニオンオブジェクトを持つ時、ClazzA.Companion に対する拡張関数 function を定義したと仮定すると、
その関数を ClazzA.function() として呼び出すことができます。


すでに定義した Person クラスは、そのドメインに特化したビジネスロジックのみを持つものとしておきたい場合に、
データのシリアライズやデシリアライズのルールは分離しておきたい(Personの核となるドメインとは別にしておきたい)ことがあります。
このような場合に拡張関数を利用すると実現可能です。

import com.google.gson.Gson

// domain core module
class Person(val name: String, val age: Int) {
    // 空のコンパニオンオブジェクトを宣言しておく
    companion object {
    }
}

// client / server communication module
fun Person.Companion.fromJSON(jsonStr: String): Person {
    return Gson().fromJson(jsonStr, Person::class.java)
}

以下のように実行可能です。

val p1 = Person.fromJSON("""{"name": "Java Taro", "age": 45}""")
println("${p1.name} is ${p1.age} years old.")


まとめ

今回は、Kotlin のコンパニオンオブジェクトへの名前付け、インターフェースの実装、拡張関数の定義が可能であることを確認しました。



関連エントリ

Kotlin - コンパニオンオブジェクト

Javaと異なり Kotlin のクラスは静的なメンバを持つことができません。Javaにおける static キーワードは Kotlin には含まれていません。

この Java における static の代替手段として以下を利用します。

  • トップレベル関数関数
    • 多くの状況で Java の staticメソッドを置き換えることが可能
  • オブジェクト宣言
    • Javaの静的フィールドと静的メソッドの置き換えをします

多くの状況では トップレベル関数 を使うことが推奨されているようです。
しかし、トップレベル関数は、クラスのprivateメンバにアクセスできないという制約が存在します。
クラスのインスタンスなしで、クラス内部にアクセスする必要がある関数を記述する必要がある場合、
対象クラスの内部のオブジェクト宣言のメンバとして関数を書くことが可能です。


クラス内で定義されているオブジェクトの1つに companion というキーワードを付けることが可能です。
このキーワードを付与することで、オブジェクトの名前を明示せずに、クラス名を使って直接そのオブジェクトのメソッド名やプロパティにアクセス可能となります。
Javaにおける静的メソッド呼び出しと全く同じ状態となります。

class Printer {
    companion object {
        fun print(data : String) {
            println("Printer printed... [$data]")
        }
    }
}

呼び出し側のコードは以下のようになります

Printer.print("Kotlin")  // -> Printer printed... [Kotlin]


companion のキーワードを付ける際にどこまで意味があるのか不明ですが、オブジェクト宣言で名前を指定することも可能です

class Printer {
    companion object Java {
        fun print(data : String) {
            println("Printer printed... [$data]")
        }
    }
}

呼び出し側のコードでは companionを付与したオブジェクト宣言の名前を経由してもしなくても内部の関数にアクセス可能です

Printer.print("Kotlin")  // -> Printer printed... [Kotlin]
Printer.Java.print("Java Lang")  // -> Printer printed... [Java Lang]


このコンパニオンオブジェクトの利用の好例が ファクトリメソッド(Javaでいうところのstaticファクトリ) になると思われます。

Javaでも同じですが複数のコンストラクタを持つクラスの場合、コンストラクタには名前を付けることができないためコード上の意味づけがしにくくなります。
そういった場合にはファクトリメソッド(staticファクトリ)を用意して、意味のある名前を付けます。


Kotlin で以下のような2つのセカンダリコンストラクタを持つクラスが存在したとします。

class User {
    val userName : String
    
    constructor(handleName: String) {
        this.userName = handleName
    }

    constructor(firstName: String, lastName: String) {
        this.userName = "$firstName-$lastName"
    }
}


これはコンパニオンオブジェクトを利用して以下のように書き換え可能です。

class User {
    val userName : String

    private constructor(handleName: String) {
        this.userName = handleName
    }

    private constructor(firstName: String, lastName: String) {
        this.userName = "$firstName-$lastName"
    }

    companion object {
        fun handleNamUser(handleName: String) =
                User(handleName)

        fun fullNameUser(firstName: String, lastName: String) =
            User(firstName, lastName)
    }
}


呼び出しコードは以下のようになります

val user1 = User.handleNamUser("handle-001")
println(user1.userName)  // -> handle-001

val user2 = User.fullNameUser("Taro", "Kotlin")
println(user2.userName)  // -> Taro-Kotlin


まとめ

サンプルコードとしては微妙だった気もしますが、コンパニオンオブジェクトによってファクトリメソッド(staticファクトリ)が実現できることがわかりました。



関連エントリ

Kotlin - シングルトン

Java でプログラムを書いている場合に、対象クラスのインスタンスがただ1つだけ存在する状態にしたい場合があります。

そういった場合には、一般的に Singleton パターン(シングルトン・パターン) というデザインパターンを用いてクラスを宣言する事になります。

Java でシングルトンを実現する方法はいくつかありますが、例えば以下のような記述をすることでシングルトンのクラスとなります。
(以下コードでは import文等を一部省略しています)

public final class ProductList {

    public static final ProductList INSTANCE = new ProductList();

    public static ProductList getInstance() {
        return INSTANCE;
    }

    private final List<String> products =
            Arrays.asList("Product1", "Product1.2", "ProductB", "ProductC");

    public List<String> allProducts() {
        return products;
    }

    private ProductList() {
    }
}

クラスがシングルトンとなっているのは上記宣言の以下のようなコードの記述の組み合わせによります。

  • コンストラクタが private (外部からの通常のコンストラクタでのインスタンス化ができないようにする)
  • static final で自分自身のインスタンスを保持する。(これが対象クラスの唯一のインスタンス)
  • 保持しているインスタンスを外部から取得するためのメソッドを定義(ここでは getInstance)


Javaでは上記のようなコードの組み合わせによってシングルトンを実現するわけですが、
Kotlin では、シングルトンの定義をするためのサポートを第一級言語機能として提供しています。
それが オブジェクト宣言(object declaration)です。

オブジェクト宣言は、クラス宣言(class declaration)とそのクラスのシングルインスタンス(single instance)の宣言が組み合わさっています。

先ほどの Java で定義したシングルトンのクラスを Kotlin で記述すると以下のようになります。(Listのクラスが若干違いますが、そこは無視で。)

object ProductList {

    private val products = arrayListOf("Product1", "Product1.2", "ProductB", "ProductC")

    fun allProducts() : List<String> {
        return products
    }
}

オブジェクト宣言は、 object キーワードで始まります。
クラスと同様に、オブジェクト宣言は、プロパティ、メソッド、初期化ブロックなどの宣言を含めることも可能です。
コンストラクタ(プライマリコンストラクタ、セカンダリコンストラクタ)は含めることができません。
通常のクラスのインスタンスと異なり、オブジェクト宣言はコード中の他の場所からのコンストラクタ呼び出しではなく、定義の時点で即座にインスタンスが生成されます。

オブジェクト宣言した内容の呼び出しは、<オブジェクト名>.<メソッド名> という形式になります。
上記でオブジェクト宣言した内容以下のようになります

ProductList.allProducts()


オブジェクト宣言は、クラスやインターフェースを継承することも可能です。

java.util.Comparator インターフェースを実装するクラスは、compare メソッドで2つのオブジェクトを引数に取り、
いずれのオブジェクトが大きいかを示す整数値を返却します。 このクラスは、基本的にcompare メソッドの引数にのみ依存して動きが制御され、基本的にクラス自体が状態を持つことはほぼありません。
そのため、インスタンスは1つだけ存在すればよいことになります。

こういった場面で オブジェクト宣言 が利用できます。

import java.io.File

object FilePathCmparator : Comparator<File> {

    override fun compare(f1: File, f2: File): Int {
        return f1.path.compareTo(f2.path, ignoreCase = true)
    }
}

このオブジェクトを利用してソートを実行したコードは以下の通りです

val pathList = listOf(
    File("/tmp"), File("/storage1/dir001/f001"), File("/storage1/dir002"),
    File("/storage1/dir001"), File("/storage1"), File("/root")
)

val sortedList = pathList.sortedWith(FilePathCmparator)

println("$pathList\n->\n$sortedList")

実行結果以下の通りです

[/tmp, /storage1/dir001/f001, /storage1/dir002, /storage1/dir001, /storage1, /root]
->
[/root, /storage1, /storage1/dir001, /storage1/dir001/f001, /storage1/dir002, /tmp]


Java からKotlinのオブジェクトを呼び出す

Kotlin のオブジェクト宣言は、1インスタンスを静的フィールドに保持するようなクラスにコンパイルされます。
その静的フィールドの名前は INSTANCE となっています。
Javaでシングルトンのインスタンスを宣言したのとほぼ同じ状態と言えます。

そのためKotlinのオブジェクトをJavaのコードから使うためには、以下のようにINSTANCEフィールドにアクセスする方法となります。
(Kotlinでオブジェクト宣言した ProductList のメソッドを呼び出している例です)

public class Main {
    public static void main(String[] args) {
        System.out.println(ProductList.INSTANCE.allProducts());
    }
}


シングルトンの補足

デザインパターンの中でも シングルトンパターン は特殊な部類に入り、シングルトンパターンのクラスは必ず実装クラスとなります。
そのため、そのクラスに依存するクラスのユニットテスト時に他の実装に差し替えること等ができません。
シングルトンパターンを利用するクラスとは密結合することになります、また、使い方を誤るとグルーバル変数のような物体と化します。
そのため、シングルトンパターンで作成したオブジェクトを多用しすぎないように注意が必要です。


まとめ

Kotlin では シングルトンのクラスを宣言するためのサポートが 言語機能として提供されているため、簡単にシングルトンのオブジェクトの作成ができることが分かりました。

Kotlin - 移譲

Java(オブジェクト指向言語)を用いて大規模なシステムを設計・開発した場合に、実装の継承 によってシステムが壊れていくことが多々あります。

共通化という名目(DRYの達成)のために、実装クラスを継承してしまっていると、
クラスを拡張し、いくつかのメソッドをオーバーライドするときに、拡張元の基底クラスの実装の詳細に依存するようになり、極端な密結合が発生します。

対象のシステムが発展する中で、基底クラスの実装内容が変更されたり、新しいメソッドが追加されたりすると
継承した側の子クラスのコードが正しく動かなくなる事があります。

実装クラスの継承を行うと、コードを共通化して綺麗に整理できた気分になれるので、陥りがちな状況な気がします・・・。
対象システムの改修作業のために、後から参画したメンバーやコードを保守するメンバーが痛い目に逢います。
後任者からは、「結局、親クラスの親クラスまで見ないとコードの意味わからないじゃん、影響範囲わからないじゃん。」とかいう声が上がる事でしょう。


Kotlin では、上記のような問題への対処のために、デフォルトでクラスは final の状態(継承不可)となっています。
拡張性を考えて設計されたクラスのみを継承可能としており、あえて open という修飾子を指定する必要があります。


クラスを拡張したいケースでは、実装の継承を行う代わりに、
拡張元のクラスと同じインターフェースを実装して、元の実装クラスをフィールドに持つという方法が取られます。
拡張が必要ないメソッドはそのまま元の実装クラスに処理を転送、拡張が必要なメソッドは自前の実装でオーバーライドする
というやり方になります。

ただし、Java でこのアプローチを行なった場合には元の実装クラスへ処理を転送する大量のボイラープレートコードが発生します。

java.util.List を実装したクラスを作成して、フィールドに持ったArrayList のインスタンスに処理を転送しようとすると以下のようなコードになってしまいます。

public class ExArrayList<String> implements List<String> {

    // 処理の転送先となる実装クラス
    private final List<String> internalList = new ArrayList<>();

    // 拡張したメソッド
    @Override
    public boolean add(String string) {
        System.out.println("Try add " + string);
        return internalList.add(string);
    }

    @Override
    public int size() {
        return internalList.size();
    }

    @Override
    public boolean isEmpty() {
        return internalList.isEmpty();
    }

    @Override
    public boolean contains(Object o) {
        return internalList.contains(o);
    }

    @Override
    public Iterator<String> iterator() {
        return internalList.iterator();
    }

    @Override
    public Object[] toArray() {
        return internalList.toArray();
    }

    @Override
    public boolean remove(Object o) {
        return internalList.remove(o);
    }

    @Override
    public void replaceAll(UnaryOperator<String> operator) {
        internalList.replaceAll(operator);
    }

    @Override
    public void sort(Comparator<? super String> c) {
        internalList.sort(c);
    }

    @Override
    public void clear() {
        internalList.clear();
    }

    @Override
    public boolean equals(Object o) {
        return internalList.equals(o);
    }

    @Override
    public int hashCode() {
        return internalList.hashCode();
    }

    @Override
    public String get(int index) {
        return internalList.get(index);
    }

    @Override
    public String set(int index, String element) {
        return internalList.set(index, element);
    }

    @Override
    public void add(int index, String element) {
        internalList.add(index, element);
    }

    @Override
    public String remove(int index) {
        return internalList.remove(index);
    }

   // 以下省略
}


ほとんどの場面で、実装の継承をするよりもより良い方法とはなりますが、移譲(delegation)というものを Java が言語的にサポートしていないために、
ボイラープレートコードが生まれてしまうという状況に陥ってしまいます。


Kotin では、この 移譲 を第一級言語機能としてサポートしています。
インターフェースを実装している場合、 by キーワード を利用する事によってインターフェースの実装を別のオブジェクトに移譲できます。


Kotlin でさきほどの実装と同様のクラスの宣言は以下のように記述できます。
自分で拡張したい add メソッド以外は登場しておらず、ボイラープレートコードが激減しています。

class ExArrayList<String>(val innerList: MutableList<String> = ArrayList()): MutableList<String> by innerList {

    override fun add(element: String): Boolean {
        println("Try add $element")
        return innerList.add(element)
    }

}

このクラスを実行するサンプルコードで実行してみると

val list = ExArrayList<Any>()
list.add("Kotlin")
list.add("Java")

println("$list : List size = ${list.size}")

実行結果は以下の通りです。addメソッドが予定通り拡張されていることが分かります。

Try add Kotlin
Try add Java
net.yyuki.sandbox.list2.ExArrayList@59a6e353 : List size = 2


by キーワードはあくまで対象インターフェースを転送してくれるだけのようなので、それとは関係ない toString などはオーバーライドされていないようです。

toString までオーバーライドまでしたければ、データクラス として宣言しちゃえばいいわけですが、こんなことしていいのかは良く分かってないです笑

data class ExArrayList<String>(val innerList: MutableList<String> = ArrayList()): MutableList<String> by innerList {

    override fun add(element: String): Boolean {
        println("Try add $element")
        return innerList.add(element)
    }

}

こうすると toString の結果も意味のある文字列となります

Try add Kotlin
Try add Java
ExArrayList(innerList=[Kotlin, Java]) : List size = 2


まとめ

"継承よりコンポジションを選ぶ" という記述が「Effective Java」に存在していますが、残念ながらJavaでは移譲(delegate)は、第一級言語機能となっていません。
そのためか、Javaで作られたシステムでは、実装の継承 というものが氾濫していることが少なくありません。

Kotlin では by キーワードによって簡単に移譲を実現できます。これによって、クラス間の不要な密結合が生まれにくくなると思われます。



関連エントリ