対話型AI(人工知能)にアドバイスを受けながら進めるJavaプログラミングの入門連載。今回は、オブジェクト指向プログラミングの機能である、不変オブジェクトとレコード型を学習します。不変オブジェクトの意義と、それを簡単な構文で実現するレコード型を、AIに聞きながら理解しましょう。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
対話型AI(人工知能)にアドバイスを受けながら進めるJavaプログラミングの入門連載「AIアシスト時代のJavaプログラミング入門」。前々回に当たる第8回では、オブジェクト指向の実用的な機能を知りたいということで、「Javaのオブジェクト指向機能を使った実用的な機能を知りたい。コードは不要です。」という質問を、Visual Studio Code(以降、VS Code)からGitHub Copilot(以降、Copilot)に投げてみました(図1)。
ここからジェネリクスとコレクション、列挙型をピックアップして学びました。AIが、なかなか的を射た解説をしてくれるので、今回もオブジェクト指向の延長として、実用的な用途に引き続き踏み込みます。今回は9番目の「不変オブジェクト」をピックアップします。なお、モデルには今回も引き続きGPT-5 miniを使っていきます。
「不変オブジェクト」(Immutable object)は、図1において、以下のように説明されています(immutableとは文字通り「不変」という意味です)。
・スレッド安全性、バグ減少、キーとしての安全な使用、値オブジェクト設計に有効
これまでもたびたびオブジェクト指向のメリットとして語られてきましたが、ここでも安全性がキーワードのようです。不変オブジェクトは、どのように安全性につながるのでしょうか? そもそも「不変オブジェクト」って何? という疑問があると思うので、その意味を聞いてみましょう。ここはシンプルに、「不変オブジェクトとは?」と投げてみました(図2)。
これを読んだ限りでは、不変オブジェクトは難しそうなものと感じるかもしれません。しかしながら、「不変」はJava以外のプログラミング言語でもトレンドとなりつつある考え方であり、不変オブジェクトの実装において関連するさまざまな知識も学べるので、ここで大まかにでも理解しておくメリットは決して小さいものではないでしょう。
そこで、ここでは「定義」「主な利点」に示された内容について、簡単に見ておくことにします。「Javaでの実装ルール(要点)」と「簡単な例(抜粋)」については、後ほど具体的なコードとともに見ていきます。まず「定義」には、以下のように示されています。
・一度生成した後にその状態(フィールドの値)が変更できないオブジェクト
言い換えると、「状態の変わらないオブジェクト」ともされています。つまり、一度作ったら変わらないということです。これによるメリットが、「主な利点」に示されています。スレッド安全性(セーフ)をはじめとする、変わらないことによる「よいこと」が多数あるわけです。
とはいえ、説明はややふわっとしている印象です。そこで、メリットを少し掘り下げてもらう質問「不変オブジェクトの利点をやや詳しく。」を投げてみました(図3)。
図2で示されたメリットに対応するものを抜粋して補足します。
・スレッド安全性(スレッドセーフ)
スレッドとは、複数のプログラムコードを同時に並行して動かす仕組みで、またその単位を表します。スレッドではコードが並行動作する性質上、データの共有には細心の注意が必要ですが、不変オブジェクトであればどのスレッドからでも同じに見えることが保証されます。これによって、オブジェクトを複数のスレッドで安全に共有することができます。
・副作用が少ない(予測可能でデバッグしやすい)
オブジェクトが不変であるので、メソッド内でオブジェクトが変化すること、すなわち副作用がありません。同じ入力に対して同じ振るまいとなるので、メソッドの呼び出し結果を予測しやすくなります。
・安定したハッシュ値と比較(キャッシュ/Mapのキーに最適)
不変であるため、hashCodeメソッドやequalsメソッドの結果が変わらず、例えばHashMap(第8回を参照)のキーに安全に使えます。キャッシュされた値も信用できるものになります。なお、hashCodeメソッドとequalsメソッドは、全てのクラスが備えるメソッドで、それぞれハッシュ(第8回を参照)の計算、同値比較に使用されます。
・テストしやすさ(単体テストの簡便化)
上記の通り副作用への対策が減るため、テストケースを少なくできて、テストが単純になります。なおテストケースとは、メソッドの正当性を確認できる入力の組み合わせを言います。
ただし、「補足(現実的な配慮)」にあるように、不変オブジェクトの利用には考慮すべき点もあるので、特性や目的からバランス良く利用することが推奨されています。
図2にも図3にも「値オブジェクト」という用語がたびたび登場しました。「オブジェクト」にわざわざ「値」を付けているわけですが、「不変オブジェクトに関連して値オブジェクトとは?」と聞いてみると、目的に応じたクラスの設計方法への理解が深まるかもしれませんよ。
ここから、不変オブジェクトをどのように実装するか? を見ていきます。図2では省略しましたが、実は「簡単な例(抜粋)」にJavaのコードが2パターン示されていました。「クラシックな実装(防御コピー)」と「record(Java 16+)を使う例(簡潔)」です。ということは、Javaで不変オブジェクトを使うには、2つの方法があるということです。これらを順番に見ていくことにして、まずは「クラシックな実装」を見ていきます。
クラシックな実装のコードを示してもらいましょう。これまで行ってきたように、「basicプロジェクトを基にimmutable-classicプロジェクトを作り、不変オブジェクトをクラシックな方法で実装したサンプルコードを入れて。」と投げて一連の作業を実行してもらいました(図4)。
immutable-classicプロジェクトに、Person.javaとApp.javaが作成されます。Person.javaは、メソッド定義部分を除くと非常にシンプルです。具体的な内容を見る前に、これらのコードについて説明してもらいましょう。「生成されたPerson.javaのコードを、専門用語を使って要点のみ説明して。コードの説明以外は不要です。」と投げてみました(図5)。
だいぶ簡潔な説明ですが、これを基に対応するコードに簡単な説明を入れてみました。かなり長いので、オーバーライドされたメソッド以外の部分のみとしています(図6)。オーバーライドされたメソッドは後ほど取り上げます。
普通のクラス定義に見えますが、「Javaでの実装ルール(要点)」にあるように、「final」が付与されます。finalとは、その名の通り「最後」ということで、これ以上継承させないという意味です。第5回と第6回で解説した通り、クラスには継承という仕組みがあります。継承先で実装を変えられてしまう可能性があるので、継承そのものを禁じるのですね。
その他、フィールドにも「final」を付与して初期化以降は変更できないようにする、コンストラクタで初期化したままとする、セッターを持たないなど、「不変」であるためのさまざまなルールが盛り込まれています。
なお、図6には「防御的コピー」なる用語がありますが、これは何でしょうか? 防御的コピーについては、次項で呼び出しコードを見た後に、改めて解説します。
コンストラクタには、Objects.requireNonNullメソッドの呼び出しが含まれています。このメソッドは、引数がnullかどうか判定し、そうでなければその値をそのまま返し、nullであれば例外(次回で解説予定)を発生させます。コンストラクタに渡される引数がnullであってはならないときのチェックに使えるので、コンストラクタに含ませておくと安全性が向上します。
このクラスをどのように使うのかを確かめるために、利用側であるApp.javaの方も見てみます。App.javaに説明を入れてみました(図7)。このコードは、複数のインスタンス間でいずれかを変更しても、その他のインスタンスが変更の影響を受けないことを確かめるコードとなっています。
コードの説明の前に、実行してみて図7のコードの動きを確かめておきます。図4にはPowerShellでの実行方法が示されているので、まずは実行結果を見てみましょう(図8)。
図7は、普通にPersonクラスをインスタンス化して初期化し、ゲッターでDate型のインスタンスを取得しているだけに見えるコードですが、随所でインスタンスの変更を行っている点に注目です。まず(1)では、コンストラクタに渡したDate型のインスタンスを変更していますが、Person型のインスタンスには影響しないことが示されています。次に(2)ではPerson型のインスタンスからゲッターで取得したDate型のインスタンスを変更していますが、やはりPerson型のインスタンスには影響しないことが示されています。
これらは、コンストラクタやゲッターに実装された「防御的コピー」(防衛的コピー、不変コピーともいいます)によるものです。インスタンスは参照型であるため、普通にフィールドをコピーをするとオブジェクトの場所のみコピーされ、オブジェクトの実体を共有します(これをシャローコピー=浅いコピーといいます)。このとき、片方でフィールドの中身を変更すると、もう一方がそれの影響を受けます。このコピーだと、不変オブジェクトの性質を満たせません。
そこで、フィールドをその中身もコピーすることで(これをディープコピー=深いコピーといいます)、片方の変更の影響をもう一方が受けないようにするのです。コピーは、コンストラクタによる初期化、ゲッターによるフィールドの取得時に行われるので、そこで防御的コピーを実施します。インスタンスの単なる代入ではなく、コピーを作成して新たなインスタンスとするので、お互いが影響を受けないのです。
防御的コピーは、finalキーワードと共に不変オブジェクトを実現する重要な考え方といえます(図9)。
上記の説明では割愛した、オーバーライドされたメソッドをここで見てみましょう。ここまでの回では触れてきませんでしたが、これらのメソッド(equals()、hashCode()、toString())は、あらゆるクラスが暗黙に継承するObjectクラスが備えるメソッドです。それぞれ、同値性比較、ハッシュの計算、文字列化を担います。Objectクラスではごくごく基本的な実装しかなされていないので、以下のようにインスタンスを使う場合には、これらのメソッドをクラスに合わせて適切にオーバーライドする必要があります。
これを踏まえて、図6の続きを見てみます(図10)。図5にある説明を反映させてみました。長い行があるので、生成されたコードを掲載の都合で一部折り返しています。
ここで行われている処理は、不変オブジェクトに限ったものではなく一般的なものです。もし、独自のクラスを継承で定義して、これらのメソッドをオーバーライドしなければならないときには、参考にしてみてください。
図7のコードには、「不変オブジェクトは Map のキーとして安全に使える」とHashMapのキーにPerson型のインスタンスを使う例が示されていました。不変オブジェクトの性質から、なぜキーに使うと安全なのかを、安全でないケースを提示してもらうことで、不変オブジェクトへの理解が、より深まるかもしれませんよ。
図2で質問した時点では期待していなかったのですが、不変オブジェクトを実現する方法の2つ目として、「record(Java 16+)を使う例(簡潔)」も示されています。実は、Javaのバージョン16以降では、record(レコード型)が利用可能になっていて、クラシックな実装で行っていたようなことを、ほとんど自動で行ってくれるようになっているのです。
レコード型による実装のコードを示してもらいましょう。「basicプロジェクトを基にimmutable-recordプロジェクトを作り、不変オブジェクトをrecordで実装したサンプルコードを入れて。」と投げて一連の作業を実行してもらいました(図11)。
immutable-recordプロジェクトに、PersonRecord.javaとApp.javaが作成されます。PersonRecord.javaは、メソッド定義部分を除くと非常にシンプルです。具体的な内容を見る前に、これらのコードについて説明してもらいましょう。「生成されたPersonRecord.javaのコードを、専門用語を使って要点のみ説明して。コードの説明以外は不要です。」と投げてみました(図12)。
だいぶ簡潔な説明ですが、これを基に対応するコードに簡単な説明を入れてみました。レコード型はクラスの特別な形態で、不変オブジェクトに求められる機能をあらかじめ含んでいます(図13)。
クラシックな実装と比べれば分かりますが、圧倒的にrecordを使った方のコード量が少ないですね。「class」の替わりに「record」を使う以外は、通常のクラス定義と変わらないように見えます。そこで、図12を基に、recordの機能を抜き出してみました。final classとしたクラシックな実装で行っていたことが、暗黙のうちに行われていることが分かります。
ここで注目すべきはコンパクトコンストラクタでしょう。本来、recordを使うとコンストラクタも自動生成されるので、別途コンストラクタを用意するのには意味がありそうです。コンパクトコンストラクタとは、その名の通り通常のコンストラクタ(カノニカルコンストラクタともいいます)と比較して記述をコンパクトにできるコンストラクタです。記述がそもそもシンプルであるrecord型にふさわしいコンストラクタといえるでしょう。
コンパクトコンストラクタは、以下のような性質を持ちます。
要は、だいぶ省力化してコンストラクタを定義できるということですね。ここでは、入力値に対する検証のためのコードを記述するためにコンパクトコンストラクタを使っているのだと理解しましょう。もちろん、カノニカルコンストラクタでも同じ目的で記述できます。検証に使っているObjects.requireNonNullメソッドについては説明済みですね。
recordをどのように使うのかを確かめるために、利用側であるApp.javaの方も見てみます(図14)。長い行があるので、生成されたコードを掲載の都合で一部折り返しています。
普通のクラスのようにRecordPersonレコードをインスタンス化して初期化し、ゲッターでnameフィールドを取得したり、独自のメソッドisAdultを呼び出しています。このように使い勝手は普通のクラスと同等といえそうなので、定義の簡略化に最大のメリットがあるということですね。
図11にはPowerShellでの実行方法が示されているので、最後に実行結果を見てみましょう(図16)。この実行結果からコードを振り返ってみてください。
ここでは、日付のためのフィールドをLocalDate型にしていました。クラシックな実装ではDate型を使用していて防御的コピーが必要とされていましたが、recordを使った実装では防御的コピーが行われている様子がなさそうです。この違いを聞いてみると、クラスの設計についての考え方の違いに対する理解が、より深まるかもしれませんよ!
今回は、不変オブジェクトを2つの実装方法で比較しながら、その性質をAIに聞きながら学習しました。また、不変オブジェクトを簡単な構文で実現するレコード型を学習しました。
次回は、エラー処理のための例外を学習します。
WINGSプロジェクト 山内直
WINGSプロジェクト所属のテクニカルライター。出版社秀和システムを経てフリーランスとして独立。ライター、編集者、開発者、講師業に従事。屋号は「たまデジ。」。
・たまデジ。 | たまプラーザで生活、仕事する。(https://naosan.jp/)
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・X: @WingsPro_info(https://x.com/WingsPro_info)
・Facebook(https://www.facebook.com/WINGSProject)
プログラマー以外にもおすすめ 「Visual Studio Code」のインストールから基本設定まで
Visual Studio Codeを活用するための人気TIPS 12選
初心者向け、データ分析・AI・機械学習の勉強方法 Deep Insiderで学ぼうCopyright © ITmedia, Inc. All Rights Reserved.