こんにちは。ECF Techブログ
担当 Michiharu.Tです。
Javaプログラミング入門記事の第13章をお送りいたします。
第13章のテーマは「多態性」です。
本連載の初回および章立ての一覧については下記のリンクから確認できます。
解説動画
2024/03/22 解説動画を掲載しました。
13-1 共通点から親クラスを考える
13-1-1 取り扱う仕様とプログラム
本章では継承関係の本質的な意味について説明し、多態性(たたいせい) と呼ばれるオブジェクト指向の重要な性質について説明します。
説明のための題材として、お店アプリで使用する次のような2種類のクーポンについて考えます。
- 定額割引クーポン:1会計ごとに合計金額から一定金額の割引をするクーポン。
- 定率割引クーポン:1会計ごとに合計金額から一定率(10%、20%など)の割引をするクーポン。
まずは題材となっている2種類のクーポンをクラスとして表現します。それぞれ、次のようなクラスとします。
プログラム例(AmountCoupon.java)
public class AmountCoupon{ public String name; //クーポン名 public int amount; //割引額 public AmountCoupon(String name, int amount){ this.name = name; this.amount = amount; } //クーポンの適用 public int apply( int total ){ return total - amount; } }
プログラム例(RateCoupon.java)
public class RateCoupon{ public String name; //クーポン名 public double rate; //割引率 public RateCoupon(String name, double rate){ this.name = name; this.rate = rate; } //クーポンの適用 public int apply( int total ){ return total - (int)(total * rate); } }
特に新しい要素はありません。これまで学習した知識だけで作成しています。
- AmountCouponクラスが定額割引クーポンで、RateCouponクラスが定率割引クーポンです。
- クーポンを適用する際に用いるapplyメソッドは、引数で合計金額を受け取り、クーポン適用後の金額を返すメソッドです。
- AmountCouponクラスでは一定金額を引く計算式になっており、RateCouponクラスでは一定率の金額を引く計算式になっていることがわかります。
- コンストラクタはどちらのクラスも、引数の値をフィールドに設定する内容になっています。
下のメインクラスで動作を確認します。
プログラム例(Main.java)
public class Main{ public static void main(String[] args){ AmountCoupon coupon1 = new AmountCoupon("新春特割",2000); RateCoupon coupon2 = new RateCoupon("毎月割引",0.1); //合計5000円の場合の割引後の金額を表示 System.out.println(coupon1.apply(5000)); System.out.println(coupon2.apply(5000)); } }
実行結果
3000 4500
解説
2つのクラスのインスタンスを生成し、それぞれのapplyメソッドの引数に5000
を渡して、割引後の金額を表示しています。AmountCouponクラスのapplyメソッドでは2000円引きで3000円となり、RateCouponクラスのapplyメソッドは500円(5000円の1割)引きで4500円となっています。
クーポンを表す基本的なクラスができました。次節から多態性の実現に向けたプログラムの修正を進めます。
13-1-2 親クラスの要素を抽出する
2つのクーポンクラスを見てみると、下図のように共通している部分があることがわかります。
オブジェクト指向で新しいクラスを検討する場合、複数のクラスの共通点をまとめて親クラスを作成するという考え方があります。今回はその方法でCouponクラスという親クラスを作成したいと思います。
プログラム例を示します。
プログラム例(Coupon.java)
public class Coupon{ public String name; //クーポン名 public Coupon(String name){ this.name = name; } //クーポンの適用 public int apply( int total ){ return 0; } }
2つのクラスの共通部分を持ったCouponクラスを作成しました。但し、applyメソッドについては処理が共通ではないため、処理部分を一旦0を返すだけの処理としています。続けてAmountCouponクラスとRateCouponクラスを、Couponクラスを継承する形に修正します。
プログラム例(AmountCoupon.java)
public class AmountCoupon extends Coupon{ public int amount; //割引額 public AmountCoupon( String name, int amount ) { super(name); this.amount = amount; } //クーポンの適用 public int apply( int total ){ return total - amount; } }
プログラム例(RateCoupon.java)
public class RateCoupon extends Coupon{ public double rate; //割引率 public RateCoupon( String name, double rate ) { super(name); this.rate = rate; } //クーポンの適用 public int apply( int total ){ return total - (int)(total * rate); } }
これら3つのクラスとメインクラスを再度コンパイル・実行すると、元の実行結果と同じ結果を得ることができます。
ですが、今回作成したCouponクラスは少し違和感のあるものになっています。nameフィールドを共通部分として定義する点は問題ないのですが、定義部分のみが共通しているapplyメソッドまで定義する意味はあるのでしょうか? 実際、戻り値として0を戻すだけの意味のない処理になってしまっています。
実はここが多態性を実現する上で重要なポイントとなります。説明は後ほど行いますので、一旦次に進みたいと思います。
13-2 is-a関連
ここで、継承の持つ意味合いについて説明します。継承は単に別のクラスのフィールドやメソッドを引き継いで、プログラミング効率を高めるためだけのものではありません。継承には、オブジェクト間に 「is-a関連」 と呼ばれる関係性を持たせる。というもう1つの役割があります。
is-a関連とは、
- タクシーやトラックは、大きなくくりで言えば乗り物
- りんごやみかんは、大きなくくりで言えば果物
といった関係性のことです。英語で表すと
- Taxi is a Vehicle.
- Apple is a Fruit.
と表現できるので「is-a関連」と呼ばれています。日本語に訳する場合は「タクシーは乗り物の一種」のように少し意訳しておくとわかりやすいと思います。
継承関係では、これを下のように表します。
子クラス is a 親クラス
今回のクーポンを表すクラス関係においても、
- AmountCoupon is a Coupon. → 定額クーポンはクーポンの一種
- RateCoupon is a Coupon. → 定率クーポンはクーポンの一種
という関係性を作ったことになります。
13-3 インスタンスをまとめて扱う
では、is-a関連を作ることでプログラムにはどのようなメリットがあるのでしょうか?説明の題材として、次のような会計処理を行なうCasherクラスについて考えます。
プログラム例(Casher.java)
public class Casher{ public static void check(int total, AmountCoupon coupon){ int newTotal = coupon.apply(total); System.out.println("お会計は" + newTotal + "円です。"); } public static void check(int total, RateCoupon coupon){ int newTotal = coupon.apply(total); System.out.println("お会計は" + newTotal + "円です。"); } }
checkメソッドはお会計を行なうクラスメソッドです。合計金額(total
)とクーポン(coupon
)を引数として受け取り、合計金額にクーポンを適用した金額を表示する内容となっています。
このプログラムは正しく動作しますが、2種類のクーポンに対応できるようにするため、引数の異なるメソッドを複数定義(オーバーロード)してしまっています。このことは次の2つの問題を持ちます。
- オーバーロードした2つのメソッドの処理内容は一言一句変わらない。(処理の重複)
- クーポンの種類が増えるたびに異なる型のメソッド定義を追加しなければならない。
そして、この問題は「is-a関連」となっているクラスのある性質を使って解決することができます。
その性質とは、
「子クラスのインスタンスは、親クラスの型に代入できる」
という性質です。この性質を用いるとCasherクラスのcheckメソッドを1つにまとめることができます。プログラム例を示します。
プログラム例(Casher.java)
public class Casher{ public static void check(int total, Coupon coupon){ int newTotal = coupon.apply(total); System.out.println("お会計は" + newTotal + "円です。"); } }
checkメソッドが1つ無くなって、第2引数の型がCoupon型になっただけです。実際にこのプログラムがどのように動作するか、次のメインクラスで動作を確認してみましょう。
プログラム例(Main.java)
public class Main{ public static void main(String[] args){ AmountCoupon coupon1 = new AmountCoupon("新春特割",2000); RateCoupon coupon2 = new RateCoupon("毎月割引",0.1); Casher.check(5000,coupon1); Casher.check(5000,coupon2); } }
実行結果
お会計は3000円です。 お会計は4500円です。
解説
2種類のクーポンを生成後、Casherクラスのcheckメソッドを呼び出しています。6行目の呼び出しと7行目の呼び出しとで、それぞれ異なるクーポンインスタンスを渡しており、それぞれ次のように動作します。
Casherクラスのcheckメソッドにcoupon1
を引数として渡しています。引数coupon
はAmountCouponインスタンスを指していますので、同インスタンスのapplyメソッドが呼び出されます。
Casherクラスのcheckメソッドにcoupon2
を引数として渡しています。引数coupon
はRateCouponインスタンスを指していますので、同インスタンスのapplyメソッドが呼び出されます。
ここで最も重要なポイントは、Casherクラス3行目のcoupon.apply(total)
の部分が、変数coupon
の指すインスタンスの違いによって異なる動作をしている。 という点です。このような動作特性を多態性(たたいせい) と呼んでいます。
13-4 多態性
多態性(ポリモフィズムとも言う) とは、同一の命令を異なるオブジェクトが受け取ることによって、異なる動作をする性質のことです。会社の社長と部下に例えると次のようなイメージです。
社長が部下に対し、「各自仕事を始めてください」と指示したとしましょう。このとき、プログラマーはプログラマーなりの仕事をし、会計係は会計係としての仕事をし、営業は営業としての仕事を開始します。
社長がいちいち「プログラマーはプログラミングを始めてください。会計係は会計処理を始めてください。営業は・・・」と指示をしなくても、「仕事を始めてください」の1つの指示だけでそれぞれの社員が自律的に業務を開始します。
これが多態性のイメージです。
13-3のプログラム例も同様です。クーポンを適用する命令はcoupon.apply(total)
の1つですが、変数couponがどのインスタンスかによって動作が異なります。定額クーポン (AmountCouponインスタンス)であれば、定額クーポンなりの割引処理を行い、定率クーポン(RateCouponインスタンス)であれば定率クーポンなりの割引処理が動作します。
13-4-1 多態性を実現する土台
この多態性を実現している土台となっているのは、Couponクラスのapplyメソッドです。このメソッドは次のように定義されていました。
public int apply( int total ){ return 0; }
戻り値として0
を返すだけの意味のないメソッドに見えますが、このメソッドは、あるとても重要な役割を果たしています。それは、
クーポン系のクラスは、必ずapplyメソッドを持っていることを保証する。
という役割です。ここで「クーポン系」と言っているのは、Couponクラスとそれを継承した子クラスのことと考えてください。
このことはコンパイラにとって重要です。なぜなら、コンパイラはcheckメソッド内のcoupon.apply(total)
の部分を解釈する際、次の様に理解するからです。
- 引数
coupon
が何型かを判断する。 → Coupon型 - Coupon型がapplyメソッドを持っているか(定義されているか)をチェックする。
- 定義されていればチェックOKとする。
この時重要なのは、コンパイラは型の判断を宣言部分から判断する。ということです。引数coupon
はcheckメソッド宣言部分においてCoupon coupon
となっています。よって、Coupon型と判断されることになります。
13-4-2 Couponクラスの違和感
ここまで多態性を実現するプログラムについてみてきましたが、その実現のために作成されたCouponクラスに次のような違和感を感じるかもしれません。
- Couponクラス自体のインスタンスを生成するとどうなるの?
- 多態性のために0を返すだけの無意味なメソッドを作っているけど、他に良い方法は無いの?
次章では、これらの疑問を解決するしくみについて説明します。