Javaプログラミング入門 12章 [継承] Part1

Java

こんにちは。ECF Techブログ
担当 Michiharu.Tです。

Javaプログラミング入門記事の第12章をお送りいたします。
第12章のテーマは「継承」です。
第12章は2回のパートに分けて掲載します。今回はPart1です。

本連載の初回および章立ての一覧については下記のリンクから確認できます。

Javaプログラミング入門 0章
0章 Javaプログラミングを始めよう こんにちは。ECF Techブログ 担当 Michiharu.Tです。 この記事はプログラミング言語のJavaを、実際に動かしながら学びたい人のための学習教材となっています。手を多く動かすこと、感覚を...

解説動画

2024/03/03 解説動画を掲載しました。

12-1 共通点の多いクラス

12-1-1 取り扱うケース

本章では、お店アプリにおける次のようなケースを題材に、継承について学習します。

2種類のカード

お店で発行しているカードにはポイントカードプリペイドカードがあり、プリペイドカードの機能を実現するクラスを作成することになりました。

プリペイドカードは、通常のポイントカードの機能に加え、次の機能を持つものとします。

  • 現金を使って直接ポイントを追加できるチャージ機能がある。
  • 月の利用回数が一定数になると、以降の支払の都度、20ポイントのボーナスが付与される。

12-1-2 共通点を持つ2つのクラス

それでは次に、仕様を元に検討したPrepeaidCardクラスを示します。対比のため、9章で登場したPointCardクラスも併せて示します。

※コンストラクタは、継承におけるルールがやや複雑なため一旦除外しています。また、同様の理由でアクセス修飾子もすべてpublicとしています。

プログラム例(PrepaidCard.java)

public class PrepaidCard {
    public int id;          //カード番号
    public int point;       //残ポイント
    public int monthlyUse;  //月の利用回数

    public void payment(int price) {
        point += price*0.01;
        //利用回数を加算
        monthlyUse++;
        if( monthlyUse >= 2 ){
            //月2回以上の利用でボーナス20ポイント
            point += 20;
        }
    }

    //現金からチャージするメソッド
    public void charge(int cash){
        point += cash;
    }
}

プログラム例(PointCard.java)

public class PointCard {
    public int id;      //カード番号
    public int point;   //残ポイント

    //支払処理
    public void payment(int price){
        //金額の1%をポイントに加算
        point += price*0.01;
    }
}

PrepaidCardクラス固有の内容としては次のようなものがあります。

3行目:

public int monthlyUse;  //月の利用回数

月の利用回数を表すフィールド(monthlyUse)が追加されています。

15行目:

//利用回数を加算
monthlyUse++;
if( monthlyUse >= 2 ){
    //月2回以上の利用でボーナス20ポイント
    point += 20;
}
  • メソッド実行のたびに利用回数(monthlyUseフィールド)が1加算されます。
  • paymentメソッドは月2回以降の支払いの場合は20ポイントのボーナスが付与されます。

23行目:

public void charge(int cash){
    point += cash;
}

現金からポイントをチャージするchargeメソッドが定義されています。

この2つのクラスを図のように対比してみてみると、フィールドや処理の一部に共通点があることがわかります。

類似するクラス

仕様には 「通常のポイントカードの機能に加え・・・」 という記載がありますので、PrepaidCardクラスとPointCardクラスの基本的な機能は同じです。したがって、PointCardクラスに変更が発生するとPrepaidCardクラスにも変更が発生する可能性があります。

例えばポイント還元率が1% → 2%になった場合には

  • PointCard.javaの8行目
  • PrepaidCard.javaの7行目

の2箇所の修正が必要となり、保守性の観点からも好ましくありません。このようなケースで継承は役に立ちます。

12-2 継承の基本

12-2-1 継承とは

継承は、あるクラスのフィールドやメソッドを引き継ぎ、追加で必要なフィールドやメソッドだけを新たに定義して新しいクラスを作成する手法です。

継承

図はPointCardクラスを継承してPrepaidCardクラスを作成するイメージを表したものです。継承のしくみを用いることで、PrepaidCardクラスはPointCardクラスの持つフィールドやメソッドを引き継ぐことができます。これによりPrepaidCardクラス内では、プリペイドカードとして新たに追加したいフィールドやメソッドを定義するだけで良くなります。

またこの時、継承される側のクラスをスーパークラス(親クラスとも言う)、継承する側のクラスをサブクラス子クラスとも言う)と言います。

本講座では以降、「親クラス、子クラス」の表現を利用します。

図ではPointCardクラスが親クラス、PrepaidCardクラスが子クラスとなります。

12-2-2 継承の文法

それではここから、継承のしくみを利用して、PrepaidCardクラスのプログラムを書き換えます。子クラスが親クラスを継承する文法は次のとおりです。

public class 子クラス名 extends 親クラス名{
    //子クラスとして追加したい
    //フィールドやメソッドを定義する
}

上の文法を使用して作成したPrepaidCardクラスのプログラムを示します。

プログラム例(PrepaidCard.java)

public class PrepaidCard extends PointCard{
    public int monthlyUse;  //月の利用回数

    //現金からチャージするメソッド
    public void charge(int cash){
        point += cash;
    }
}
  • PointCardクラスを継承してPrepaidCardを作成しています。
  • PointCardクラスのフィールドやメソッドはPrepaidCardクラスに継承されているので、追加で必要なフィールドとメソッドだけを定義しています。

次にPrepaidCardクラスを利用するメインクラスのプログラム例を示します。

プログラム例(Main.java)

public class Main{
    public static void main(String[] args){
        PrepaidCard card = new PrepaidCard();
        card.payment(1000);
        card.charge(2000);
        System.out.println(card.point);
    }
}

メインクラスができましたので実行可能ですが、ここまでのプログラムを実行させるには次の3つのクラスのコンパイルが必要です。

  • PointCard.java
  • PrepaidCard.java
  • Main.java

実行結果

2010

PointCardクラスを継承して作成したPrepaidCardクラス、およびそのインスタンスは次のようなイメージとなります。

継承クラスのインスタンス

PrepaidCardクラスは、PointCardのフィールドとメソッドを引き継いでいます。図のようにPrepaidCardクラスがPointCardクラスからの継承部分を持っているイメージです。インスタンスについても、内部にPointCardクラスからの継承部分を持っている。と考えると、継承における文法全般のイメージがしやすくなります。

このような構造になっているため、メインクラスからはPointCardクラスから引き継いだフィールドやメソッドが利用できるようになります(下図)。

実行イメージ1

メインクラスの処理を順番に説明すると、

3行目PrepaidCard card = new PrepaidCard();
PrepaidCardインスタンスを生成し、変数cardに代入します。

4行目card.payment(1000);
cardの指すインスタンスのpaymentメソッドを呼び出します。PointCardクラスで定義されたpaymentメソッドの処理が行われるため、1%のポイントが付与され、pointフィールドの値は10となります。

5行目card.charge(2000);
cardの指すインスタンスのchargeメソッドを呼び出します。引数の2000をpointフィールドに加算するため、pointフィールドの値は2010となります。

6行目System.out.println(card.point);
cardの指すインスタンスのpointフィールドの値を表示します。2010が表示されます。

まずは継承を使ったPrepaidCardクラスの第1段階ができました。ですがまだ、コンストラクタが利用できない。 という不便な問題があります。また、PrepaidCardの仕様で求められている「月の利用回数が一定数になると、以降の支払の都度、20ポイントのボーナスが付与される。」という点も実現できていません。

以降の節では、これらの問題を段階的に解決しながら、PrepaidCardクラスを改善していきます。

12-3 継承とコンストラクタ

12-3-1 子クラスのコンストラクタのルール

継承を活用してクラスを作成する際のコンストラクタの使い方について説明します。まず初めに、PointCardクラス側で除外していたコンストラクタを復活させます。次のようなプログラムになります。

プログラム例(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;
    }
}

次にPrepaidCardクラスにコンストラクタを追加しますが、ここでコンストラクタに関するルールに注意が必要です。そのルールとは「子クラスのコンストラクタは、処理の最初で親クラスのコンストラクタを呼び出さなければならない」というものです。

親クラスのコンストラクタを呼び出す文法は次のとおりです。

super(引数...);

コンストラクタを含むPrepaidCardクラスのプログラム例を示します。

プログラム例(PrepaidCard.java)

public class PrepaidCard extends PointCard{
    public int monthlyUse;  //月の利用回数

    //コンストラクタ
    public PrepaidCard(int id, int point){
        //親クラスのコンストラクタを呼び出す
        super(id, point);
        this.monthlyUse = 0;
    }

    //現金からチャージするメソッド
    public void charge(int cash){
        point += cash;
    }
}

5行目にコンストラクタを定義しています。その中の7行目のsuper(id, point);が親クラスのコンストラクタを呼び出す処理です。どのように動作するかを次のメインクラスを使って説明します。

プログラム例(Main.java)

public class Main{
    public static void main(String[] args){
        PrepaidCard card = new PrepaidCard(2,50);
        System.out.println(card.id);
        System.out.println(card.point);
    }
}

実行結果

2
50

解説
Main.javaの3行目が実行された時のイメージを下図に示します。

スーパークラスのコンストラクタ呼び出し

PrepaidCard card = new PrepaidCard(2,50);でPrepaidCardインスタンスが生成されると同時に、コンストラクタが呼び出されます。

PrepaidCardクラスのコンストラクタでは、始めにsuper(id, point);でスーパークラスのコンストラクタを呼び出しています。引数には、PrepaidCardのコンストラクタの引数をそのまま渡していますので、メインクラスから渡された引数250がそのまま渡されます。

PointCardクラスのコンストラクタは、渡された引数をフィールドに代入する処理です。したがって、idとpointのフィールドにそれぞれ250が代入されます。

処理はPrepaidCardのコンストラクタに戻り、this.monthlyUse = 0;が実行されます。

12-3-2 子クラスのデフォルトコンストラクタ

2つのコンストラクタが正しく作成できるようになったところで、子クラスのデフォルトコンストラクタについて説明したいと思います。説明のためにPrepaidCardクラスのコンストラクタを再度削除し、次のような状態にします。

プログラム例(PrepaidCard.java)

public class PrepaidCard extends PointCard{
    public int monthlyUse;  //月の利用回数

    //現金からチャージするメソッド
    public void charge(int cash){
        point += cash;
    }
}

この状態でPrepaidCard.javaを再度コンパイルすると、次のようなエラーが発生します。

PrepaidCard.java:1: エラー: クラス PointCardのコンストラクタ PointCardは指定された型に適用できません。
public class PrepaidCard extends PointCard{
       ^
  期待値: int,int
  検出値:    引数がありません
  理由: 実引数リストと仮引数リストの長さが異なります
エラー1個

このエラーは子クラスのデフォルトコンストラクタによって引き起こされています。

あるクラスを継承したクラス(子クラス)でコンストラクタを記述しなかった場合、次のようなデフォルトコンストラクタが作成されます。

クラス名(){
    super();
}

引数は無く、ブロック内にsuper();の処理が入ったコンストラクタが自動生成されます。そのため、次のような流れでエラーが発生しています。

子クラスのデフォルトコンストラクタ

このように、子クラスのコンストラクタの動作は、時に予期しないエラーを発生させることがありますので注意が必要です。

(Part2に続きます)

タイトルとURLをコピーしました