対話型AI(人工知能)にアドバイスを受けながら進めるJavaプログラミングの入門連載。今回は、ポリモーフィズム(多態性)を、インタフェースとともに学習します。使いこなせれば便利ながらも、言葉の響きからも難解とされがちなポリモーフィズムを、親しみやすい事例から理解しましょう。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。
対話型AI(人工知能)にアドバイスを受けながら進めるJavaプログラミングの入門連載「AIアシスト時代のJavaプログラミング入門」。前回は、Javaをオブジェクト指向言語たらしめる機能として、カプセル化と継承について学びました。いずれも、「現実社会のモノや概念をプログラムで表現する」ために必須の機能ということが分かりました。
今回は、前回Visual Studio Code(以降、VS Code)からGitHub Copilot(以降、Copilot)に投げた質問「オブジェクト指向についてメソッドやコンストラクタ以外の主な機能を重要度順に教えて。サンプルコードは不要です。」で得られた、その他の回答に注目していきます。まず、その回答を振り返ってみます(図1)。
「まとめ」も再掲すると、2番目の「ポリモーフィズム」が前回扱っていない内容です。
今回は、この「ポリモーフィズム」からオブジェクト指向を深掘りしていきましょう。
「ポリモーフィズム」とは、日常生活では(おそらく)使わない言葉なので、機能のイメージもしにくいのではないでしょうか。図1の回答を見ると、「まとめ」に「コードの再利用性と柔軟性」とある他に、以下のように説明されています。
AIの回答においては「インターフェース」が登場しますが、本文中では@ITのルールに合わせて「インタフェース」に統一しています。意味としては変わりませんのでご了承ください。
1.については、前回で学習した内容です。「同じメソッド名や操作で異なる動作を実現する」というのは、「オーバーライド」という機能で実現されていました(「例:」にも示されていますね)。前回の例では、同じmoveというメソッドをDogクラスとCatクラスで呼び出すと、それぞれ以下のような動きとなりました。
これは、DogクラスとCatクラスの親クラスであるAnimalクラスではmoveメソッドを「〜が移動します。」としていたのに、Dogクラスでは「〜が走ります。」となるようにオーバーライドしたからでした。これにより、moveメソッドをDogクラスとCatクラスで異なる動きにできたわけです。
しかしながら、このような動きの違いは継承によってできたわけで、ポリモーフィズムとの関係がよく分かりません。さらに、「例:」には「インタフェース」という新しい言葉も登場しています。
2.に示されているメリットも、1.の問題がクリアにならないと具体的にイメージしにくいかもしれません。何をもって「柔軟性」があるというのでしょうか。そこで、ポリモーフィズムについて掘り下げる質問を改めて投げてみることにしましょう。「ポリモーフィズムについてもう少し詳しく。」と投げてみました(図2)。
前回の流れでチャットビューを使うと、「コードは不要です。」が生きていて、ひとまずはコードなしで説明してくれます。回答にコードが出てきてしまったら、質問文に「コードは不要です。」を付加してみてください。
図2では、ポリモーフィズムのポイントがまず示されています。「インタフェース(型)」も出てきていますが、ひとまず脇に置いておき、ポイントを見てみましょう。
3.の「柔軟性」についてはたびたび出てきているので、これは最後に検討するとして、まずは最初の1.と2.です。「共通の型」って何? という疑問が湧くと思うので、その説明を読んでみましょう。
・親クラス型やインタフェース型の変数で、さまざまな子クラスのインスタンスを扱うことができます。 例:Animal型の変数にDogやCatのインスタンスを代入できる。
例に、すごいことが書いてあります。前回の「継承」の解説では触れていなかったのですが、実は
子クラスのインスタンスは親クラスの変数に代入できる
のです。前回の「継承」の例で「多態性の例」とあったコードに少しだけ触れましたが、それが相当します。前回の図から、該当箇所のみを切り出して再掲します(図3)。
図3の赤字にあるように、「子クラスのインスタンスは親クラスの変数に代入でき」「子クラスのメソッドが呼ばれる」のです。型はAnimalでも、実際のインスタンスの型(この場合はDog)に沿って振る舞うというわけですね。これがCatならCatに沿って振る舞います。これが、図2におけるポイントの「2. 動的なメソッド呼び出し」に説明されている内容です。ポリモーフィズムの和訳である「多態性」という用語は、ここから来ています。Animalが「多」くの「態」(さま)を持つ「性」質を備えるわけです(図4)。
ポリモーフィズムについて深めるために、このコードを踏み台に今回用のプロジェクトを作成しましょう。これまでは、第1回で紹介した手順でプロジェクトを作成していましたが、今回はプロジェクト作りもCopilotに任せてしまいましょう。
VS Codeを、前回紹介した手順でAgentモードに切り替えてください。そして、「inheritanceプロジェクトを複製してpolymorphismプロジェクトを作って。」と投げてみましょう。ワークスペース内に自動的にpolymorphismプロジェクトが作成され、ひとまず内容は前回作成したinheritanceプロジェクトと同じになります(図5)。
ビルドして実行すればinheritanceプロジェクトと同じ結果となりますが、その前にコードを少し書き換えてもらいましょう。「他に追加や修正したい点があればご指示ください。」と言ってくれていますしね。遠慮なくいきましょう。
エディタビューにApp.javaが開かれている状態で、プロンプトパネルに「App.java」と正体で表示されていることを確認してください(図7)。もし斜体になっている場合には、ファイル名右の[+]をクリックしてください。
ここで、「多態性の例にCatクラスも追加して。」と投げてみます(図8)。
インデントが少しおかしいのですが、既存のコードが削除され、DogクラスとCatクラスのインスタンスを格納する2つのAnimal型変数が宣言されました。実行結果がコメントに表示されてしまっていますが、改めて実行して確かめましょう(図9)。
ポリモーフィズムに関係ないコードの結果も混じっていますが、最後の2行が該当する出力です。同じAnimal型の変数ですが、DogクラスのインスタンスとCatクラスのインスタンスで結果が変わっています。Dogクラスではmoveメソッドをオーバーライドしているので「〜が走ります。」となりますが、CatクラスではオーバーライドされていないのでAnimalクラスのmoveメソッドすなわち「〜が移動します。」になります。このように、実際のインスタンスに合わせて振る舞うのが多態性というわけです。
さて、ここで疑問が湧く人もいるかと思います。mainメソッドの冒頭ではDogクラスとCatクラスのインスタンスをそのままDog型、Cat型の変数に入れているので、これでいいんじゃないかと。確かに、それぞれのクラスのメソッドを呼ぶだけなら、わざわざAnimalクラスの変数を使う必要もありませんね。ここで、図2の「ポリモーフィズムの主なポイント」にあった「拡張性と柔軟性」です。この当たりを具体的に示してもらうことで、この疑問を解決しましょう。
ストレートに、「ポリモーフィズムの拡張性と柔軟性が分かる例を追加して。」と投げてみました(図10)。
エディタビューでコードが改変されます。改変の内容は、
となっています(図11)。
1.についてはこれまでの知識で読み解けると思います。2.では何をやっているか見るために、まずは実行していることにしましょう。その前に、mainメソッドの中にいろんなメソッド呼び出しがたまってきてしまっているので、例を追加した部分より前をコメントアウトしてしまいます。該当範囲を指定して、[Ctrl]+[/](Windows)/[Command]+[/](macOS)キーを押すだけと簡単です。実行してみると、図13のようになりました。
これを踏まえて、追加のコードを図解してみたのが図14です。
コードのポイントは(1)3つのクラスのインスタンスで、Animal型の配列を作り、(2)それを拡張for文で表示し、(3)Birdクラスのsingメソッドを呼び出す、この3つです。これで、4行結果が表示されているわけです。なお、配列や拡張for文を忘れてしまったという人は、それぞれ第2回、第4回を振り返ってください。
(1)と(2)でやっていることは、配列も拡張for文も使わずに、それぞれのクラスのインスタンスを生成して変数に入れて、それにmoveメソッドを呼び出せば、同様のことが行えます。しかしながらこの方法は、プログラムを拡張しようとしてPigクラスやLizardクラスを作っていったら、それらの変数を用意して呼び出すコードを追加していくという、考えるだけで面倒な作業が想像できますね。
現状の方法だと、(新しいクラスの定義は別として)配列に新しいクラスのインスタンス生成を追加するだけで、拡張for文には全く手を付ける必要はないのです。拡張のしやすさ、変更への追従のしやすさ、これが「拡張性と柔軟性」というわけです。これは、Animal型を受け取る(返す)メソッドを設計する場合にも同じことが言えます。
このようにポリモーフィズムはクラスの継承やメソッドのオーバーライドを生かした、いかにもオブジェクト指向的な機能ですが、万能ではありません。図14のコードの最後では、Animal型の変数からBirdクラス独自のsingメソッドを呼び出していますが、このときはAnimal型をBird型に型変換(キャスト)して、変数はBird型です! というように宣言してからsingメソッドを呼び出しています。
つまり、ポリモーフィズムを利用して子クラスのメソッドを呼び出す場合、そのメソッドは親クラスにも定義されている必要があります。親クラスは全ての子クラスを知っているわけではないので、存在しないメソッドを直接呼ぶことはできないのです。存在しないメソッドを呼び出すには、この例のように「実際にはBird型である」と宣言(キャスト)することで、はじめてBird独自のメソッドを利用できるようになります。
もしキャスト先が誤っていて、実際にはBird型でなかった場合、そのメソッドは呼び出せず、「例外(エラー)」が発生します。この部分は開発者が責任を持って、型が正しく一致するようにコードを書く必要があるわけです。
最後に、「型変換」(キャスト)が登場しました。このように、Javaではある型を別の型に変換することができます。ただし、何でもかんでも変換できるわけではなく、一定のルールが存在します。Javaの型変換について聞いてみると、今回のようなことが可能な理由が判明するかもしれませんよ!
次は、5番目に重要とされている「インタフェース」です。冒頭の図1では、以下のように説明されています。
メソッドの仕様を定義するならクラスと同じとも言えそうですが、「クラスが実装すべき」がポイントのようです。ところで、インタフェースという用語は、図1の4番目「抽象化」(Abstraction)にも「抽象クラス」というものとともに登場しています。抽象ということがポイントのようですが、これを知るためにインタフェースについて掘り下げる質問を改めて投げてみることにしましょう。「インタフェースについてもう少し詳しく。コードは不要です。」と投げてみました(図15)。
上記でも示された「仕様」を定めるものと示されました。インタフェース自体には具体的な処理は含まれず、メソッドの仕様のみが宣言されているものと読み取れます。特徴が4つ示されましたが、仕様のみを定めるとはいったいどのようなことなのか、これまでと同様に具体的なイメージで示してもらいましょう。「インタフェースの具体的なイメージをコードなしで。」と投げてみました(図16)。
インタフェースを、「プリンタ」で例えてくれました。かなりシンプルな説明で、これでも十分理解できると思いますが、さらに簡略化すると、このような感じでしょうか。
ここではプリンタに例えられましたが、ポリモーフィズムの例えで登場した動物でも同様です。「移動する」(moveメソッド)ということだけインタフェースで決めておき、どのように移動するかはそれぞれのクラスで決めるというわけです(図17)。
インタフェースという用語は、既に学習したポリモーフィズムの中でもたびたび登場してきたので、両者には密接な関係がありそうだということは分かりますね。そこで、分かりやすいように動物の例を使って、インタフェースの例を提示してもらいましょう。polymorphismプロジェクトを作ったときと同じように、プロジェクトを複製して必要な修正を加えてもらうことにします。
Agentモードになっていることを確認して、「polymorphismプロジェクトを複製してinterfaceプロジェクトを作って。」と投げてみます。すると何と、プロジェクトを複製するばかりか、インタフェースを使ったコードに書き換えてくれました(図18)。
せっかくコードを作ってくれたので、実際のコードを眺めながらインタフェースについて理解を深めていくとします。
インタフェースを使ったコードは、継承のコードによく似ています。というより、インタフェースが継承を前提とした機能なので、当然といえば当然です。異なるのは、クラス定義の前に、図19のようにインタフェースの定義があることです。
インタフェースの定義では、classキーワードではなくinterfaceを使います。そして、その中にメソッドの定義を入れていきますが、メソッドの処理内容は入れません。インタフェースには、どのようなメソッドがあるかという情報のみを入れていくのは前述の通りです。ここでは、moveメソッドがあることだけを表しています。
次は、クラスの定義を見てみます。クラスは3つありますが、だいたい似たような内容なので、最初のDogクラスを見てみます(図20)。
これまでのクラス定義と変わらないように見えますが、クラス名の後に「implements Animal」と続いている点が異なりますね。implements(実装する)とあるように、インタフェースを実装する場合には「implements」キーワードに続けてインタフェース名を指定します。これで、クラスDogはインタフェースAnimalを実装する、なのでmoveメソッドの処理内容を記述する必要がある、ということになります。
@overrideアノテーションの後に、moveメソッドの定義があります。繰り返しになりますが、インタフェースにあるメソッドは、必ずクラスの方で実装しなければなりません。もしも実体がなかったら、呼び出されたときに何をしたらいいか分かりませんからね。
インタフェースを使う場合も、独自のメソッド(Dogクラスならbarkメソッド)を定義できます。これは、Catクラス、Birdクラスでも同じです。
最後に、mainメソッドのコードを見てみたいと思いますが、これはポリモーフィズムにおけるものと全く変わりません(図21)。
キーワード的には同じAnimalですが、ポリモーフィズムの例ではAnimalはクラス、ここではAnimalはインタフェースです。インタフェース型の変数を宣言し、そこにクラスのインスタンスを入れることができます。その変数を使い、インタフェースにあるメソッドを呼び出せる点も同じです。
ポリモーフィズムもインタフェースも、今回で紹介したことは最も基本的な事柄です。引っ掛かったところがあればAIに質問を投げて、学習の幅を広げていってください。
本稿では触れませんでしたが、インタフェースと似た機能に「抽象クラス」があります。抽象クラスもインタフェースと同様に、継承した側で実装しなければならないメソッドを宣言します。その構文やインタフェースとの使い分けを聞いてみると、インタフェースの意義や継承とポリモーフィズムへの理解がさらに深まるかもしれませんよ!
今回は、前回に引き続きオブジェクト指向の要であるポリモーフィズムとインタフェースについて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)
Copyright © ITmedia, Inc. All Rights Reserved.