富士通

 

COBOL技術者のためのJava言語入門
7章:クラスの継承

[索引]  継承の例 |  継承の仕様 |  サブクラスのコンストラクタ |  メソッドオーバーライド |  スーパークラス変数によるサブクラスの操作 |  オブジェクトコンポジション |  抽象クラス |  インターフェース |  instanceof演算子 |  コラム 

さて、いよいよオブジェクト指向言語のクライマックスである継承です。 継承を使うと基準となるクラスに機能を追加した新しいクラスを作ることができます。 継承はカプセル化と並んでオブジェクト指向の重要な概念です。 継承そのものは比較的単純な機能ですが、関連していくつかの概念が出てきます。 これらを包括的に理解することが重要です。

この章には、かなり高度な内容も含まれています。 その部分は(※上級)と記述しました。 難しければ、とばして読んでいただいてかまいません。

継承の例

例を使って継承の働きを説明します。 まず基準となるクラスの例として、おなじみのPersonクラスを再度示します。 複雑になるのでsetメソッドやコンストラクタは省略してあります。

        class Person {
          String name = "";             // 変数 氏名
          int age = 0;                  // 変数 年齢

          String getName() {            // メソッド 氏名取得
            return name;
          }

          int getAge() {                // メソッド 年齢取得
            return age;
          }
        }

継承のし方 ★

上記のPersonクラスを継承してStudent(学生)クラスを定義してみましょう。 ここでは学生に特有の変数grade(学年)とそれを取得するメソッドを追加します。 継承したクラスを作るには以下のようにクラス名の直後にキーワードextendsと継承元のクラス名Personを書きます。 中括弧 { } の中に追加するメンバーを定義します。

        class Student extends Person {  // Personを継承することを示す
          private int grade=0;          // 変数 gradeを追加

          int getGrade() {              // メソッド getGradeを追加
            return grade;
          }
        }

継承の利点

継承の元となるクラス(ここではPerson)をスーパークラス、継承して新しく作るクラス(ここではStudent)をサブクラスと言います。 サブクラスのインスタンスはスーパークラスのメンバーに加え、サブクラスで追加したメンバーを使うことができます。 継承の原語であるinheritance(インヘリタンス)には遺産とか相続の意味があります。 継承とは、まさにスーパークラスの遺産をそっくり受け継いで、最初から自分が持っていたかのように利用することです。

ただそれだけなら、わざわざ継承など使わずに、コピー&ペーストで複写すれば良いのではないかと思われるかも知れません。 しかし、プログラムには修正が付き物です。 後から修正しようとしたときにコピー&ペーストで作成した部分を漏れなく探すのは大変です。 コードの重複を避けることが保守のしやすいプログラム開発のポイントです。

継承したメンバーの使い方

以下ではStudentのインスタンスstがスーパークラスのメンバーとサブクラスのメンバーを使う様子を示します。

        Student st = new Student();     // Studentクラスのインスタンスstをnew
        st.getAge();                    // 元のクラスのメンバーも呼べる
        st.getGrade();                  // 新たに定義したメンバーも呼べる

※ サブには元のものより劣る語感がありますが、サブクラスはスーパークラスより機能が多いことに注意してください。 C++ではスーパー/サブではなく、Base(基本)/Derived(派生)と名付けていました。 この名前の方が特徴を表しているかもしれません。 しかしSuper/Subの方が簡単でいいですね。 本記事では紙面の節約のために親クラス/子クラスと言うこともあります。


【確認問題】 7.1

前章6.4で作ったCircleクラスを継承してColoredCircleクラスを作ってください。 このクラスに色を保持するString型の変数colorを定義してください。

継承の仕様

継承して作った子(サブ)クラスをさらに継承して孫のクラスを作ることができます。 この連鎖の階層には制限がありません。 しかし、通常の業務プログラムでは、むやみに階層の数を増やすのはよくないとされています。

あるクラスは上位のすべてのクラスの(privateでない)メンバーを使うことができます。 また1つのクラスから複数の子クラスを作ることができます。 しかし、逆に1つのクラスが複数の親のクラスを継承することはできません。 すなわち継承の関係はグラフ理論でいう木(tree)構造になっています。

多重継承

1つの子クラスが複数の親クラスを同時に継承することを多重継承と言います。 C++では多重継承ができましたが、Javaではプログラムの構造が複雑になりすぎるという理由で、禁止されています。 同等の機能は後に述べるインターフェースを使って実現します。

Objectクラス

JavaにはObjectという特別なクラスがあります。 このクラスは継承を指定しないクラスに対し暗黙的に継承元になります。 すなわち、以下の 01: のように書いても、02: のように書いたのと同じことです。 あるクラスがObject以外の親クラスを継承しても、その親クラスまたはその上位のクラスがどこかでObjectを継承するので、Objectはすべてのクラスのスーパークラスとなっています。 Objectクラスは木構造の根(root)にあたります。

  01:   class Person {
  02:   class Person extends Object {

Objectクラスではすべてのクラスに共通のメソッドを提供しています。 例えば、各インスタンスの説明を文字列で返すtoStringメソッドや、後にスレッドの章で説明するwaitメソッドなどを提供しています。

サブクラスのコンストラクタ

サブクラスはスーパークラスのメンバーを引き継ぎますが、コンストラクタだけは自前で作らねばなりません。 先にPersonを継承して定義したStudentクラスのコンストラクタを作ってみましょう。

        Student(String n, int a, int g) {       // 氏名,年齢,学年を設定
          super(n, a);          // Personクラスのコンストラクタを呼ぶ
          grade = g;
        }

最初のsuper(n,a)はスーパークラス(ここではPersonクラス)のコンストラクタを呼ぶメソッドです。 これはnameとageをセットしているので、ここでは残りのgradeをセットしています。

コンストラクタに関する仕様と注意点 ★

コンストラクタの先頭行にthisまたはsuperのいずれかがないときは、コンパイラが自動的にsuper()を付加し、親クラスのコンストラクタを呼びます。 したがって、このとき親クラスでデフォルトコンストラクタを定義していないとコンパイルエラーになってしまいます。 前章で述べたようにコンストラクタが1つも定義されていないと、自動的にデフォルトコンストラクタが生成されるので問題はありませんが、デフォルトコンストラクタ以外のコンストラクタを定義したときは、必ずデフォルトコンストラクタも定義するようにしてください。


【確認問題】 7.2

前問(7.1)で作ったColoredCircleクラスに色と半径をセットするコンストラクタを追加してください。 super()を活用してください。


【確認問題】 7.3

以下のクラスがあります。

  class A {
    A() {
    System.out.print("A0 ");
    }
    A(int a) {
    System.out.print("A" + a +" ");
    }
  }
  class B extends A {
    B() {
    System.out.print("B0 ");
    }
    B(int b) {
    super(b);
    System.out.print("B" + b +" ");
    }
  }

このとき次のプログラムを実行すると何が表示されますか。

    B b1 = new B();
    B b2 = new B(1);

メソッドオーバーライド

メソッドオーバーライドとはスーパークラスで定義したのと同じシグニチャのメソッドをサブクラスで再定義することです。 前章で説明したオーバーロードと名前が似ているのでまちがえないでください。

        ★オーバーロード…同名だが引き数が違う:単なる名前の節約
        ★オーバーライド…引き数も含めて同じ:サブクラスでの再定義

オーバーライドの働きを例で示しましょう。 この章の最初に挙げたPersonとStudentの例を使います。 Personクラスで次のようなメソッドdisplayが定義されていたとします。 これは氏名と年齢を表示するメソッドです。

        void display() {        // 親クラスで定義されているdisplayメソッド
          System.out.print("  氏名:" + name);
          System.out.print("  年齢:" + age);
        }

StudentクラスはPersonを継承しているので、displayメソッドを使うことができます。 しかし、このままではせっかくセットした学年は表示されません。 そこで次のように学年も表示するようにメソッドを定義しなおします。 このとき同じdisplayと言う名前にするのがミソで、これがメソッドオーバーライドです。

        void display() {        // 子クラスでオーパーライドするdisplayメソッド
          super.display();      // 親のdisplay()を呼んで氏名と年齢を表示
          System.out.print("  学年:" + grade);
        }

これにより、同じdisplayメソッドでも、対象がStudentクラスのときは学年も表示されるようになります。 このように1つの名前で複数の振る舞いを示すことを、ポリモーフィズム(polymorphism:多態性)と言います。 ポリモーフィズムはカプセル化、継承と並んで、オブジェクト指向の3大概念と言われています。

オブジェクト指向プログラミングの神髄

少し大げさですが、この辺がオブジェクト指向プログラミングの神髄といえます。 すなわち呼ぶ側は対象(のある性質)のことを知らずにプログラムを作っているので、対象(のその性質)が変化(つまり仕様変更)しても、プログラムコードに影響が出ようがありません。 また学生以外に、例えば学生クラスの他にビジネスマンクラスを追加して、部署名を表示するようにしたとしても、呼び出し側の変更は不要です。

換言すれば、当事者だけが知っていることにより(その仕様に関する)関心の分離(Separation of concerns)が起こり、簡明で変化に強いプログラムを作ることができます。 これこそがオブジェクト指向プログラミングの神髄であり、これによって構築だけでなく保守も容易になると言うわけです。

メソッドオーバーライドの条件 ★

オーバーライドするには以下の全ての条件を満たしている必要があります。

  • スーパークラスのメソッドと名称、引き数の型/数/順序、戻り値の型が同じ。
  • スーパークラスのメソッドがfinalでなくかつprivateでない。
  • アクセス修飾子の範囲がスーパークラスのメソッドと同じかより広い。
  • throws文で出す例外の種類がスーパークラスのメソッドと同じか少ない。

【確認問題】 7.4

前問(7.2)で作成したColoredCircleクラスにdisplayメソッドを追加(オーバーライド)して「半径xxの円です(改行)色はyyです」を表示するようにしてください。
(ヒント) Circle クラスのradiusはprivateなのでColoredCircleクラスからは直接アクセスできません。

スーパークラス変数によるサブクラスの操作(※上級)

ここでは、オーバーライドによるポリモーフィズムを補完する2つの機能について説明します。 第1の機能は以下のようにスーパークラスの型の変数にサブクラスのインスタンスを代入することができることです。

        Super s1;       //s1はSuperクラスの変数であることを宣言
        s1 = new Sub(); //Subクラスのインスタンスを生成してs1に代入

第2の機能はメソッドがサブクラスでオーバーライドされていると、上記のような変則的な変数s1ではオーバーライドされた方のメソッドが動作することです。 すなわちs1のように表面的にはSuperクラスだが、実体はSubクラスの場合には、実体側のメソッドを実行するのです。 これらの機能の効果を例で見てください。

まず前章の最後で示したクラスの配列の例に、PersonだけでなくStudentも代入します。 以下では山田さんだけがStudentでgrade(学年)の変数を持っています。

        Person[] p = new Person[3];
        p[0] = new Person("田中", 27);
        p[1] = new Student("山田", 20, 3);      // Studentクラスをnewして代入
        p[2] = new Person("佐藤", 34);

この配列はPerson型で宣言されていますが(1行目)、前述の第1の機能により、そのサブクラスであるStudentクラスも要素とすることができるのです(3行目)。 さて、ここで先にメソッドオーバーライドの項で定義したdisplayメソッドを以下のようにして呼んでみましょう。

        for(int i=0; i<p.length; i++) {
          p[i].display();
          System.out.println("");       // 改行の出力
        }

すると前述の第2の機能により、インスタンスが学生のときだけオーバーライドが働き学年が追加され以下のように出力されます。

        出力結果        氏名:田中  年齢:27
                        氏名:山田  年齢:20  学年:3
                        氏名:佐藤  年齢:34

ここでのポイントは、表示するプログラム(print文を持つプログラム)は、学年を表示するか否かにまったく関知していないことです。 これが2つの機能の効果です。 ポリモーフィズムがうまく活かされて、関心の分離が実現できていることを実感していただけるでしょうか。

※ 第1の機能の逆である「サブクラスの型の変数にスーパークラスのインスタンスを代入すること」はできません。 キャストして無理に代入すると実行時にClassCastException例外が発生します。 この代入を許さない理由はサブクラスのみで定義されているメンバーへのアクセスを実行段階で拒否しなければならないからです。

※ この第2の機能はdynamic binding(動的束縛)と呼ばれることがあります。

オブジェクトコンポジション(※上級)

継承を覚えるとすぐに使ってみたくなりますが、実際には継承がフィットする状況は必ずしも多くはありません。 オブジェクトコンポジションを使う方が適切な場合も多いのです。 オブジェクトコンポジションとは、あるクラス A に別のクラス B を取り込むことです。 すなわち、クラス A の変数としてクラス B を定義することです。 取り込んだクラスに処理をまかせることを委譲と呼んでいます。

オブジェクトコンポジションの例を示しましょう。 Personが携帯電話の契約をしたとします。 契約を表すクラスPlanを作り、その中に基本料金や無料通話分などの変数と料金を計算するメソッドを定義します。 そしてPersonの中に、Planクラスを取り込み、p.charge()のようにして、料金計算はPlanクラスに委譲します。 これによってPersonクラスでは契約形態やそれに関わる料金計算のことは知らずにすみ、プログラムの作成と保守が容易になると言うわけです。

        class Person {          // Personクラスの定義
          String name;
          int age;
          Plan p;               // Planクラスを取り込む
            …
            pay = p.charge();   // Planクラスに処理を委譲
            …
        }

        class Plan {            // Planクラスの定義
          int base;             // 基本料金
          int noCharge;         // 無料通話時間
          public int charge() { // 料金計算メソッド
             …

is-a関係とhas-a関係

サブクラスはスーパークラス(の一種)であると言えるので、継承関係はis-a関係と呼ばれます。 これに対しオブジェクトコンポジションの関係はhas-a関係または(主述を逆にして)part-of関係と呼ばれます。 自動車とタイヤとの関係はhas-a関係、自動車と乗り物との関係はis-a関係です。

抽象クラス(アブストラクトクラス)

スーパークラスレベルでは何も書くべきことがないメソッドがあります。 例えばShape(図形)クラスでdraw(描画)メソッドがあるとします。 しかし図形という抽象的な概念の図を描画するといっても何をしてよいかわかりません。 図形クラスを継承して円のクラスを定義して初めて、中心と半径を取得するなどして描画を実行できます。

このような場合にはShapeクラスではdrawメソッドの名前や引き数の形式(正確にはシグニチャ)だけを定義して、内容を書きません。 このように内容がないメソッドが抽象メソッドです。 抽象メソッドは先頭にabstractキーワードを書き、中括弧 { } は書かずにセミコロンで終わります。 抽象メソッドを1つでも含むとそのクラスは抽象クラスになり、クラス宣言にもabstractを付けます。 以下に抽象クラスの例を示します。

        abstract class Shape {  // 先頭のabstractが抽象クラスであることを示す
          private int width;
          void setWidth(int w){ // これは非抽象メソッド。内容がある
            width = w;
          }
          abstract void draw(); // これは抽象メソッド。内容がない
        }

抽象クラスはnewできない

抽象クラスはnewすることはできません。 これを許すと実行できないメソッドを持つインスタンスが存在することになるからです。 抽象クラスを継承したクラスを作り、未定義のすべてのメソッドに対し具体的な処理が存在するメソッドをオーバーライドして初めてnewできます。 このことをメソッドを実装すると言います。

抽象クラスの意義

わざわざスーパークラスで抽象クラスのdrawメソッドなどを書かずに、サブクラスで初めてdrawを定義すればよいではないか、と考える方もあるかも知れません。 しかし抽象メソッドとして定義しておくと、サブクラスでそれを実装せずにnewしようとするとコンパイルエラーになってしまうので、メソッドの記述(引数の形式も含めて)が強要でき、誤りの少ないプログラムが書けるのです。 この考えは次項のインターフェースでさらに発展していきます。

抽象クラスに関する仕様のまとめ ★

  • 抽象メソッドを1つでも持つクラスは抽象クラスになる。 抽象クラスにはabstractキーワードを付けなければならない。
  • 抽象クラスはnewすることができない。 サブクラスですべての抽象メソッドにコードを実装して、抽象クラスでなくして初めてnewできるようになる。

インターフェース

抽象クラスには抽象メソッドが少なくとも1つ存在しますが、抽象ではない(具体的な処理が記述されている)メソッドも混在しているのが普通です。 このような混在を止めて、すべてを抽象メソッドにしてしまったのが、インターフェースです。 インターフェースの例を示します。

        interface Shape {
          int width=3;
          void draw();
        }

インターフェースを定義するにはclassの代わりにinterfaceキーワードを使います。 インターフェースはpublic属性を持った抽象メソッドと、初期値付きのpublicかつstaticかつfinalな変数だけから構成されています。

インターフェースの実装

インターフェースを基にして、実体のあるクラスを作ることをインターフェースを実装すると言います。 インターフェースを実装したクラスの書き方は、継承のときと似ていますが、キーワードはextendsではなくimplementsを使います。 またクラスの継承と異なり、1つのクラスで複数のインターフェースを実装することができます。 以下に実装の例を示します。 以下ではクラスCircleはShapeとSerializableの2つのインターフェースを実装しています。

        class Circle implements Shape, Serializable {
          void draw(){
              // 円の描画処理
        }

※ 前述のようにJavaでは複数のクラスの継承(多重継承)は禁じられているので、必要がある場合にはインターフェースを使って同等の機能を実現します。

インターフェースの継承

あるインターフェースを別のインターフェースで継承することができます。 そのときはextendsを使います。 インターフェースはクラスと違って複数のインターフェースを継承できます。 インターフェース B と C を継承してインターフェース A を定義するときは次のように書きます。

        interface A extends B, C {

インターフェースに関する仕様のまとめ ★

  • インターフェースに記述できるのは抽象メソッドと初期値付き変数のみである。
  • インターフェースの変数の属性は必ずpublicかつstaticかつfinalになる。
  • インターフェースのメソッドの属性は必ずabstractかつpublicになる。
  • インターフェースを実装するときはimplementsを使う。 複数可能である。
  • インターフェースを継承するときはextendsを使う。 複数可能である。
  • クラスの継承とインターフェースの実装とを同時に行うこともできる。

仕様書としてのインターフェース(インターフェースの意義)

インターフェースは書式だけを指定したものですので、メソッドの使い方を定義した「仕様書」であると言えます。 名前と引数構成がよく吟味された抽象クラスがうまく用意されていれば、プログラマーは要求されたメソッドを記述していくだけで望ましいクラス構成が実現できます。 初めにインターフェースをきちんと設計しておき、それを実装する形でプログラムを書くのが優れた開発方法であると言われています。

instanceof演算子

継承や実装を行うと、あるインスタンスが複数のクラスやインターフェースに属していることになります。 インスタンス x がクラス A に属しているか否かは以下のようにして調べることができます。

        if(x instanceof A)

A と B をインターフェース、X と Y と Z をクラスとします。 X が Y を継承し、かつ Y が Z を継承していて、さらに X が A と B を実装しているとき、X のインスタンス x はABXYZのすべてに属しています。 またObjectクラスはすべてのクラスのスーパークラスですので、x はObjectにも属しています。


【確認問題】 7.5

以下の文で正しいものを全部選択してください。

(1)オーバーライドは同一クラスのメソッド間でも行うことができる。
(2)抽象クラスには抽象メソッドしか定義できない。
(3)インターフェースを実装したクラスを作るにはimplementsキーワードを使う。
(4)インターフェースが別のインターフェースを継承するにはextendsを使う。
(5)1つのクラスは複数のインターフェースを実装できる。
(6)インターフェースにはコンストラクタを持たせることができる。


☆☆☆コラム☆☆☆ ---- どうやったらオブジェクト指向になるか

Javaはオブジェクト指向言語ですが、Javaを使ったからと言って必ずしもオブジェクト指向が実現できるわけではありません。 この様子を以下に例で示します。 以下の内容は解説記事「オブジェクト指向プログラミングへの道」のダイジェスト版です。 本編にはもう少し詳しく書かれていますので、ご参照ください。

【課題】
ある携帯電話会社の料金プランは次の通りです。
  楽々プラン  基本料金:4000円  通話料:40円/分  無料通信分:2000円
  得々プラン  基本料金:6000円  通話料:25円/分  無料通信分:4000円

このとき、以下のユーザの支払額を求めてください。
  高橋さん  契約:楽々プラン  通話時間:35分
  鈴木さん  契約:楽々プラン  通話時間:55.5分
  加藤さん  契約:得々プラン  通話時間:200分

【プログラム1】
public class Payment {
  public static void main(String[] args) {
    charge("高橋", 1, 35);
    charge("鈴木", 1, 55.5);
    charge("加藤", 2, 200);
  }

  static void charge(String name, int plan, double usedMinute) {
    int sum = 0;
    if(plan == 1) {
      double chouka = 40 * usedMinute - 2000;
      if(chouka<0) sum = 4000;
      else sum = (int)(4000 + chouka);
    } else if(plan == 2) {
      double chouka = 25 * usedMinute - 4000;
      if(chouka<0) sum = 6000;
      else sum = (int)(6000 + chouka);
    }
    System.out.println(name + "さんの今月の支払額は" + sum + "円です。");
  }
}

いかがでしょうか。 多少の改良の余地はあるでしょうが、簡明でわかりやすい立派なプログラムだと思います。 この課題だけに限れば、このプログラムで十分です。 しかし、実際に業務で使うプログラムとしては問題があります。

それは拡張性です。 この課題には、今後、次のような拡張が考えられます。

  • 着信中心のユーザや、ヘビーユーザ用の新プランの開設
  • 通話相手や時間帯による通話料の相違など料金計算ロジックの多様化
  • メールサービスや支払い処理など通話料金計算以外の業務処理への拡張

これに対応できるようにしたのが、以下のプログラム2です。

【プログラム2】
public class Payment {
  public static void main (String[] args) {
    User takahashi = new User("高橋", new RakurakuPlan(35));
    User suzuki = new User("鈴木", new RakurakuPlan(55.5));
    User katou = new User("加藤", new TokutokuPlan(200));
    takahashi.printPayment();
    suzuki.printPayment();
    katou.printPayment();
  }
}

class User {
  String name;
  Plan plan;
  public User(String n, Plan p) {
    name = n;
    plan = p;
  }
  void printPayment() {
    System.out.println(name +"さんの今月の支払額は"+ plan.charge() +"円です");
  }
}

class Plan {
  int base, noCharge, chargePerMinute;
  double usedMinute;
  public int charge() {
    int sum;
    double chouka = chargePerMinute * usedMinute - noCharge;
    if(chouka<0) sum = base;
    else sum = (int)(base + chouka);
    return sum;
  }
}

class RakurakuPlan extends Plan{
  RakurakuPlan(double used) {
    base = 4000;
    noCharge = 2000;
    chargePerMinute = 40;
    usedMinute = used;
  }
}

class TokutokuPlan extends Plan{
  TokutokuPlan(double used) {
    base = 6000;
    noCharge = 4000;
    chargePerMinute = 25;
    usedMinute = used;
  }
}

このプログラムはクラスに分割され、継承やオブジェクトコンポジションが使われています。 20行から50行とサイズも2倍以上になりました。 しかし、このプログラムでは、先に示した拡張性の問題(の一部)が次のように改善されています。

  • 新プランへの対応はPlanクラスを継承したサブクラスを作ることで可能です。
  • 料金計算ロジックの拡張は、このプログラムでは困難です。 ここでは示しませんが、Planをクラスからインターフェースにして、実装を下位のクラスに任せると、料金計算ロジックの変更にも対応できます。
  • この構造では、現業務に影響せずに、他の業務処理を追加するのは容易です。 例えばメールの料金計算を追加するなら、MailServiceを作りUserクラスに取り込む(オブジェクトコンポジション)ことで、他の処理とは独立したプログラムとすることができます。

ただし、本当に拡張する可能性があるかを見極めることは重要です。 ありもしない拡張に備えてプログラムを複雑にするのはばかげています。