Kotlinでは「プロパティには何らかの値が代入されていなければならない」という決まりがあります。
しかし例外というのは付き物です。今回はKotlinに、プロパティの初期化を「ちょっと待って!」と訴えるlateinit
とby lazy
をご紹介します。
lateinit
プロパティの値が、そのプロパティを定義した時点ではまだ用意できないという場合、lateinit
でそのプロパティの初期化を遅らせることができます。lateinit
はnull許容型の代わりに使うことができ、そのプロパティを参照するまでに必ず値が代入されることを開発者の責任で保証します。
その代わり、そのプロパティには(定義した時点での)初期化が必要無くなります。「使う時までに、どこかで値が代入されればOK」というわけです。
lateinitの例
例えばAndroidのActivity(画面表示に関するクラス)のプロパティは、onCreate
メソッドによって初期化されることがあります。ミュージックプレイヤーのコードを抜粋したものが以下です。
class MainActivity: AppCombatActivity{
private lateinit var player: MediaPlayer //この時点では初期化できない
override fun onCreate(savedInstanceState: Bundle?){
...
player = MediaPlayer.create(this,R.raw.music) //ここで初期化
}
}
2行目のplayerプロパティは、定義した時点ではまだ初期化できません。onCreate
ソッドの中で初期化される必要があるためです。
こんな風に「プロパティの値は他の場所で生成する」といった事情があるのであれば、lateinit
を使うべき場面かもしれません。
注意点とisInitialized
lateinit
は可視性修飾子の後に加えるだけで簡単に使えますが、使う際の注意点として、
- null許容型には使用不可
- リードオンリーの(valで定義した)プロパティには使用不可
- 型がIntやFloat、Booleanなどのプリミティブ型の場合、使用不可 (Charは×、StringはOK)
- 可能であれば可視性はprivateにすべき (初期化後の意図しない代入を防ぐ)
- もし初期化される前に参照されれば「UnitinitializedPropertyAccessException」(初期化されていないプロパティにアクセスした)例外エラーが送出される
といった点が挙げられます。
またlateinit
を使って初期化されるプロパティは、isInitialized
プロパティで初期化されているかどうかを調べることができます。状況に応じて使用しましょう。
Kotlin: 「例外エラーになったら…あんたの責任だかんね!」
lazy initialization
もう1つの手段がlazy initialization
です。こちらは「値が用意できない」という状況では使えません。後で初期化したほうが都合がいい場面です。例えばこんなコードがあったとします。
import java.io.File
class Music{
val artist: String = ""
fun getArt(): String{
val songData = File("musicFiles/lib.csv") //csvファイルの読み込み
.readText()
.split("\n")
.shuffled()
.first()
return songData.split(",")[0] //アーティスト名を出力
}
}
getArtメソッドは別ファイルのcsvからデータを読み込み、アーティスト名をランダムに出力します。ちなみにサンプルのcsvがこちらです。
山崎まさよし,あじさい,04:29,STEREO 2
aiko,キラキラ,05:08,まとめ Ⅱ
Bank Band with Salyu,to U,07:13,to U
Bon Jovi,Livin' On A Prayer,04:08,Tokyo Road
Taylor Swift,Style,03:51,1989
米津玄師,アイネクライネ,04:50,YANKEE
このメソッドの出力をartistプロパティに代入したいとすると、まず初期化ブロックが考えられます。init{}
の中ににこのメソッド丸ごと記述すれば、インスタンス生成時にartistプロパティは初期化されます。しかし、このcsvが5万行あればどうでしょう?
lazy initializationの例
5万行のファイルを読み込む作業は誰がどう見ても時間がかかる作業です。インスタンス生成のたびに、「参照するかどうか分からない」プロパティのために多くの時間をかけるのは馬鹿げています。
ならせめて、このプロパティを実際に使うときに初期化した方がマシではないでしょうか。
import java.io.File
class Music{
val artist by lazy{getArt()} //lazyによって初期化
private fun getArt(): String{
val songData = File("musicFiles/lib.csv")
.readText()
.split("\n")
.shuffled()
.first()
return songData.split(",")[0]
}
}
4行目はプロパティの初期化を行う処理ですが、お馴染みの“=”はありません。代わりにby lazy
を使って初期化しています。
by
このby
は「~によって」のbyです。Kotlinではこれを「Delegated Properties」(委譲プロパティ)とよんでいます。
簡単に言うと、「このプロパティは〇〇によって初期化します」という意味。
lazy
byの後ろにlazy
。これはラムダ式を取る関数です。
fun <T> lazy(initializer: () -> T): Lazy<T>
lazy
関数に渡されたラムダは、そのプロパティが初めて参照される時まで実行されません。また一度実行された後はその結果がキャッシュされ、以降はキャッシュの値がそのまま出力されます。
注意点と特徴
lazy
関数はby
と共に利用され、プロパティの初期化を、そのプロパティが初めて参照されるときまで遅延させます。特徴として、
- lazy関数は同じプロパティに対して、一度しか呼び出されない
- valプロパティでのみ使用可能
- プリミティブ型でも使用可能
といった点が挙げられます。
インスタンス生成時にプロパティに代入する値が用意できるのかできないのかでlateinit
とby lazy
を使い分けましょう。
Kotlin: 「待っててあげる。…私だってそのくらいできるわよ!」