18日目:パッケージと標準ライブラリ
今日はKotlinの標準ライブラリが提供する便利なクラスや関数について紹介します。ライブラリは、自然言語のメタファを拝借するなら、語彙と例えられることがあります。ライブラリの提供する機能群(語彙)を知ることにより、表現豊かなコードを書くことができるようになります。
パッケージ
標準ライブラリの紹介の前にKotlinにおける名前空間のお話をします。
KotlinではJavaと同様に名前空間をパッケージによって区切ることができます。名前空間を区切ることは、クラスなどの名前(完全修飾名)の衝突を避け、世界中でユニークなものにできたり、類似した機能をまとめたりといったメリットがあることは、Javaプログラマにとっては釈迦に説法ですね。
パッケージを定義するには、これもJavaと同じで、ソースファイル先頭にキーワード package を記述します。その後に続けてパッケージ名を書いていきます。必要があればドットを打ってパッケージを区切ることもできます。
package com.taroid.sample
Kotlinでは、パッケージはファイルシステムと対応していなくても問題ありません。Javaでは例えば、上記のようなパッケージを定義したら、その配下にあるクラスのソースファイルは次のようなパスのディレクトリに格納しなければならないというルールがあります。
<プロジェクトルート>/com/taroid/sample/
Kotlinにはこのようなルールは存在しません。
インポート
パッケージ内に定義されているクラスにアクセスする場合は、完全修飾名を指定するか、パッケージをインポートする方法があります。これもJavaと同様です。
インポートするときに次のようにキーワード as を付けるとインポート対象に別名を付けることができます。
package foo import bar.Bar as Hoge fun main(args : Array<String>) { Hoge() }
標準ライブラリ
では、Kotlinの標準ライブラリの提供するクラス、関数について見ていきたいと思います。デフォルトでは*1、パッケージ kotlin とパッケージ jet *2内のクラスや関数は暗黙的にインポートされているようです*3。
リストと配列
クラス Array のコンストラクタ
Kotlinでは配列は特別な存在ではなく、単なるジェネリッククラスです。そのコンストラクタは引数を2つ取ります。1つ目が配列の長さ。もう1つが、配列初期化のための関数を取ります。次のコードは5つの要素を持った配列を生成しています。配列の要素は先頭から、0、2、4、5、6、8です。
val array = Array(5) { it * 2 } array forEach { print(it) }
各要素を走査する関数 forEach も上記例で使用しています。リストクラスやレンジクラスも同様の関数を持っています。関数 forEach は引数に「要素を引数に取る関数」を取ります。上記コードを実行すると標準出力に「02468」と書き出します。
リストのファクトリ関数
リストのファクトリ関数として listOf、arrayLisfOf、linkedListOf があります。どれも引数は可変長のジェネリック型 Tを取ります。listOf はイミュータブルなリストを返します(型は jet.List<out T>)*4。arrayListOf と linkedListOf はそれぞれ java.util.ArrayList と java.util.LinkedList のインスタンスを生成し、返します。これらはご存知の通りミュータブルです。
val ints : jet.List<Int> = listOf(1, 2, 3) val nums : jet.List<Number> = ints println(nums[0]) // 1 println(nums.size) // 3
リストの要素には、配列のように角括弧の中にインデックスを指定する形式でアクセスすることができます。また、プロパティ size により要素数を取得することができます。
リストの要素を判定
リストや配列には関数 all と any という便利な関数が用意されています。それぞれ引数には関数を取ります。その型は ( T ) -> Boolean で、その引数にはリストの要素が順に与えられていきます。このように Boolean を返す関数リテラルのことを述語と呼ぶことがあります。
リストの関数 all を呼び出したとき、与えた述語の評価結果がすべての要素に対して true の場合に限り、all は true を返します。それ以外の場合は false を返します。逆に関数 any は、述語の評価結果が1度でも true なら true を返し、それ以外の場合に false を返します。
val langs = listOf("java", "kotlin", "groovy") val a = langs.any { it.size >= 8 } println(a) // false val b = langs.all { it.toCharArray().all { it.isLowerCase() } } println(b) // true
何か面白いことをして新しいリストを得る
関数 filter はその名前の通り、リストの各要素を任意のルールでフィルタリングして新たなリストを返す関数です。関数 map はリストの各要素を取り、なんらかの変換を施した新しい要素から構成されるリストを返す関数です。
val ints = listOf(1, 2, 3, 4, 5) val filteredInts = ints.filter { it % 2 == 0 } println(filteredInts) // [2, 4] val mappedInts = ints.map { it * 2 } println(mappedInts) // [2, 4, 6, 8, 10]
畳み込み関数
畳み込み関数は、リストの各要素を1つの値にまとめたいときに使用します。例えばInt型リストの各要素の総和を求めたい場合などです。畳み込んでいく順番は結果に影響しますので、左(リストの先頭)からの畳み込みには関数 fold を、右からの畳み込みには関数 foldRight を使用します。 関数 fold のとる引数は2つです。1つ目が初期値です。2つ目が関数で型は (R, T) -> Tで、1つ目の引数は現時点での畳み込み結果、2つ目の引数はリストの要素です。関数 foldRight の取る引数は fold と数と種類は同じですが、順番が逆です。
val words = "i live in tokyo".split(' ') val leftResult = words.fold("") { (a : String, e : String) -> "$a $e" } println(leftResult) // i live in tokyo val rightResult = words.foldRight("") { (e : String, a : String) -> "$a $e" } println(rightResult) // tokyo in live i
マップ
マップを生成するには hashMapOf、sortedMapOf、linkedMapOf といった関数が用意されています。引数には可変長のペアを取ります。ペアはクラス Pair で表現される2つの値を持つことができるコンテナです。ペアは次のように生成することができます。
val pair = Pair("key", "value")
しかし特にマップに渡すようなペアである場合は関数 to を使用したほうがすっきりと記述することができます。
val map = hashMapOf("key" to "value")
このマップに対して、キーから値を取得したい場合やキーに対して値を設定したい場合は連想配列ライクに記述することができます。
println(map["key"]) // value
関数の事前検証、事後検証用関数 2012-12-28追記
プログラム(具体的には関数など)が正しく動作するために満たされるべき条件(仕様)をコード中に記述し、間違いを防ごうとする手法を契約による設計と言ったりします。簡単に言えば、関数が正しく動く条件が整っているか(オブジェクトの状態や引数が適切であるか)、関数は正しく動作を終えたか(関数呼び出しにより期待する状態を得られたのか)などの条件をコードで表現します。
具体例を見るとシンプルなアイデアであることがわかります。クラス Rectangle は長方形を表現しているクラスです。コンストラクタは長方形の幅と高さを引数として受け取ります。幅と高さは0より大きい値を期待します。もし0以下の値が渡されたら、不正な状態の長方形オブジェクトを生成することになってしまいます。それを防ぐためのコードを次に示します。
class Rectangle(width : Double, height : Double) { val width : Double val height : Double { require(width > 0.0) require(height > 0.0, "height should be greater than 0, but actual it's $height.") this.width = width this.height = height } } fun main(args : Array) { val a = Rectangle(4.0, 3.0) val b = Rectangle(1.0, 0.0) // throw EXCEPTION!!! }
関数 require は第1引数にBoolean値を取ります。この値が false の場合、例外 IllegalArgumentException を投げます。例外へ渡すメッセージを第2引数に指定することもできます。実行時例外を投げることは乱暴のような気もしますが「誤りはすぐに知らせる」という原則に基づけば理に適っています*5。ちなみに、第2引数には例外メッセージとして、文字列を返す関数リテラルを指定することもできます。単に文字列を指定するバージョンとの違いは、評価のタイミングです。関数リテラルを渡すバージョンは例外を投げる直前に評価されます(文字列を渡すバージョンは、常に評価される)。
標準ライブラリが提供するこのような関数を以下の表にまとめます。
関数 | 対応する例外 |
---|---|
assert | AssertionError |
require | IllegalArgumentException |
requireNotNull | IllegalArgumentException |
check | IllegalStateException |
まとめと次回予告
今日はKotlinにおけるパッケージと標準ライブラリの提供する一部の機能について紹介しました。パッケージはJavaのそれと似ていますが、ところどころ異なるので注意が必要です。標準ライブラリはまだまだ紹介していないクラスや関数がたくさんあるので、明日以降もちょいちょい紹介していきたいと思います。
明日はまだ紹介していないオブジェクト指向に関する機能についてお話ししたいと思います。
日記
最近、昼食はラーメンばっかり...。
*1:Kotlinにはモジュールという概念があり、それはビルド単位です。そのビルドスクリプト内で事前にインポートするということができるようです。このモジュールという機能は現在、再設計中のようです。
*2:パッケージ jet内のクラス は正確には標準ライブラリというよりも組込みクラスのような感じがします。ただしパッケージ jet 内のクラスの「拡張関数」はパッケージ kotlin 内にあります。
*3:ここの根拠となるような文書は見つかりませんでした。Kotlinの言語仕様は公開されていません(たぶん)(というかまだ確定していないだけかも)。
*4:現時点では返されるオブジェクトの実体はjava.util.ArrayListのインスタンスです。しかし、このことに依存するコードを書くべきではありません。
*5:もし実行時例外を投げず、不正な値を許可したらどうなるでしょう。ここでの誤りが形として現れるのは、コード上のどこか遠いところです。そこで初めて間違いに気付くのでデバッグは困難です。
17日目:ジェネリクス
アドベントカレンダー17日目の今日は、Kotlinのジェネリクスについてお話しします。Javaプログラマにとっては当たり前のことかも知れませんが、ジェネリクスというのは型を柔軟でかつ安全に扱おうという仕組みです。
ジェネリクスは既に親友さ、という方はKotlinの文法にのみフォーカスしていただければと思います。
ジェネリッククラス
KotlinのクラスはJavaと同様に、型引数を持つことができます。クラス内で扱う何かしらの変数の型に型引数を指定することで、その型を仮に決めておくことができます。クラスがインスタンス化されるなどのタイミングで型パラメータを与えます*1。そうすることで仮に決めていた型が実際に使用する型に置き換わります。具体例を見ましょう。次のクラスはコンストラクタから型が T であるオブジェクトを受け取ります。型 T を型引数として使用することを宣言しています。
class Container<T>(val component : T)
このクラスをインスタンス化します。
val container = Container<Int>(123)
コンストラクタを呼び出す際に、型パラメータとして Int を指定しているのがわかります。そして、コンストラクタの引数として Int型のリテラル 123 を指定しています。このタイミングで型引数 T が Int として見なされたクラス Container のインスタンスが生成されるわけです。
ちなみに上記のインスタンス生成で、型パラメータとコンストラクタのパラメータの型は同じですので、同じ情報を2ヶ所に記述していることになり冗長です。この場合Kotlinコンパイラは型推論をしてくれるので、次のように無駄なく記述できます。
val container = Container(123) val component : Int = container.component println(component) // 123
さらに生成したオブジェクトからプロパティ component の値を取得しています。component はクラス定義の段階では型が確定していませんでしたが、インスタンス化のときに型が確定したので、上記の例では Int型の値として受け取っているのがわかると思います。
で、何が嬉しいの?
例で作成したクラス Container は型パラメータに任意の型を指定することで、あらゆる型のオブジェクトのコンテナとして機能するようになりました。まぁそれはいい。それのどこが便利なのか。次のようなコードでも同じことができるのでは?
class Container2(val component : Any) fun main(args : Array<String>) { val container = Container2(123) val component : Int = container.component as Int println(component) // 123 }
クラス Any はKotlinのすべてのクラスのスーパクラスとなるクラスです。Java の java.lang.Object のような存在です。なるほど。クラス Container2 のコンストラクタで受け取るオブジェクトの型を Any にすれば、あらゆる型に対応できますね。事実、このコードはコンパイルも成功しますし、実行結果も期待通りです。
しかし待ってください!見慣れないキーワードが登場しました。キーワード as です。これはキャスト(型変換)のためのキーワードです。component の型が Any なので Int として扱うにはキャストが必要です。これは型安全ではありません*2!次のようなコードを簡単に書けてしまいます。キャストの失敗によりプログラムはクラッシュします。
//注意!!! 例外投げます val container = Container(123) val component = container.component as String
このように、ジェネリクスは型を柔軟に扱うだけでなく、型安全性も提供してくれる強力な機能なのです。
こんなジェネリクスもあるよ
2つ以上の型引数を持ったクラス
型引数を複数個持つことも可能です。型引数をカンマで区切って宣言するだけです。
class Pair<T, U>(val component1 : T, val component2 : U)
コレクションはジェネリッククラス
標準ライブラリはコレクションクラスをたくさん提供しています。コレクションクラスは、複数のオブジェクトをまとめて管理することに責任があり、管理しているオブジェクトの型には関心を持ちません。それらの型に自由度を持たせるためジェネリッククラスとして定義されています。
一番簡単な例は、配列であるクラス Arrayです。関数 main の唯一の引数のクラスです。
ジェネリック関数
クラスと同様に、関数も型引数を持つことができます。
fun <T> head(array : Array<T>) = array[0] fun last<T>(array : Array<T>) = array[array.size - 1]
関数 head と last、書き方が若干異なりますが、どちらも文法的にはOKらしいです。
今日はここまで
ジェネリクスの基本的なことは以上です。ここからは使用頻度があまり高くないジェネリクスの機能について見て行きます。
ジェネリクスはもうお腹いっぱいかな、という方は「まとめと次回予告」に飛んでいただいてOKです。
上限制約
型引数に対して制約を設けることができます。型引数に対して任意の型を指定できますが、制約によって指定できる型を絞ることができます。
上限制約によって、型階層の上限による型引数の制限ができます。Javaでは <型引数 extends 上限型> という風に書くアレです。Kotlinでは extends の代わりにコロンを使用するだけです。ひとつの型引数が複数の上限を持つ場合は where 節で区切ります。
trait TraitA trait TraitB trait TraitC class NiceClass() where T : TraitB, T : TraitC
分散
まず次の問題のあるJavaコードを見て、どこが悪いのか考えてみましょう。
// Java final Integer ints = {1, 2, 3}; final Number nums = ints; nums[0] = Double.valueOf(0.1);
このコードはコンパイルに成功します。しかし実行すると3行目で実行時例外を投げます。3行目だけを見ると、悪くはなさそうです。DoubleをNumberと見なすことが可能だからです。2行目を見ると、微妙ですね。2行目をコンパイル可能にしたばっかりに、3行目のような記述を許し、その結果例外を投げてしまいます。これを禁止すれば万事解決、というわけにもいきません。IntegerをNumberと見なしたい場合も少なくないからです。
では、上記のコードをもう一度Javaで、今度はジェネリクスを使って表現してみます。
//Java final List<Integer> ints = Arrays.asList(1, 2, 3); final List<Number> nums = ints;
これは2行目でコンパイルエラーとなります。配列のときと違い、List<Integer>をList<Number>と見なしてくれないようです。しかし、2行目を次のように書き直すとコンパイルを通ります。
// Java final List<? extends Number> nums = ints;
このように、型パラメータの継承関係を受け入れたり、拒否したりするような操作や機能のことを分散(variance)と言います*3。
用語を整理しておきましょう。
不変(invariance) | 型パラメータ間の継承関係を考慮しません。つまりList<Integer>とList<Number>は別物であると見なします。 |
共変(covariant) | 型パラメータ間の継承関係を考慮します。Javaの配列は共変です。また、List<Integer>はList<? extends Number>となることができます。 |
反変(contravariant) | 型パラメータ間の継承関係を考慮します。List<Object>はList<? super Number>となることができます。 |
分散の型安全性
Javaの配列が共変で、型安全でないことがわかりました。ジェネリクスはデフォルトで不変であることによって型を安全に保ちますが、共変や反変になっても型安全を貫きます。そのための工夫は、言葉で説明するよりもコードを交えて説明した方がわかりやすいと思いますので、次のセクションで使用箇所分散と共にお話しします。
使用箇所分散(型プロジェクション)
上記のJavaの例のように、Kotlinでも型パラメータに共変性または反変性を指定することができます。その指定をジェネリッククラスを使用する場所(コード上)で行うため、これを使用箇所分散と呼びます*4。
例を示すために次のようなジェネリッククラスを定義しました。
class Container<T>(var component : T?)
このクラスを使った例を示します。特に何も指定していないので、不変です。つまり次のコードはコンパイルを通りません。
val int : Container<Int> = Container(777) val num : Container<Number> = int // compile ERROR!!!
では、変数 num の型を共変にします。共変性を指定するにはアノテーション out を使用します。
val num : Container<out Number> = int
これはコンパイルを通ります。通りますが、Javaの配列のような悲劇を思い出しますね。つまり、num はContainer<Number> ですが、その実体は Container<Int> なので、 component に例えば Double型の値をセットしようとすると実行時例外が起こります。安心してください。前述しましたが、ジェネリクスは安全です。この場合、num を介してcomponent の値はセットできません。仮に num.component = 5 のようなコードを書いてもコンパイルに文句を言われるだけです。一般的に言うと、out 指定された型パラメータを持つ変数に対する代入ができなくなります*5。値を取得するだけなら安全なので、可能です。つまり out 指定すると、その型を持った変数に対しては読み取り専用となります。読み取り専用、という意味の「out」らしいです。
次に反変の例を見てみましょう。反変性はアノテーション in によって指定します。お気づきかも知れませんが、in 指定された型の変数に対しては書き込み専用となります。書き込み専用という意味の「in」ですねー。
val num : Container<Number> = Container(5.0) val int : Container<in Int> = num
正確には、値を取得することもできます。が、取得する際に、その型は Any? です。というのは、<in Int>は<Number>でも<Any>でも受け付けるため、上限がありません。そのため、取得する際の型はすべてのルートである(しかもNULL許容型) Any? です。
スタープロジェクション
使用箇所分散においてスタープロジェクションと呼ばれる、Javaの非境界ワイルドカードに相当する記法があります。型パラメータ指定部分に * を記述するだけです。これは <out Any?>の構文糖衣に過ぎません。
宣言箇所分散
使用箇所分散は、Javaでも可能でした。次に紹介するのは宣言箇所分散です。これはJavaにはない機能です。使用箇所分散に対して型引数の宣言箇所で不変、共変、反変を指定するのでこのような呼び方をします
今まで使用してきたクラス Container のプロパティ component をイミュータブル(変更不可)にします。
class Container<T>(val component : T)
val 宣言したのでインスタンス生成後、component を変更することはできません。ここで共変の性質を思い出してください。指定された型パラメータに対する変数は読み取り専用でした。つまり変更不可(読み取り専用)となった component に対して宣言箇所分散による共変の指定は特に不都合を引き起こすものではありません。むしろ Container を柔軟に扱えるようになります。
class Container<out T>(val component : T) fun main(args : Array<String>) { val int : Container<Int> = Container(123) val num : Container<Number> = int val t = num.component num.component = t // compile ERROR!!! }
変数 num を介してプロパティ component に値をセットしようとしていますが、この部分はコンパイルエラーとなります。out 指定されていることもありますが、それ以前に val 宣言されているからです。
まとめと次回予告
今日はジェネリクスについて、その性質と特長、Kotlinにおける文法を学びました。クラスや関数は型引数を取ることでジェネリッククラス(あるいはジェネリック関数)になることができます。ジェネリッククラスにより型を柔軟かつ安全に扱うことができます。
明日はKotlinの標準ライブラリが提供する便利なクラスや関数をいくつか紹介します。
日記
昨日は暖かかったです。午後は散歩しました。
16日目:ぬるぽとの別れ
今日は、面倒な奴だけど憎めない、そんなNullPointerExceptionとの別れの日です。KotlinにはNPEを起こさない仕組みが備わっています。それが今日取り上げる機能、NULL安全です。
nullをセットできない型
Javaでは、すべての参照型変数がnullとなることができるので、NPEが発生するリスクを常に持っています。Kotlinの型システムは、nullという状態を取り得る型と、取り得ない型を明確に区別します。これがNULL安全を実現する仕組みです。前者をNULL許容型、後者を非NULL型と言います。
コード上では、非NULL型は通常の型宣言で表し、NULL許容型は型名の後に?を付けることで表現します。次のコードは非NULL型の変数にnullをセットしようしていますがコンパイルエラーとなります。
val str : String = null
熟考の末、この変数がnullを取り得るべきであると感じたなら、型名の後に?を付けて、コンパイラにNULL許容型であることを伝えます。次のコードは上手くコンパイルできます。
val str : String? = null
NULL許容型へのアクセスは慎重に
非NULL型の変数は絶対にnullではないということが保証されているので安心ですね。しかし、NULL許容型の変数はnullを取り得るので、そこにアクセスを試みればNPEが起こるのではないかと心配になります。安心してください。NULL許容型の変数には簡単にはアクセスできないようになっています。次のコードはNULL許容型の文字列 str の文字数を取得しようとしています。str には適切に文字列が設定されているので null ではありませんが、このコードはコンパイルに失敗します。
val str : String? = "Advent Calendar" val length = str.length
では、どうやってNULL許容型の変数にアクセスすればいいのでしょうか。
その1:NULLチェック
方法はいくつかありますが、まずNULLチェックによる方法が挙げられます。NPEが起こらないようにアクセス対象の変数が null でないかを if を使ってチェックします。
val str : String? = "Advent Calendar" val length = if(str != null) { str.length } else { null } println(length) // 17
if で str が null でないことを確認していることをコンパイラは知っているので、そのブロック内でのみ str が指すオブジェクトにアクセスできます。ただし str が今回の場合のように val 宣言されていることが前提です。var 宣言の変数は常に null になる機会をもっているため、この方法ではアクセスできません。
その2:安全呼び出し
2つ目の方法は安全呼び出しによるものです。通常、変数を介してオブジェクトのメンバにアクセスするためには*1変数名とメンバ名の間にドットを置きますが、安全呼び出しでは、ドットの代わりに?.を使用します。something?.do()という具合に。安全呼び出しは、var宣言されている変数でも使用可能です。
var str : String? = "Advent Calendar" val length = str?.length
もし、null 状態の変数に対して安全呼び出しを行うとどうなるのでしょうか。そんな場合でもNPEは発生しません。単に null を返すだけです。
var str : String? = null val length = str?.length println(length == null) // true
これは便利です。安全呼び出しチェーンを形成した場合でも、途中で失敗してプログラムがクラッシュするという問題はありません。結局、nullが返されるだけです。
val str : String? = null val length = str?.trim()?.length println(length == null) // true
その3:演算子 !!
最後に挙げる、この方法はぬるぽフェチのための方法です。NULL許容型の変数の後に演算子 !! を記述すると、その変数の非NULL型バージョンを返します。しかし、変数が null である場合、NPEを投げます。
val hoge : String? = "hoge" println(hoge!!.toUpperCase()) // HOGE val fuga : String? = null println(fuga!!.toUpperCase()) // throw NPE!!!
変数 fuga に演算子 !! を適用した瞬間にNPEがスローされプログラムはクラッシュします。3つ目のこの方法を検討するときは注意が必要です。
おまけ:エルビス演算子
NULL許容型について次のようなコードをよく書くことがあるかも知れません。
val r = if(hoge != null) hoge.toString() else "hoge is null."
つまり、変数が null でない場合は関数を呼び出してその結果を返すが、null である場合はそのとき専用の値を返すような場合です。この場合にはエルビス演算子が便利です。?: がその演算子です。上記のコードをエルビス演算子を用いて記述し直すと次のようになります。
val r = hoge?.toString() ?: "hoge is null."
エルビス演算子の左辺が null でなければ、そのままその値を返しますが、null である場合に限り右辺を返す、という演算子です。
名前の由来は横から見ると、かのロック歌手のエルビス・プレスリーに見えるからです。
おまけ:Kotlinにおける同値比較
2種類の同値比較があります。両オブジェクトが値として等しいかどうかを調べる比較と、両ポインタが同一のオブジェクトを指しているかどうかを調べる比較です。
前者の方法で比較するには演算子 == を使用します(Javaとは違うことに注意してください)。後者の方法で比較するには関数 identityEquals を使用します。
val a : Int = 128 val b : Int = 128 println(a == b) // true println(a identityEquals b) // false
変数 a と b には同じ数値を代入しています。そのため == による比較では true が返されますが、オブジェクト的には別物なので identityEquals による比較では false が返されています。
おまけのおまけ
余談ですが、上記の例で128を選んだのには理由があります。いや、128以外でもいいのですが、-128から127の範囲内だと都合が悪い理由がありました。Int型の値は基本的にjava.lang.Integerの値です。メソッド Integer.valueOf によって整数のインスタンスを取得します。java.lang.Integerでは、-128から127のIntegerインスタンスをプールしており、オブジェクトを使い回しています(インスタンス生成のコストを避けるため)。そのため、上記の例で-128から127の範囲内の値で実行してみると identityEquals で比較した際にも true を返します。
さらにもっとおまけですが、Kotlinコンパイラは非NULL型Intの場合にはIntegerを使用せず、プリミティブ型の int を使用します。オブジェクトが必要な場合には、必要になった場所で Integer.valueOf を使ってオブジェクトを取得します。
以上を踏まえて、面白い実験をします。次のコードの結果を予想できますか?
fun main(args : Array) { val i : Int = 128 val j : Int = i println(i identityEquals j) val a : Int? = 128 val b : Int? = a println(a identityEquals b) }
なんと1回目の比較では false が、2回目の比較では true が返されます。変数 i と j はプリミティブ型の int としてコンパイルされ128がそれぞれに代入されます。identityEquals により比較されるタイミングでそれらの変数を参照型に変換します。そこで Integer.valueOf が使用されます。128という数値はオブジェクトプールに存在しないので、毎回インスタンスを生成することになります。したがって、identityEquals による比較では別物という判断が下ります。
変数 a と b はNULL許容型なのでint型ではなく Integer型としてコンパイルされます。a に128を代入しようとしているのでInteger.valueOf(128) によりインスタンスが生成されます。そして、その参照を b にコピーしています。ということは、当然ながら identityEquals による比較で同一と見なされます。
コンパイラのバージョンが上がれば、このような挙動はなくなるのかな?
まとめと次回予告
今日はNULL安全という仕組みを見てきました。nullという状態を取り得る型とそうでない型があることと、それぞれをNULL許容型、非NULL型と呼ぶことを学びました。NULL許容型変数にアクセスする際には、特別なやり方が必要です。NULLチェックを突破したり、安全呼び出しを行ったり、もしくはちょっと危険な演算子 !! を使用したりします。最後に挙げた方法を除けば、これらはNullPointerExceptionの恐怖から我々プログラマを解放してくれます。そして、おまけとしてエルビス演算子と、Kotlinにおける同値比較について学びました。
明日もコードを安全に保ってくれる素晴らしい機能についてお話しします。型の安全を保証しつつ、柔軟に扱えるジェネリクスの登場です。お楽しみに!
日記
投票行きます。
*1:ちょっと回りくどい言い方に聞こえるかも知れません。今日はは変数にフォーカスして話をしているからです。nullという状態を持ち得るのは「変数」であってオブジェクトではありません。変数はオブジェクトへのポインタを格納していることを考えればしっくりきますね。
15日目:トレンドなトレイト
アドベントカレンダー15日目の今日はトレイトという機能に注目したいと思います。トレイトとは実装を持ったインタフェースのようなものです。これはJVM言語のScala(まさにトレイトという名前の機能)や、Java SE 8(予定)にもある機能です。インタフェースが実装を持つとことで便利なこともありますが、問題もあるのではないかと心配になりますね。そこらへんを見て行きましょう。
抽象クラス
トレイトの話題の前に抽象クラスを紹介します。抽象クラス、Javaプログラマならご存知ですね。抽象関数を持つクラスのことを抽象クラスと言い、そのクラスはインスタンス化できません。サブクラスを作成するために使用します。抽象関数とは実装を持たず、関数シグネチャのみを宣言した関数です。具体的な実装はサブクラスで定義します。ポリモーフィズム(多態性)を実現するための重要な仕組みです。
抽象クラスの概念のおさらいはここまでにして、実際にKotlinコードを見てみましょう。クラスや関数を抽象として宣言するにはアノテーション abstract を伴う必要があります。
abstract class Greeter() { abstract fun greet(name : String) }
抽象関数は実装を持ってはいけません。また、抽象関数を持つクラスは必ず抽象クラスとして定義します。まぁここらへんはJavaと同じですね。
抽象クラスは継承して使うわけですが、アノテーション open が付いていなくても継承することが可能です(抽象関数もopenでなくてもオーバライド可能です)。継承の例を示します。
class GreeterImpl() : Greeter() { override fun greet(name : String) { println("Hello, $name!") } }
トレイト
トレイト(trait)は、Javaで言うところのインタフェースのようなものです。抽象関数を持ち、クラスは複数のトレイトを実装*1することができます。そしてJavaのインタフェースとの最大の違いは、トレイトはデフォルトの実装として具象関数を持つことができます*2。
トレイトの定義
トレイトを定義するには、キーワード trait を使用します。それに続けてトレイト名を指定します。クラスに似ている宣言ですが、トレイトはコンストラクタを持ちません。そのため、トレイト名の後にプライマリ・コンストラクタを表す括弧はあってはなりません。
trait Greeter { fun greet(name : String) { println("Hello, $name!") } fun greetInLoudVoice(name : String) }
この例では関数 greet は実装を持っています。関数 greetInLoudVoice は実装なしなので抽象関数です。トレイトで宣言された実装を持たない関数はデフォルトで abstract です。また、具象関数はデフォルトで open です。
トレイトの実装
トレイトを実装するにはクラス名とトレイト名の間にセミコロンを置きます。継承と同じですね。このトレイト Greeter を実装する例を見ます。
class GreeterImpl() : Greeter { override fun greetInLoudVoice(name : String) { println("HELLO, ${name.toUpperCase()}!!!"); } }
関数 greet をオーバライドしていないのがわかると思いますが、デフォルト実装が提供されているので、これについてコンパイラは文句を言いません。
複数のトレイトを実装(ミックスイン)
トレイトはJavaのインタフェースのように複数個を同時に実装できます。次の例は、具象関数を持った2つのトレイトを1つのクラスが実装する例です。
trait Hoge { fun foo() = "foo" } trait Fuga { fun bar() = "bar" } class NiceClass() : Hoge, Fuga
お気づきかも知れませんが、これはJavaの世界では追放されている多重継承に似ています。多重継承は、それが形成する構造の複雑さ、曖昧さなどの問題と付き合わなければならない諸刃の剣です。そのためJavaではこれを限定的に許可しています。それがインタフェースです。インタフェースは状態も実装も持たないので安全です。ではトレイトはどうでしょうか。トレイトは状態を持ちません。つまり純粋な具象関数定義を促します。また、曖昧さの問題は次に紹介するオーバライドのルールによって解決しています。
オーバライドのルール
多重継承が可能であると、関数名の衝突の扱いが重要になります。次のコードは特に問題はありません。
trait Hoge { fun piyo() = "hoge" } trait Fuga { fun piyo() } class NiceClass() : Hoge, Fuga fun main(args : Array<String>) { println(NiceClass().piyo()) // hoge }
同一シグネチャの関数を持った2つのトレイトを実装していますが、一方は具象関数であるのに対してもう一方は抽象関数です。そのため NiceClass のインスタンスに対して関数 piyo を呼び出したとき、トレイト Hoge の提供する実装が採用されるのは明らかです。では、次のようにトレイト Fuga 側でも実装を持ったときにはどうなるでしょうか。
trait Hoge { fun piyo() = "hoge" } trait Fuga { fun piyo() = "fuga" } class NiceClass() : Hoge, Fuga
これはコンパイルエラーになります。NiceClass で piyo を実装してくれ、それは複数の実装を持ってるから!とコンパイルは文句を言ってきます。もしこのコードをコンパイラが受け入れていたら、NiceClass の piyo の実装について曖昧さが生じてしまいます。では、素直にコードを修正します。
class NiceClass() : Hoge, Fuga { override fun piyo() = "nice" }
これでコンパイルは通ります。NiceClass の piyo を呼び出すと "nice" を返します。
デフォルト実装を呼び出す
オーバライド前の関数を使用したい場合があるでしょう。そんなときは次のようにすればオーバライド前の関数を呼び出せます。
trait Hoge { fun piyo() = "hoge" } class NiceClass() : Hoge { override fun piyo() = super.piyo() }
Javaと同じようにキーワード super 、ドット、関数名で簡単に呼び出せます。
継承した関数の名前が衝突した場合はどのようにしてオーバライド前の実装を呼び出せるのでしょうか。Kotlinにはこの解決策も用意されています。
trait Hoge { fun piyo() = "hoge" } trait Fuga { fun piyo() = "fuga" } class NiceClass() : Hoge, Fuga { override fun piyo() = "${super<Hoge>.piyo()} & ${super<Fuga>.piyo()}" } fun main(args : Array<String>) { println(NiceClass().piyo()) // hoge & fuga }
superに続けて<型>を指定することで曖昧さを排除することに成功します。
委譲はいいじょう(いいぞ〜)!
継承は強力な機能ですが、サブクラスはスーパクラスの実装に依存してしまう危険性があります。そこで、継承を避けて委譲(デリゲーション)を使用することでクラスのカプセル化が保たれます。Javaの世界でこのテクニックは一般的*3ですが、どうしても定型コードが多くなり、見通しが悪くなってしまいがちです。
Kotlinでは言語レベルでこの問題を解決します。委譲を簡単に実現するための機能が言語に組み込まれています。
trait Greeter { fun greet(name : String) } class GreeterImpl() : Greeter { override fun greet(name : String) { println("Hello, $name!") } } class Person(g : Greeter) : Greeter by g
トレイト Greeter とその実装クラス GreeterImpl、それからクラス Person を定義しました。人には挨拶してもらいたいところなので、GreeterImpl を継承するのもアリなんですが、そうしてしまうと全人類の挨拶が GreeterImpl に支配されてしまいます。上記の例では Greeter型のオブジェクトをPerson が持つことになります。いわゆる has-a 関係というやつです。
肝心なのはPersonのクラス宣言で、継承のような書き方をしている部分がありますが、これが自動で委譲コードが生成される仕組みです。これによりPersonがGreeterになっているかのように見えます。つまり、is-a 関係です。継承のようにPersonはGreeterが持っている関数と同一シグネチャの関数を持つことになります。実際にはその関数内では単にGreeter型オブジェクトの g に処理を委譲しているに過ぎません。
ぐだぐだ言ってきましたが、重要なのは、PersonはGreeterを継承しているかのように見えますし、実際、そのように振る舞います。継承と違うポイントは、型と実装が分離していること。そして具象クラス(例ではGreeterImpl)の内部状態にはアクセスできないことと、具象クラスは静的に決定されるものではないことです。
最後にクラス Person の使用例を示します。
trait Greeter { fun greet(name : String) } class GreeterImpl() : Greeter { val something : String = "hogehoge" override fun greet(name : String) { println("Hello, $name!") } } class AnotherGreeterImpl() : Greeter { override fun greet(name : String) { println("Hi, $name!") } } class Person(g : Greeter) : Greeter by g fun main(args : Array<String>) { val foo : Person = Person(GreeterImpl()) foo.greet("Delegation") // Hello, Delegation! // println(foo.something) // compile ERROR!!! val bar = Person(AnotherGreeterImpl()) bar.greet("Kotlin") // Hi, Kotlin! }
まとめと次回予告
今日は抽象クラスから始めて、トレイト、委譲について学びました。抽象クラスやトレイトでポリモーフィズムを実現します。抽象クラスはJavaのそれとほぼ同じです。トレイトは具象関数を持てるインタフェースと言いましたが、抽象クラスからインスタンス変数の存在を消し去ったものと言えるでしょう。トレイトは複数実装(ミックスイン)することが可能です。その際に起こる名前衝突などの曖昧さを排除するためのオーバライドのルールも学びました。最後にトレイトを上手く使うテクニックとして委譲を紹介しました。
明日はNULL安全についてお話しします。NULL安全によりKotlinでは基本的にNullPointerExceptionは起こりません。この例外を見るとげんなりしますよね。明日、それを倒す方法を伝授します。
日記
今日は寒いし雨降ってるっぽいから外出したくないなぁ(´・ω・`)
14日目:継承とオーバライドの約束
Kotlinにも継承の仕組みが備わっています。継承により、上位のクラス(スーパクラス)の型のサブセットとなることができ、さらにスーパクラスの実装を受け継ぐことになります。再利用性を高める重要な機能について今日は見ていきます。
継承
継承は、上位のクラスを基に、派生したクラス(サブクラス)を簡単に作成する機能です。Kotlinの継承は、Javaのそれと同じです。が、ところどころコード記述や細かいルールが異なります。
基本は継承できない
Kotlinではクラスを定義した際、デフォルトで継承を禁止しています。Javaで言うところのfinalクラスです。この慎重な設計思想はEffective Java 第2版の項目17*1に基づいています。そのため、継承を許可するクラスを作成するには、クラスにアノテーション openを付けて定義します。
open class SuperClass(val name : String)
継承する
では次に、上記のクラス SuperClass を継承したクラス SubClass を定義しましょう。やり方は簡単です。クラス定義の際、自身のプライマリ・コンストラクタに続けて、コロンとスーパクラスの名前とプライマリ・コンストラクタを記述します。
class SubClass(name : String) : SuperClass(name)
スーパクラスのプライマリ・コンストラクタにはパラメータをセットする必要があります。上記の例では、サブクラスのプライマリ・コンストラクタで受け取った値をスーパクラスのプライマリ・コンストラクタに転送しています。
オーバライド
継承により、スーパクラスの持っているインスタンス関数はサブクラスにコピーされますが、その関数をサブクラス側で上書きして実装を変更することをオーバライドと言います。オーバライドもJavaよりも厳しいルールとなっています。
open class SuperClass(val name : String) { open fun printName() { println("$name") } } class SubClass(name : String) : SuperClass(name) { override fun printName() { println("!!! $name !!!") } }
その他、細かいルールがあります。
- open指定していないクラスの関数にはアノテーション openを付けられない(関数をopenにしたところで意味がないから)
- オーバライドした関数にアノテーション openを付けられない(既にopen状態であるから)
- オーバライドした関数の再オーバライドを許可したくない場合はアノテーション finalを付ける
まとめと次回予告
今日はKotlinにおける継承について勉強しました。Javaと同様にクラスは、他のクラスを継承することができます。また、スーパクラスが持つ関数をオーバライドすることができます。ただし、継承やオーバライドをするための制限がJavaよりも厳しいです。それは足枷ではなく、不注意な設計判断によってクラスのカプセル化が損なわれてしまう事態を回避する工夫です。
明日はトレイトと呼ばれるJavaで言うインタフェースのようなものの紹介をします。また、それに関連して、オーバライドのルールをもう少しだけ紹介します。トレイトを利用した委譲パターンの簡単な実装法についてもお話しします。
日記
Scala勉強中。面白いけどやっぱり複雑という印象。
*1:項目17 「継承のために設計および文書化する、でなければ継承を禁止する」
13日目:プロパティとフィールド
一昨日はクラスの話をしました。クラスは、そのメンバとして関数とプロパティを持つことができます。今日はプロパティについてお話ししたいと思います。
まずはJavaの慣例のおさらい
Javaではpublicなフィールドを避けるべきであるとされています。メソッド呼び出しのオーバヘッドが無視できない状況を除けば、すべてのフィールドをprivateにし、publicなメソッドを介してフィールドにアクセスすべきです。フィールドを非公開にすることによって適切にオブジェクトはカプセル化されます。
しかし、setterやgetterの呼び出しはもちろんのこと、クラスに逐一定義していくのは非常に退屈で面倒な作業ですし、定型コードであふれ返った見通しの悪いクラスが出来てしまいます。これを上手く解決する仕組みであるプロパティをKotlinでは導入しています。
プロパティ
Kotlinにおけるプロパティは、一見フィールドのように見えます。
class Rectangle() { var width = 1.0 var height = 1.0 } fun main(args : Array) { val rect = Rectangle() rect.width = 2.0 println(rect.width) // 2.0 println(rect.height) // 1.0 }
プロパティを宣言すると、Kotlinコンパイラはフィールドと2つのアクセサ(getterとsetter)を内部的に作成します。しかし通常、フィールドに直接アクセスすることはできず、アクセサを経由することになります。上記のコードではデフォルトのアクセサを使用して width の値を読み書きしていたのです。
プロパティによって、直接フィールドを扱っているような記述になるので直感的にプログラミングできる上、記述量も減り、可読性も向上します。
アクセサの定義
アクセサは自動生成されますが、独自のアクセサを定義することが可能です。ミュータブルなプロパティ(すなわちvarキーワードの付いたプロパティ)はgetterとsetterを持ちます。イミュータブルなプロパティ(valキーワードの付いたプロパティ)はgetterのみを持ちます。アクセサの書式は次のとおりです。
var プロパティ名 : 型 = 初期値 get() { return 返す値 } set(value) { }
型や初期値は省略が可能な場合もあります。getterはgetという名前の関数のようなものです。プロパティの型と同じ型の値を返す必要があります。単一式関数のような記述も可能です。setterはgetterと同様、関数に似ています。setterは引数のようなものを取ります。引数には任意の名前が付けられますが、valueが一般的のようです。
class Rectangle() { var width = 1.0 var height = 1.0 val area : Double get() = width * height } fun main(args : Array) { val rect = Rectangle() rect.width = 3.0 rect.height = 4.0 println(rect.area) // 12.0 }
クラス Rectangle に面積用のプロパティ area を追加しました。そして、area には独自のgetterを用意しています。area 自体は値を持ちません。getterが代わりに値を返してくれるからです。そのため area はイミュータブルであるにも関わらず初期化する必要がありません。この場合、コンパイラは area のフィールドを作成しません。
バッキング・フィールドへのアクセス
Kotlinでは基本的にプロパティを使用してインスタンスの状態にアクセスしますが、直接フィールドへアクセスしたいときもあります。ちなみに、プロパティではなく「フィールド」を強調して指すために「バッキング・フィールド」と呼んだりしています。
クラス Rectangle の例を考えましょう。width と height はミュータブルなので、外部からの変更を許可しています。このクラスの利用者がうっかりマイナスの長さの長方形を作成しないように width と height に独自のsetterを定義しましょう。
class Rectangle() { var width = 1.0 set(value) { require(value >= 0) width = value } var height = 1.0 set(value) { require(value >= 0) height = value } val area : Double get() = width * height }
関数 require を各setterで使用しています。この関数は、引数のテスト結果が false の場合に IllegalArgumentException をスローします。事前条件の検査には持ってこいの関数です。ここでは代入しようとしている値が負数でないことを検査しています。
これで上手く行きそうです。実際、コンパイルも通ります。しかし待ってください。このクラスのインスタンスの width または height に値を代入しようとするとスタックオーバフローが起こります。なぜでしょうか。width や height がプロパティであることを思い出してください。上記のsetterでは値を検査した後、その値をプロパティへセットしようとしています。すると、無限ループに陥り永遠に値をセットできない上にスタックを食いつぶしてしまいます。
ここでバッキング・フィールドの登場です。上記のsetterの中で、新しい値をプロパティではなく、バッキング・フィールドへ直接セットできれば上手く行きます。バッキング・フィールドにアクセスするには、プロパティ名の先頭に $ を付けた名前を使用します。次のコードは期待通りに動いてくれます。
class Rectangle() { var width = 1.0 set(value) { require(value >= 0) $width = value } var height = 1.0 set(value) { require(value >= 0) $height = value } val area : Double get() = width * height } fun main(args : Array) { val rect = Rectangle() rect.width = 3.0 rect.height = 4.0 println(rect.area) // 12.0 }
まとめと次回予告
今日はKotlinにおけるプロパティとフィールドの扱い方についてお話ししました。Kotlinにおいてフィールドは基本的に隠れています。代わりにプロパティを使います。プロパティはフィールドを外部に晒すことなく外部とのやり取りを実現できます。それは従来のアクセサとは違いより直感的に記述できます。アクセサはコンパイラによって自動生成されますが、独自に定義することもできます。必要があれば、バッキング・フィールドに直接アクセスすることもできます。バッキング・フィールドが存在しないプロパティを作ることもできます。
明日は、オブジェクト指向を強力にしている重要な仕組みのひとつである継承について取り挙げます。
日記
アドベントカレンダー折り返し地点です...。文章を書くのが苦手なんで、よく頑張ったなぁって感じですw
12日目:番外編!JetBrains勉強会の感想 #jbugj
Kotlin解説は1回お休みです。今日は、昨日あったJetBrainsユーザグループの勉強会の報告をします。本編とは若干逸れますが、Kotlinについてもちょっぴり触れます。
第1回JetBrainsユーザグループ勉強会
に出席しました。IntelliJ IDEAなどのIDEやツールなどで有名なJetBrains社の製品についての情報交換を主とした活動を行うユーザグループの勉強会です。
発表スライド
- WebStormとRubyMineを使ってみた #jbugj
- IntelliJ IDEA製品群、ライセンス形態について #jbugj
- Intelli j vs-eclipse-by-mike-neck #jbugj
勉強会報告エントリ
- 第一回 JetBrainsユーザーグループ に行ってきた #jbugj - j-mason's diary
- 第一回 JetBrainsユーザーグループに行ってきた #jbugj - PiyoPiyoDucky
- JetBrainsユーザーグループ #jbug を開催しました - ブログ・アンケート記入でIntelliJライセンスプレゼント - 侍ズム #samuraism
- 第一回 JetBrainsユーザーグループ #jbugjに参加してきました - @johtani の日記
動画
その他
Kotlinについての発表
感想
第1回目ということなので広く浅くという感じの内容でした。そのため、普段使わない言語のツールの話をされると若干話について行けない感がありました。しかし、JetBrains製品について「へぇ〜こういうのもあるんだ!」という発見がいくつかありました。
私はKotlinの紹介ということで、Kotlinの雰囲気をざっくりと発表させていただきました。人前で話すのはあまり得意な方じゃないので緊張してしまいました(ギャグも滑ったしw)。
懇親会も非常に盛り上がりました。たくさんの素晴らしい技術者のみなさんとお話しできてすごく楽しかったです。
この場をお借りして、主催者のイケメンこと @yusuke さんにお礼を申し上げます。ありがとうございました。また発表させてくださいヾ(*´∀`*)ノ
独り言
で、次回以降は各言語や各ツールにスコープを絞っての開催になると思います。Kotlinの回はあるのかなーと期待していますが、むしろKotlinはKotlinで国内ユーザグループを立ち上げたいです。コミュニティ運営するバイタリティはあると思いますが、スキルが...。
次回予告
明日は本編に戻り、Kotlinのプロパティとフィールドのお話をします。