こんにちは。ECF Techブログ
担当 Michiharu.Tです。
Javaプログラミング入門記事の第14章をお送りいたします。
第14章のテーマは「抽象クラスとインターフェース」です。
本連載の初回および章立ての一覧については下記のリンクから確認できます。
14-1 抽象クラス
13章ではAmountCouponクラスとRateCouponクラスの2つのクーポンクラスから共通部分を抽出し、親クラスであるCouponクラスを作成しました。改めて3つのクラスを示します。
プログラム例(Coupon.java)
public class Coupon{ public String name; //クーポン名 public Coupon(String name){ this.name = name; } //クーポンの適用 public int apply( int total ){ return 0; } }
プログラム例(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); } }
このうち、Couponクラスは継承を前提としたクラスです。そのため、インスタンスを生成する想定はありませんし、多態性実現のためだけに定義されているapplyメソッドも意味のない処理になってしまっています。本節では、これらの問題の解消を目指してCouponクラスの修正を行います。
14-1-1 抽象メソッドと抽象クラス
ここではまず、意味のない処理になってしまっているapplyメソッドを修正します。Couponクラスのapplyメソッドのように、オーバーライドが前提になっている処理が不要なメソッドは、抽象メソッドとして定義できます。文法は次のようになります。
abstract 戻り値型 メソッド名(引数...);
- メソッド定義に
abstract
修飾子をつけます。 - 処理部分
{...}
は書きません。代わりに;
を記述します。
処理が不要なので定義部分だけ書くとシンプルに理解すると良いと思います。
ただし、抽象メソッドが定義されたクラスは抽象クラスとして定義しなければならない。というルールがあります。抽象クラスを定義する方法は、通常のクラス定義にabstract
修飾子をつけるだけです。
上記を踏まえてCouponクラスを修正すると、次のようになります。
プログラム例(Coupon.java)
public abstract class Coupon{ public String name; //クーポン名 public Coupon(String name){ this.name = name; } //クーポンの適用 public abstract int apply( int total ); }
- クラス定義部に
abstract
修飾子がついていますので、抽象クラスとなります。 - applyメソッドにも
abstract
修飾子がつけられ、処理部分が削除されています。
14-1-2 抽象クラスの特徴
抽象クラスは、処理の無いメソッドが存在する言わば未完成の設計図です。そのため、抽象クラスからはインスタンスを生成することはできません。
次のメインプログラムで確認してみましょう。
プログラム例(Main.java)
public class Main{ public static void main(String[] args){ Coupon coupon = new Coupon("テストクーポン"); } }
コンパイル結果
Main.java:3: エラー: Couponはabstractです。インスタンスを生成 することはできません Coupon coupon = new Coupon("テストクーポン");
メインクラスでインスタンス生成の文を実行すると、抽象クラスからインスタンスが生成できないことを示すエラーが表示されました。
14-1-3 抽象クラスと継承
抽象クラスを継承する子クラスは、継承されてきた抽象メソッドをオーバーライドする必要があります。抽象メソッドをオーバーライドする際は下のようにabstract
キーワードを取り除いて、定義部分をコピーし処理を追加するようにします。
//抽象メソッド public abstract int apply( int total ); //オーバーライドされたメソッド public int apply( int total ){ //処理を追加 }
抽象クラスを継承すると抽象メソッドまで継承されてしまうため、そのままにしておくと未完成の設計図になってしまいます。なので、メソッドをきちんと書き直す。というイメージで考えておくと良いでしょう(下図)。
今回の題材となっているAmountCouponクラスやRateCouponクラスは、すでにapplyメソッドをオーバーライドしていますので、特に問題なくこれまでどおり動作します。仮にCouponクラスを継承したAmountCouponクラスからapplyメソッドを削除した次のようなプログラムをコンパイルすると、エラーとなってしまいます。
プログラム例(AmountCoupon.java)
public class AmountCoupon extends Coupon{ public int amount; //割引額 //ここにあったapplyメソッドを削除 }
コンパイル結果
AmountCoupon.java:1: エラー: AmountCouponはabstractでなく、Coupon内のabstractメソッドapply(int)をオーバーライドしません public class AmountCoupon extends Coupon{ ^ エラー1個
このように抽象メソッドや抽象クラスを利用することで、Couponクラスの次のような役割を明確に示すことができます。
- Couponクラスは継承を前提としたクラスで、インスタンスを生成できない。
- applyメソッドはCouponクラスとその子クラスにapplyメソッドが存在することを示すための定義である。
14-2 特定の処理だけが共通したクラス
ここから新たに、下のような題材を使ってJavaの新たな要素についてご紹介したいと思います。
お店アプリにおいて、新たにメンバーズカードを取り扱います。メンバーズカードの仕様は次のとおりです。
- ポイントカードの一種であり、IDとポイントの情報を持つ。
- 会計の都度、3%の割引を受けられる。
14-2-1 MembersCardクラスの作成
まずは上記の仕様に基づき検討されたMembersCardクラスのプログラムを示します。仕様に「ポイントカードの一種」とあるため、PointCardクラスを継承して作成します。親クラスとなるPointCard.javaのプログラムも併せて掲載しています。
プログラム例(PointCard.java)
public class PointCard { public int id; //カード番号 public int point; //残ポイント public PointCard(int id, int point){ this.id = id; this.point = point; } //支払処理 public void payment(int price){ //金額の1%をポイントに加算 point += price*0.01; } }
プログラム例(MembersCard.java)
public class MembersCard extends PointCard{ //コンストラクタ public MembersCard(int id, int point){ super(id,point); } public int apply(int total){ //割引の適用 return total - (int)(total * 0.03); } }
PointCardクラスを継承し、3%の割引を適用するapplyメソッドを持っています。
14-2-2 CasherクラスでMembersCardクラスを扱う
MembersCardクラスにはCouponクラス同様の割引機能があります。この割引機能をCasherクラスで扱うことを考えます。
ここで少しおさらいになります。13章ではクーポン系のクラスをまとめて扱うしくみを使って、会計係に該当するCasherクラスを次のように定義していました。
プログラム例(Casher.java)
public class Casher{ public static void check(int total, Coupon coupon){ int newTotal = coupon.apply(total); System.out.println("お会計は" + newTotal + "円です。"); } }
お会計をするcheckメソッドは、Coupon型を引数とすることでその子クラスであるAmountCouponクラスやRateCouponクラスをまとめて取り扱えるようにしていました。
今回新規に作成したMembersCardクラスも割引機能であるapplyメソッドを持っていますので、Coupon系のクラス同様、上記Casher.javaの3行目で呼び出せるようにしたいと思います。
これを実現するにあたり、割引サービスをクラスとして定義したDiscountService.javaを作成することにします。
プログラム例(DiscountService.java)
public abstract class DiscountService{ public abstract int apply(int total); }
このDiscountServiceクラスは、CouponクラスとMembersCardクラスの共通点であるapplyメソッドの定義部分だけを抽象メソッドとして定義したものです。このクラスを下図のように、Coupon.javaとMembersCard.javaに継承させて、DiscountService.javaを共通の親クラスとして定義します。これにより、Coupon.javaとMembersCard.javaを同じ型で扱えるようにすることを考えます。
ですが、この方法には次のようないくつかの問題があります。
- MembersCardクラスとDiscountServiceクラスはis-a関連として適切ではない。「MembersCard is a DiscountService(メンバーズカードは割引サービスの一種である)」という定義は不自然。
- MembersCardクラスはすでにPointCardクラスを継承している。
※Javaでは複数のクラスを継承することができない。
したがって、この方法をとることはできません。このようなケースでは、インターフェースを利用することが適切な解決策となります。
14-3 インターフェース
14-3-1 インターフェースの定義
では早速、インターフェースを利用して14-2の問題の解決を進めましょう。厳密な理解には時間を要しますので、一旦はインターフェースを「クラスのように使えるもの」と理解しておいてください。定義方法は次のとおりです。
public interface インターフェース名{ //フィールドやメソッドなどを定義する。 }
インターフェースがクラスと類似している点は
- インターフェースを定義したファイルのファイル名はインターフェース名と同じにしなければならない。
- インターフェースを定義すると、インターフェース名を型として使える。
- 他のクラスはインターフェースの性質を引き継ぐことができる。
※ただし、インターフェースの場合は「継承」とは言わず「実装」と表現する。
などがあります。一方でクラスと異なる点は
- フィールドやメソッドを定義できるが、定義可能なものは以下に示すようなごく限られたものだけ。
- 抽象メソッド
static final
がついたフィールド など
- 1つのクラスに複数のインターフェースを実装できる。
- インスタンスは生成できない。
などです。ルールが多く使い方がイメージしづらいので、使いながら徐々に慣れていくと良いと思います。では、先ほど定義したDiscountServiceクラスをインターフェースとして定義しなおします。
プログラム例(DiscountService.java)
public interface DiscountService{ public abstract int apply(int total); }
こちらは、14-2-2のDiscountService.javaのabstract class
の部分をinterface
に書き換えただけのものです。インターフェースも抽象メソッドを定義することができます。
14-3-2 インターフェースの利用
インターフェースは「クラスに実装する」という使い方をします。インターフェースをクラスに実装するには、クラスの定義部分でそのことを宣言しなければいけません。クラスの継承とインターフェースの実装を同時に行うクラスの場合、定義部分は次のようになります。
class クラス名 extends 親クラス名 implements インターフェース名{ //フィールドやメソッドを定義する。 }
implements
というキーワードの後ろに、実装したいインターフェース名を記載します。
具体例で見ていきます。MembersCardクラスを上記の定義を使って作成すると次のようになります。
public class MembersCard extends PointCard implements DiscountService{ public MembersCard(int id, int point){ super(id,point); } public int apply(int total){ //割引の適用 return total - (int)(total * 0.03); } }
宣言部分は「MembersCardはPointCardの一種であり、なおかつ割引サービスの性質も併せ持つ」と読むと良いでしょう。
インターフェースの実装を宣言すると、抽象クラスと同様に抽象メソッドが引き継がれます(下図)。
したがって、MembersCardクラスはDiscountServiceクラスに定義されているapplyメソッドをオーバーライドする義務が生じます。今回はMembersCardがすでにapplyメソッドをオーバーライドしていたためコンパイルとして正常終了しています。
次にCouponクラスを、DiscountServiceインターフェースを使って再定義します。
プログラム例(Coupon.java)
public abstract class Coupon implements DiscountService{ public String name; //クーポン名 public Coupon(String name){ this.name = name; } }
- 定義部に
implements DiscountService
を追加しています。 - 抽象メソッドとして定義していたapplyメソッドは、DiscountServiceインターフェースから引き継がれています。
- 引き継がれてきたapplyメソッドは、抽象メソッドのままとしておきます。この場合、抽象メソッドが残る状態となりますので、Couponクラスは抽象クラスのままとしておきます。
ここまでに作成した関連するクラスやインターフェースを図にまとめると、次のようになります。
図ではapplyメソッド以外のメソッドやフィールドの表記は割愛しています。図に示すようにCouponクラスとMembersCardクラスに共通する親にあたるインターフェースを持つことができましたので、Casherクラス側でこれらをまとめて取り扱うことができます。
14-3-3 インターフェースを親にする効果
DiscountServiceインターフェースをCoupon系のクラスやMembersCardクラスの親としたことで、これらのクラスをDiscountService型でまとめて扱うことができます。この特徴を利用して、Casherクラスを修正すると次のようになります。
プログラム例(Casher.java)
public class Casher{ public static void check(int total, DiscountService service){ int newTotal = service.apply(total); System.out.println("お会計は" + newTotal + "円です。"); } }
checkメソッドの第2引数がDiscountService型になっただけです。では実際に次のメインクラスを使って動作を確認してみましょう。
public class Main{ public static void main(String[] args){ AmountCoupon coupon1 = new AmountCoupon("テスト割引",1500); MembersCard card = new MembersCard(1111,100); Casher.check(5000,coupon1); Casher.check(5000,card); } }
実行結果
お会計は3500円です。 お会計は4850円です。
- 6行目ではAmountCouponインスタンスを引数として渡し、5000円から1500円引きした3500円が計算結果として表示されています。
- 7行目ではMembersCardインスタンスを引数として渡し、5000円から3%(150円)引きした4850円が計算結果として表示されています。
checkメソッドの引数をDiscountService型とすることにより、AmountCouponインスタンスやMembersCardインスタンスを1つの型で受け取ることができるようになります。
インターフェースについては、一旦以上となります。少し難しい概念なので、本編とは別にトピック記事を掲載予定です。