Kotlin applyから理解するレシーバー付きラムダ
こんにちは、tkyです。
『レシーバー付きラムダ』という用語をご存知でしょうか。Kotlinインアクションとかで使われている表現です。
英語では Function literals with receiver
と言われています。
今回は apply{}
がどのように動作するのか確認しながら『レシーバー付きラムダ』何なのかというものを理解して行きたいと思います。
ラムダ式とは、スコープ関数、拡張関数といった用語の説明はしません。
apply
まず apply
をおさらいします。こんな感じでインスタンスを生成したあとにプロパティに値を設定したりとかそんな使い方をするのが一般的ですね。
class User(val name: String) { var age: Int = 0 var email: String = "" } val user = User("ticktakclock").apply { age = 10 email = "example@example.com" }
apply
の実装を見てみるとこの様になっています。
public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }
注目していただきたいのは関数の引数である block: T.() -> Unit
です。
引数にラムダを渡しているわけなのですが、 T.()
というように Tの拡張関数のような表現になっていますね。
ここで拡張関数のドキュメントを見に行きます。
以下抜粋
拡張関数を宣言するには レシーバータイプ (receiver type) を関数名の前に付ける必要があります。 次の例では、 swap 関数を MutableList<Int> に追加しています: fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' がリストに対応する this[index1] = this[index2] this[index2] = tmp } 拡張関数内での this キーワードは、レシーバオブジェクト(ドットの前に渡されたもの)に対応しています。これで、この関数を任意の MutableList<Int> からでも呼べるようになりました:
はいでました!レシーバー!
この関数を実行(受ける)ときのthisの型またはオブジェクトを レシーバー
というのですね。
swap関数をレシーバーはMutableList
block: T.() -> Unit
は拡張関数で定義しようとすると次のようになりますが、このときのthisはTですね。別の視点からみてもこの理解でおおよそあってそうです。
fun T.block() { // thisはT }
ラムダ式におけるthisはあくまでそのラムダ式を実行するオブジェクトとなりますが、 レシーバータイプを指定するとことでthisの型を指定することができるということです。
いったん apply
のドキュメントを見に行ってみましょう。
コンテキストオブジェクトは、レシーバー(this)として使用できます。
ここでもちゃんとレシーバーが出てきました。
新しい単語(コンテキストオブジェクト
)も出てきました。関数のスコープ内で使用する特定のオブジェクトのことを指しているのであっていると思いますが、その特定のオブジェクトにthisでアクセスできるということですね。
続いてラムダ式のリファレンスも見に行ってみましょう。きっと何かが書いてありそうな予感。
ありました。関数型はレシーバーを使用することで関数型のインスタンスを呼び出す事ができる、というようなことが書いてあります。
applyに戻ります。
class User(val name: String) { var age: Int = 0 var email: String = "" } val user = User("ticktakclock").apply { // applyを呼んだUserインスタンスをレシーバーとしてラムダ式を実行する age = 10 // thisはUserインスタンスなのでthis.age =10 と同じ email = "example@example.com" }
おお〜理解できるかも〜!
まとめ
- レシーバー付きラムダというのは拡張関数をラムダ式で記述したもののこと
- レシーバーというのはこの関数を実行(受ける)ときのthisの型またはオブジェクトのこと
- ドキュメントって何でも書いてあるんだなぁ
理解の仕方や調べ方の助けになったら幸いです。
LiveDataをどのレイヤー(Repository層)まで許容するか
こんにちは、tkyです。
先日、とあるcodelabをやっていて『Repository層でLiveDataって使っていいの?』と感じたことがあり。
この疑問からLiveDataを使用するレイヤーとそこから生じるメリット・デメリットなどを考察した内容をまとめておきます。
こういうことを考えながらプログラミングしてるんだなと感じていただけたらと思います。
結論から申し上げると『用法用量を守ればRepository層にLiveDataを使っても良い』になりました。
codelabs.developers.google.com
上記Codelabではkotlin coroutine Flowの使い方を学習するためにあえてそういった書き方をしているとも思いますが、後のYoutubeリンクのDevsummitでも同様の書き方が見られました。
LiveDataとは、といった事などに関することは触れません。
何を疑問視しているのか
私のLiveDataの普段の使い方はこんな感じです。特に言うことはないです。
- ViewModel <-> Repository でデータの取得
- 取得したデータをLiveDataにpostValue()する
- FragmentまたはDataBindingでLiveDataをobserveしてViewに反映する
interface BookRepository { fun fetchBooks(): List<Books> } class BookViewModel(private val bookRepository: BookRepository): ViewModel() { private val _books = MutableLiveData<List<Book>> val books: LiveData<Book> = _books init { runCatching { val books = bookRepository.fetchBooks() _books.postValue(books) } } } class BookFragment: Fragment() { fun onViewCreated() { bookViewModel.books.observe(Observer{ books -> // リスト更新 }) } }
このときのRepositoryでLiveDataを直接返すようにするのってありなの?という疑問です。
interface BookRepository { fun fetchBooks(): LiveData<List<Books>> } class BookViewModel(private val bookRepository: BookRepository) { val books: LiveData<Book> = bookRepository.fetchBooks() }
LiveDataはViewModelまでかなと思っている派
疑問を持ったということは想定と実態が異なっていたためですが、以下の理由でRepository層にLiveDataを持ってくることにためらいを持っていたために生まれたものでした。
- RepositoryのテストにAndroidライブラリの依存が入る
- LiveDataはViewにリアルタイムに通知するものだからViewに近い層で利用するほうが管理しやすそう、という先入観
逆にこの2つくらいしかなくて、普通ViewModelまでしか使われていないから程度のことしか出てこないですし、大きい否定材料にもなりません。
Repository層までLiveDataを持ってきた場合何が良さそう?
- ViewModelでデータを一次受けしないでよくなる
- _books.postValue(books) のような記述をしなくて良いということ(特にメリットとも言えない・・・)
- LiveDataが最新の値を保持するためキャッシュっぽい動きしてくれそう
逆にデメリットも考えてみました
- LiveDataは複数でObserveするのには向いてなさそう
- 2箇所でObserveしても1個しか処理しないということ
- 特定の場合のみpostValueするみたいな仕様が入ってしまう場合Repositoryにロジックが入る。設計が乱れる。
- RxならViewModel側でFilterすればRepositoryにロジックが入ることなく実装できそう
- RepositoryのテストにAndroidライブラリの依存が入るためテストしづらい
- 実際Contextは使われていないのでUnitテストで処理できますが例えばKotlin MultiPlatformProjectなどではLiveData使えない
RoomはLiveDataを返すクエリをサポートしている
冒頭にも書いたとおりFlowを導入するために敢えてRepository層までLiveDataを持ってきて書き換えるという手順をやっているのかなと思いましたが、
RoomでRepository層とDaoまでLiveData使ってるじゃん・・・!ということはダメというわけでもないのかと考えます。
気になったのはこの一文
『このパターンは、データベースに格納されているデータと同期したデータが常に UI に表示されるようにするうえで役立ちます。』
こういったユースケースを実現する場合は許容しても良いのかもしれません。(前提としてRoomもAndroidライブラリだからLiveDataが入っててもおかしくない)
Dev Summit から得た知見
以下の'18セッションで実際にRepositoryにLiveDataを扱うケースが存在しており、許容されていそうということが伺えます。
また同時にアンチパターンについても解説がありました。
- HTTPリクエストから得た巨大なデータ群にLiveDataを使うとその分がメモリにとどまる
- APIのRepositoryには使わないほうが良さそう
- LiveDataインスタンスを複数画面で共有すると意図しないUI更新が起こる可能性がある
- 都度LiveDataインスタンスを生成すれば解決しそう
- データに対してたくさんのオペレータ(filterなど)が必要ならRxを使いましょう
- ライフサイクルやUIに関係ないところにLiveDataは使わない。そのために設計していない。(もしくはRxを使いましょう)
- 1回きりのデータ(動画では 1ショットオペレーションと言っている)ならCoroutinesを使いましょう(もしくはRxを使いましょう)
以下の'19セッションでCoroutines FlowでLiveDataの扱い方を解説していました。
- 去年('18)話したRepository層でLiveDataを使えることは話したが、LiveDataはRxのようにリアクティブストリームビルダーとして設計されていない。
- LiveDataのかわりにCoroutines Flowでできるようになる
確かにFlowの場合Androidライブラリ関係ないのでMultiPlatformProject考慮しても特に問題なさそうだなと思いました。
まとめ
- LiveDataはUIに紐づくことは忘れずに
- データが1回きりの表示(1 shot operation)ならCoroutinesで良い、都度変わる(observable)ならLiveDataでも良い。
- 永続化したデータの内容とUIの表示内容を常に同期したいときはRepositoryでLiveDataを返すのはありかも
- 巨大なデータクラスをLiveDataで扱うとメモリ圧迫につながるので注意
- 用法用量を守ればRepository層にLiveDataを使っても良いけどCoroutines Flowで同じことができる
- ただしFlowの細かい部分はまだExperimentalなので完全に移行できるかと言われると微妙
何を実現したいのかによってどの技術を使うか正しく選択できるようになるのが今の自分には必要だなと思います。日々勉強ですね。
参考文献
ViewModelのテストを書く
こんにちは、tkyです。
テスト、書いてますか? 僕はぼちぼち書いてます。
今日はViewModelのテストを書いてみたいと思います。
サンプリリポジトリです。サンプルではGitHubのAPIにアクセスする簡単なサンプルにテストを書いてみた感じです。
本稿ではこの2つを扱いたいと思います。
- LiveDataのアサート
- Coroutinesを含むテスト
モックライブラリにはMockKを使っています。mockkの使い方については触れません。
LiveDataのアサート
例えば『View(ActivityやFragment)にデータを渡せているか』をテストしたいときに用います。
val repository = mockk<ProjectRepository>() coEvery { repository.getProjects(any()) } returns emptyList() val observer = mockk<Observer<List<Project>>>(relaxed = true) val viewModel = ProjectListViewModel(repository, testDispatcherProvider) viewModel.projects.observeForever(observer) viewModel.onResume() verify { observer.onChanged(emptyList()) }
ポイントはここ。mockkで作成したObserverをLiveDataに設定します。
val observer = mockk<Observer<List<Project>>>(relaxed = true) viewModel.projects.observeForever(observer)
LiveDataに値が設定されたときにobserver.onChange()が呼ばれることを利用してこのonChange()をアサートします。
verify { observer.onChanged(emptyList()) }
Coroutinesを含むテスト
ほぼCoroutinesを使うケースになるかと思いますが、
- CoroutinesTestRuleを作成
- TestCoroutineScopeでテストを実行
という流れでやっていきます。
@ExperimentalCoroutinesApi class CoroutinesTestRule( val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() ) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() testDispatcher.cleanupTestCoroutines() } }
// このPrivate functionを作っておくと `coroutinesTestRule.testDispatcher.runBlockingTest` を毎回書かなくて良くて便利です private fun test(block: suspend TestCoroutineScope.() -> Unit) = coroutinesTestRule.testDispatcher.runBlockingTest { block() } @Test fun onResume_repositoryのAPIが叩ける() = test { val repository = mockk<ProjectRepository>() coEvery { repository.getProjects(any()) } returns emptyList() val viewModel = ProjectListViewModel(repository, testDispatcherProvider) viewModel.onResume() coVerify(exactly = 1) { repository.getProjects(any()) } }
MockkでCoroutines用のモック(coEvery)とアサーション(coVerify)を使うだけです。 上記の場合『ライフサイクルのonResume()が呼ばれたとき、repository.getProjects()が呼ばれることをアサート』することになります。
ポイントはViewModelのコンストラクタにCoroutinesのDispatcherをDIしていることです。
ViewModelではこの様になっています。
fun onResume() { viewModelScope.launch(dispatcherProvider.io()) { // 普通はviewModelScope.launch(Dispatchers.IO) で良いのだが・・・ try { val response = repository.getProjects("ticktakclock") _projects.value = response } catch (e: Exception) { e.printStackTrace() _projects.value = emptyList() } } }
なぜこのようなことをしているのかというと、 テスト用のコルーチンスコープで動くIOスレッドとViewModelで動くDispatchers.IOのスレッドが異なるため、 テストのcoVerify{}が正しく動作しないことがあるためです。LiveDataも同様に正しく動作しないことがあります。
理由については上記の通りで、図解するとこのようなイメージです。
この図解はAndroid Dev Summit '19にて説明されたもので、詳しくはこちらのアーカイブを見ていただいたらよいかと思います。 www.youtube.com
まとめ
- livedataとCoroutinesのテストがかけるようになった
- Coroutinesのテストのテストを見越してDispatcherもDIしておくと後々楽になるかも
JetpackComposeのチュートリアルをやってみた
こんにちは、tkyです。
かねてから触ってみたかったJetpackComposeのチュートリアルをやってみました。
本稿は2020/08/09時点のAndroid Jetpack compose チュートリアルを実施するに当たり ハマらないため+これからやることをイメージできるようにすることを目的とした共有的な位置づけで記載します。
すでにチュートリアルをやったことがあったり、必要な環境を
事前情報
事前、というか最初にまとめ書いときます。
- jetpack composeを試すにはAndroidStudio canary版が必要
- build gradleバージョンはほぼ最新じゃないとコンパイルエラーになるかも
- jetpack composeバージョンを1つ上げるとコンパイルエラーになることはよくある
Android Studio Canary版をインストールする
ここからCanary版をインストールします。筆者環境はMacなので既存のASと競合することはなかったですが、Win環境の人はインストール先を工夫したりすると良いかもですね。
https://developer.android.com/studio/preview
DL当時は AS 4.2 CANARY7でした。
ちなみにStableのASでjetpack composeを動かそうとすると画像のような通知が表示されます。
app.gradleに必要なもの
Jetpack Composeに必要なものは以下の様です、現時点ではDev13まで進んでいる模様
android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } composeOptions { kotlinCompilerExtensionVersion "0.1.0-dev13" } buildFeatures { // Enables Jetpack Compose for this module compose true } } dependencies { implementation 'androidx.ui:ui-core:0.1.0-dev13' implementation 'androidx.ui:ui-tooling:0.1.0-dev13' implementation 'androidx.ui:ui-layout:0.1.0-dev13' implementation 'androidx.ui:ui-material:0.1.0-dev13' }
build.gradleの方も更新が必要です。 最新じゃないと「java.lang.AbstractMethodError」というメッセージのコンパイルエラーが発生してちょっとだけハマります。
classpath "com.android.tools.build:gradle:4.2.0-alpha07"
チュートリアル自体は簡単
こういう関数を作成して、
@Composable fun Greeting(name: String) { Text(text = "Hello $name") }
ActivityのonCreateでsetContentする。 layoutの場合はsetContentView()だったのでまぁまぁ覚えやすいのではないでしょうか。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Greeting("Android") } }
一応これでビルドするとこちらのようなレイアウトが完成します。
これだけじゃ面白くないし、そもそもTextViewが左によりすぎてますね。
ご安心ください、チュートリアルでちゃんとレイアウト組みます。
いろいろ端折りますが、こんな感じにColumnというレイアウト用のcomposeUIを使ってLinearLayoutのような事ができます。
setContent { Column(modifier = Modifier.padding(16.dp)) { Greeting("Android") Greeting("Android") Greeting("Android") } }
ここで私が抱いた感想はこちらです
「Flutterと同じじゃん、てことは横はRowかな」
はい、Rowでした。ちゃんと横方向レイアウトになりました。
チュートリアルとしてはこの先 ・画像 ・マテリアルデザインの実現(角丸つけたりする)
を行って終了です。
Jetnews という存在
公式のサンプル。チュートリアルの後はこのアプリで実際にどうやってjetpack composeアプリを作るのか学んで行くことになりそうです。
Canary版で新規プロジェクト
を作ることもできます。
ここから作ることで最新のJetPackCompose環境が作れるので自身の環境でビルドエラーになってしまったときなどは このプロジェクトとの差分を見ることでトラブルシューティングできるかもしれません。
これから
jetpack compose チュートリアルをやってみました。 ReactやFlutterなどの宣言的UIでレイアウト組んだことはあったので自然に馴染むことはできました。
今後は ・どのようにUIを作るか、どの単位でUIを作るか ・命名規則 ・パッケージ構成の考察 ・コピペで使える逆引きCompose ・FlutterとかReactとか宣言型UIと比較してみて
このあたりを書いてみるのも面白いかなと思いました。
Shared Elementで画面遷移にアニメーションを加える
こんにちは、tkyです。
Androidで動きのある画面遷移に挑戦してみようと思います。
画面間遷移にアニメーション
今回は画面から画面の遷移にSharedElement(これって機能名になるんですかね?)を指定して
「遷移元のUIの一部が遷移先のUIの一部になるように見せる遷移」
を実現してみたいと思います。
文面だけではかなり想像がつきにくいのでSharedElementしない画面遷移と対応した完成形イメージです。
対応前 | 対応後 |
---|---|
Activitiy -> Activity のSharedElement
今回やったのはActivitiy -> Activityの画面遷移です。 他にはFragmentがあると思いますが、別の記事にしようと思います。
- Fragment -> Fragment
僕自身どうやって画面遷移を実現してるのがあまり理解していなかったのでざっくりメモ含めて解説していきます。
順序としてはこんな感じのことをやればSharedElement実現できます。
この3つをこのコミットで対応していますので、差分だけ見るとより理解しやすいかもしれません。
- startActivity時に遷移元のViewを指定
- 遷移先のViewを指定
- 遷移中のTransitionを指定
引用元:https://developer.android.com/training/transitions/start-activity
上記引用元はAndroidXで書かれていないので、参考にしつつAndroidX対応で書いていきます。
遷移元のViewを指定
アニメーションさせたいViewを指定します。第3引数のStringは後に出てくる遷移先のViewで指定するStringと同じものにする必要があります。
遷移元でやることはこれだけです。
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity, view, "photo") ActivityCompat.startActivity( this, intent, options.toBundle() )
遷移先のViewを指定
最終的に到達したい場所のViewを指定します。この場合ImageView->ImageViewの遷移です。 onCreate() 等でレイアウトをInflateしたあとに遷移させたいViewを指定します。この時setTransitionNameの第2引数は遷移元のViewで指定したStringと同じものにします。
ViewCompat.setTransitionName(binding.imageView, "photo")
ハマったところ
ここで1つハマったところがあって画像は基本的にURLがあってロードしてあとから表示の流れになると思います。
こうなるとImageViewの高さがあとから決まるのでwrap_contentとかにしていると高さ0のImageViewとかになってしまいます。
その高さ0のViewめがけてアニメーションしてしまうのでちょっと変な動きになります。下の動画で「ペスカトーレ」「ジャンバラヤ」をタップした時にViewが上に吸い込まれるような挙動になってしまったりします。
遷移先のViewは高さを指定するなどしてレイアウト時点でどこに配置されるべきかちゃんと定義する、を意識するのが良いのかもしれません。
遷移中のTransitionを指定
res/transition/change_image_transform.xml
(名前は何でも良いです)に遷移中どのようにViewを移動させるかを定義していきます。
それぞれのChangeBounds、ChangeImageTransformについてはあまり調べていません。
(transformで位置(x, y)を変更、boundsで大きさ(w, h)変更ですかね?この2つを組み合わせてアニメーションします)
<?xml version="1.0" encoding="utf-8"?> <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"> <changeBounds /> <changeImageTransform /> <targets> <target android:excludeId="@android:id/statusBarBackground" /> <target android:excludeId="@android:id/navigationBarBackground" /> </targets> </transitionSet>
まとめ
- Activity -> Activity の画面遷移でSharedElementを使ってみた
- 直線的なレイアウトは結構簡単にできた
- Fragmentの画面遷移もトライしてみたい
参考にしたURL
Start an activity using an animation | Android Developers
Shared Element Activity Transition | CodePath Android Cliffnotes
simple pollを使ってslackでアンケートを取る
こんにちは、tkyです。
N番煎じの小ネタなのですが、案外使っていない人が多いのかもしれませんね。
slackでアンケート的な何かがあるときにリアクションを使うパターンはよくありますね。
実は/pollコマンドでアンケート作れます
/poll
というコマンドがあって、これで簡単にアンケートが作れます。
アンケートを取りたいslackのチャンネルに以下のように投稿するだけです。半角スペースで区切ります。必要な文だけ答えを並べます。
フォーマット:/poll 質問 答え1 答え2 答え3 答え4 答え5
例:/poll 旅行どこに行きたいですか!?!!?!?!!?!?!! 北海道 沖縄 大阪 長野 東京
文章中に半角スペースを使いたい場合もあると思うので各要素を""でくくるとより表現しやすいかもしれません。
例:/poll "旅 行 どこに行きたいですか!?!!?!?!!?!?!!" "北 海 道" "沖 縄" "大 阪" "長 野" "東 京"
質問や回答に半角スペースが含まれていても問題なしですね!!
超小ネタで、知っているみなさんにとってはなんともない話かと思いますが案外知らない人も多いかもしれない、そんなネタでした。
KotlinプロパティとカスタムゲッターのBytecodeから見る違い
こんにちは、tkyです。
今日はKotlinのプロパティとゲッターの種類と違いを深堀りしてみようと思います。
はじめにプロパティとフィールドについておさらい程度に単語を整理します。
プロパティとフィールド
Kotlinのプロパティについて復習です。
Kotlinにおいて以下のように記述することはプロパティを宣言することであって、暗黙的にゲッターセッターを持っています。
Javaにするとフィールドとゲッターセッターを記述する必要があるのに対してボイラーコードが減っていいですね。
// kotlin var name: String = "This is a property"
// convert to java private String name = "This is a field."; public String getName() { return name; } public void setName(String var1){ name = var1; }
さらにKotlinのプロパティは明示的にゲッターを記述することもできます。
val isEmpty: Boolean get() = this.size == 0
そして fun getName()
のように宣言しようとするとコンパイルエラーとなります。プロパティ宣言によって暗黙的にgetName()メソッドが宣言されていることが確認できます。
ここまではある程度みなさんも理解していることでしょう。
ゲッター書き方色々
1つのプロパティにアクセスする方法がいくつか存在します。特に暗黙的に作成されたゲッターと明示的に作成されたゲッターは何が違うのでしょうか?
- プロパティ(暗黙的なゲッターを利用)
- カスタムゲッター(明示的に記述する)
- メソッド(暗黙的なゲッターとは異なる命名で作成)
この謎を解明すべくByteCodeという名の秘境の奥地に足を踏み入れてみたのでした。
サンプルコード
適当に作ってみました。全部valで定義しているのでセッターはありません。
class User(val firstName: String, val lastName: String, val age: Int) { // プロパティ宣言 val fullName: String = "$firstName $lastName" // カスタムゲッター val fullNameCustom: String get() = "$firstName $lastName" // メソッドでゲッター fun getFullNameMethod(): String { return "$firstName $lastName" } }
バイトコード
Android Studioの機能で Tools > Kotlin > Show Kotlin Bytecode を選択することでBytecodeが見れるようになります。
長すぎたので要所だけ切り取ります。
// プロパティ宣言 public final getFullName()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 6 L0 ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.fullName : Ljava/lang/String; ARETURN L1 // カスタムゲッター public final getFullNameCustom()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 10 L0 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.firstName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; BIPUSH 32 INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder; ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.lastName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ARETURN L1 // メソッドでゲッター public final getFullNameMethod()Ljava/lang/String; @Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 14 L0 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.firstName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; BIPUSH 32 INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder; ALOAD 0 GETFIELD com/github/ticktakclock/myapplication/User.lastName : Ljava/lang/String; INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ARETURN L1
プロパティ宣言
プロパティ宣言のものについてはフィールド fullName
と getFullName()
メソッドが作成されていて、読みだしたフィールドの値を返却していることがわかります。
fullName
もコンストラクタで文字列の連結が行われて、ゲッターでは計算済みの文字列を返却しています。
カスタムゲッターとメソッドのゲッターは特に違いなし
カスタムゲッターとメソッドのゲッターではフィールドは作られずメソッドだけが作成されていました。
バイトコードちゃんと読めないのですが、中に書かれていることも都度文字列の連結をして返却していることが雰囲気でわかります。
カスタムゲッターとメソッドによるゲッターはどちらも同じ振る舞いをするのですが、Kotlinっぽさが出るのはプロパティ宣言にしてカスタムゲッターを使うやり方かなと思います。(多分好みの問題
ゲッター内のコードの計算量に注意
例えば何度も getFullNameCustom()
を呼び出す場合、プロパティ宣言方式のほうが計算済みの値を返すだけなので効率が良いかもしれないなと思いました。これがfor文とかが入ってO(n)などになるとパフォーマンスに影響するかもですね
まとめ
おそらくKotlinのコードの書き方によって生成されるバイトコードは最も良い形に最適化されて出力されると思うので必ずしもこの通りになることはないかもしれません。
しかし自らが書いたプログラムがどう動こうとしているのかを把握しておくことで「なぜこう書いたのか?」の理由づけや意思決定の材料になったり、困ったときにバイトコードを見てみるといった調査の幅も広げられるのではないかなと思います。