継承(inheritance)

オブジェクト指向の目標の一つに,ソフトウェアの再利用ということがある. プログラムを作る際に,すべてを1から作るのは大変である.人が作った,似 たようなことをするプログラムをちょっと変更して使うという手はあるが, などの問題がある.

そのため,人が作ったかもしれないあるクラスの定義を拡張して,新たなク ラスを作成する手段が用意されている.これをクラスの継承(inheritance)と 呼び,元のクラスを親クラス(又はスーパークラス super class),新たに作成 されたクラスを子クラス(又はサブクラス sub class)と呼ぶ.


サブクラスの宣言

サブクラスの宣言は以下のようにおこなう

class 子クラス名 extends 親クラス名 {
 クラス宣言の本体
}
たとえばこれまでの例で,Point2Dを親クラスとして,nameというString型の データフィールドも持つPointNameというクラスを作ってみよう.
class Point2D {
  public double x,y;
  public Point2D(double xx,double yy){
    x=xx; y=yy;
  }
  public double length(){
    return Math.sqrt(x*x+y*y);
  }
  public double distance2D(Point2D point2){
    double dx=(x-point2.x),dy=(y-point2.y);
    return Math.sqrt(dx*dx+dy*dy);
  }
}

  // Point2Dクラスのサブクラスとして PointNameクラスを作成するので、
  // 「extends Point2D」をつける。
class PointName extends Point2D {
    // Point2Dクラスには含まれないインスタンス変数 name を宣言する。
  public String name;
    // PointNameクラスのコンストラクタ
  public PointName(double xx,double yy,String n){
      // 親クラスであるPoint2Dクラスのコンストラクタを呼ぶ。
      // (x=xx; y=yy;)に当たる部分が実行される。
    super(xx,yy);
    name=n;
  }
}

class ObjectTest6 {
  public static void main(String[] args){
    PointName p1,p2,p3;
      // PointNameクラスのコンストラクタを呼び出しオブジェクトを作成する。
    p1=new PointName(0.0,0.0,"Point1");
    p2=new PointName(1.0,1.0,"Point2");
    p3=new PointName(0.0,1.0,"Point3");
      // Point2Dクラスにはなかったインスタンス変数 name を参照でき、
      // Point2Dクラスで定義したlengthメソッドを新たな定義なしに使える。
    System.out.println("length of "+p1.name+" = " +p1.length());
    System.out.println("length of "+p2.name+" = " +p2.length());
    System.out.println("length of "+p3.name+" = " +p3.length());
    System.out.println("distance between "+p1.name+" and " +p2.name+" = "+p1.distance2D(p2));
    System.out.println("distance between "+p2.name+" and " +p3.name+" = "+p2.distance2D(p3));
    System.out.println("distance between "+p3.name+" and " +p1.name+" = "+p3.distance2D(p1));
  }
}
これまでの Point2Dに加えて,今後の説明のため,原点との距離を記述する lengthというメソッドも加えている.

このように PointNameの定義の中では,データフィールド x, yやメソッド distanceの宣言をおこなっていないが,親クラスPoint2Dの性質を受け継いで, 使うことができる.コンストラクタ PointNameの中で superとやって呼び出し ているのは親クラスのコンストラクタである.


メソッドのオーバーライド

Point2Dを親クラスとして,3次元のPointであるPoint3Dを定義することを考え てみよう.今度は,lengthの定義を変更してみる.
class Point2D {
  public double x,y;
  public Point2D(double xx,double yy){
    x=xx; y=yy;
  }
  public double length(){
    return Math.sqrt(x*x+y*y);
  }
  public double distance2d(Point2D point2){
    double dx=(x-point2.x),dy=(y-point2.y);
    return Math.sqrt(dx*dx+dy*dy);
  }
}

  // Point2Dクラスのサブクラスとして、Point3Dクラスを定義する。 
class Point3D extends Point2D {
    // Point2Dクラスにはなかったインスタンス変数 zが加わる。
  public double z;
    // Point3Dクラスのコンストラクタの定義
  public Point3D(double xx,double yy,double zz){
      // 親クラスである Point2Dクラスのコンストラクタを呼び出す。
    super(xx,yy);
    z=zz;
  }
    // 親クラスである Point2Dで定義した length はPoint3Dでは使わずに、
    // ここで再定義する。
  public double length(){
    return Math.sqrt(x*x+y*y+z*z);
  }
}

class ObjectTest7 {
  public static void main(String[] args){
    Point3D p1,p2,p3;
    p1=new Point3D(0.0,0.0,1.0);
    p2=new Point3D(1.0,1.0,1.0);
    p3=new Point3D(0.0,1.0,0.0);
      // p1はPoint3Dクラスのインスタンスなので、
      // lengthメソッドは Point3Dクラスのメソッドが呼ばれる。
    System.out.println("length of p1 = "+p1.length());
    System.out.println("length of p2 = "+p2.length());
    System.out.println("length of p3 = "+p3.length());
  }
}
上のように,親クラスで定義されているメソッドを同じ名前(及び同じ引数の 型)で再定義することをメソッドのオーバライド(override)と呼ぶ.

親クラスの型の変数に子クラスの型のオブジェクトを代入することはできる. その場合も,動的メソッドの呼び出しをした場合は子クラスでオーバーライド されたメソッドのプログラムが実行される.

この例では親クラス Point2D,子クラス Point3Dなので,

 Point2D p1; // 親クラスの変数
 p1 = new Point3D(1.0,1.0,1.0); // 子クラス(Point3D)のオブジェクトを代入
とでき,その後で,
 double len=p1.length(); 
のように動的メソッド length を呼び出すと,変数の型である Point2Dで定義 されたlengthではなく,インスタンスの型である Point3Dで定義された lengthが呼ばれる.

子クラスの型の変数に親クラスのオブジェクトを代入する際には,キャスト演 算子を使って,「(子クラス名)式」のように使う.ただし,データフィールド をアクセスしようとしたり,メソッドを呼び出そうとすると,実行時にエラー が発生する.

これでは,意味がないようだが,子クラスのオブジェクトを親クラスの変数に 代入したあとで,また子クラスの変数に代入するという特殊な用途で役に立つ.
それでは,PointNameとPoint3Dの両方の性質を受け継いだクラスは作れるの だろうか? これは,オブジェクト指向言語では多重継承(multiple inheritance)と呼ばれるもので,効率的な実装が難しい上に,プログラムの設 計においては,あまりメリットがないと言われている.C++ は途中のバージョ ンから多重継承を言語仕様に入れたが,これは失敗だったという専らの評判で ある.

Java言語では多重継承に変わるものとして,インタフェースの宣言とその実 装という仕組みを用意しているが,これに関しては後回しにする.

すべてのクラスは Object というクラスのサブクラスになっている.これは, Vectorなどを使う際には有用であるが,普通は特に気にしなくても良い.
自分の書いたクラスを継承する別のクラスを作るということは,初級プログ ラミングにおいては,あまり出てこないだろう.継承して再利用することので きるクラスを適切に設計するというのは,ある程度熟練が必要な作業だからで ある.一方,Java言語の 標準API 中のクラスの多くは,継承して利用するように設計されている.しかし, String クラス のように,
public final class クラス名
のように,finalクラスとして定義されている.このようなクラスからは,継 承してサブクラスを作ることはできない.


練習問題

任意精度の整数を表現できる BigIntegerクラスを使って,ある素数pに対して,2p-1がメルセンヌ素数になっているかどうか判定するプログラムを作成しなさい.

ヒント

  1. メルセンヌ素数については,Mersenne Primes: History, Theorems and Lists などが参考になる(日本語で説明 を読みたかったら,検索エンジンで探すこと).
  2. BigIntegerを使って,階乗を計算するプログラムのサンプルをあげる.
    // java.math.BigInteger を使うため
    import java.math.*;
    
    // factorial(階乗)を計算する
    class Factorial {
      public static void main(String args[]){
        // コマンドラインの引数を整数に変換し変数 n に入れる
        int n=Integer.parseInt(args[0]);
        // BigInteger型変数 x の宣言と, BigInteger定数1の代入
        BigInteger x=BigInteger.ONE;
        // ループ変数 i の宣言と1からnまでの繰り返し
        for(int i=1;i<= n;i++){
          // 整数 i を文字列に直してから,BigIntegerを作成して xと乗算する.
          x=x.multiply(new BigInteger(Integer.toString(i)));
        }
        // 結果の表示xと書くだけでx.toString()が呼ばれる
        System.out.println("factorial "+n+" = "+x);
      }
    }
    
課題ではないので採点はしないが,自分のプログラムを他の人に見てもらい たい人は,ローカルニュースグループ lectures.g01.cp1-ktanaka-W-Tue-5 上 に投稿してみても良いだろう. <
ktanaka@XXX.ecc.u-tokyo.ac.jp

トップページに戻る