目次へ戻る 下へ↓
7、超初歩のオブジェクト指向とJava
作成者:Fumita Makino 更新日:2003-04-14 16:56

・継承(inheritance)という概念

世の中のオブジェクト指向を標榜する言語は普通は継承という概念をもっています。そしてJava言語はまずオブジェクト指向ありきで作られたプログラミング言語であり、継承の概念はJavaの根幹をなしています。それでは、この継承とはどのような概念でしょうか?

まず英語の継承「inheritance:インヘリタンス」の意味を調べてみましょう

inheritance 遺伝的性質[体質];遺伝;相続すること

とあります。つまり遺伝したり、何らかの資産を相続するという意味のようですね。プログラムにおける継承も全く同じ意味です。親から子へ、子から孫へと相続、遺伝が行われます。これだけではイメージが湧かないかもしれませんね。

ではここで一気に具体的な話をします。Javaにおいて継承の単位(親とか子に相当)はクラスとなり、自分の親(正確には先祖)をスーパークラス(SuperClass)、自分の子供(正確には子孫)をサブクラス(SubClass)と呼びます。またJavaにおいて継承は、必ず単一継承と決められており常に直接の親クラスは1つ(子クラスは複数可)です。つまり親をさかのぼっていけば必ず最後はひとつのクラスに行き着くということです。

継承の概念は「資産の相続」という側面と「型の遺伝」という二つの側面から見ていくと非常にわかりやすいです。

「資産の相続」という側面から見ると、「自分」は「親」の資産(変数、メソッド)を相続しています。また「子供」は「自分」の資産を相続しています。ここでよーく考えてみてください。「親」がもっている資産は「自分」も持っています。そして「自分」が持っている資産は「子」も持っています。つまり「子」は「親」と「自分」の資産を持っていることになります。こんどは、逆に見ていきましょう「子」が新たに獲得した資産は当然ですが「自分」は所持していません。また「自分」の世代で獲得した資産は「親」は所持していません。このように必ず資産の相続(継承)の流れは親から子供への一方通行です。

表:資産の相続
続柄 資産(赤字は新たに獲得した資産)
土地
自分 土地、
土地、家、

「型の遺伝」という側面から見ると、「自分」は「親」の性質(型)を全て受け継いでいるので「親」と同様に扱うことができます。また「子供」は「自分」の性質(型)を全て受け継いでいるので「自分」と同様に扱うことができ、さらに「自分」が受け継いだ「親」の性質(型)も受け継いでいるので「親」と同様に扱うこともできます。しかしここでも型の遺伝(継承)の流れは親から子供への一方通行でありその逆はありません。

図:型の遺伝
「親」の型として扱えるもの →「親」「自分」「子」(顔が丸いから)
「自分」 の型として扱えるもの →「自分」「子」(顔と目が丸いから)
「子」 の型として扱えるもの →「子」(顔と目が丸く、口が三角だから)

ここまでをいったんまとめてみましょう。

継承とは親から子供へ資産を相続、または型を遺伝することであり、その継承の方向は親から子供への一方向である。

これがオブジェクト指向における継承の基本概念です。なぜこのような一見して面倒くさい継承の概念を用いるかというと、それはひとえに生産効率を上げるためです。つまり継承を利用すれば一度作ったプログラムは実装の度に作る必要はありません。一度作ったクラスを継承し、追加したい機能や値を書き足してやるだけでよいのです。また型の遺伝を利用すれば、古いクラスを継承して新しい機能を実装したとき古いクラス(親)と新しいクラス(子)を入れ替えても結果はどうあれ正常に動作します。

 

・java.lang.Objectクラス

実はJavaにおいて、全てのクラス(Stringでも、Fileでも・・)の一番上にあるスーパークラスはObjectクラスです。つまりJavaで利用している全てのクラスはObjectクラスから派生したクラスであると言うことです。

これを最初に説明した「資産の相続」という側面から見ると、全てのクラスはObjectクラスの資産(機能)を持っているということです。また「型の遺伝」という側面から見ると、全てのクラスは実体(インスタンス)がどうあれ、Object型として扱うことが可能であるということを意味しています。

 

・継承を踏まえたJavaDocの利用

継承の概念を学習したところでもう一度JavaDocを見てみましょう。java.lang.StringのJavadocを開いてください。

クラスの説明の一番上には、このように表示されていますね? StringクラスはObjectクラスを継承して作られているということがわかります。さらにフィールドやメソッドの一覧の下あたりに

のような表があると思います。これはStringクラスがObjectクラスから継承したメソッド一覧です。ここをクリックするとObjectクラスのJavaDocへと画面が切り替わり継承したメソッドの機能を調べることが可能です。

 

・継承を利用したJavaプログラム

では実際に継承のプログラミングを練習してみましょう。サンプルとして利用するコードはObjectを除く以下の6つです。クラス名をクリックするとソースコードをダウンロードできます。

図:サンプルの系統図
  • class java.lang.Object
    • class ExtTestMain1 「資産の相続」を実証するMainクラス
    • class ExtTestMain2 「型の遺伝」を実証するMainクラス
    • class ExtTestSuper 継承用の親クラス(名前と電話番号を保持するクラス)

JavaDocはこちら

ExtTestMain系のクラスに関してはコメントを読めばわかるので説明は省略します。実際の継承用のクラスの関係としては下図のように、ExtTestSuperを親クラスとして、その下にExtTestSub1、ExtTestSub2が子クラスとして存在ます。さらにExtTestSub1を継承して ExtTestSub1_subが孫クラスとして存在しています。

図:サンプルのクラス図

まずExtTestSuperクラスのソースを見てみましょう。もちろんJavaDocもあわせて見て下さい。このクラスは名前と電話番号を表すString型の変数を内部に保持するクラスです。両変数ともにコンストラクタによって初期値を設定することができます。nameはpublicなアクセスが可能なので直に書き換えることができます。またtelはprivateアクセスなのでsetTelやgetTelメソッドによって書き換えや取得を行います。

例:ExtTestSuper.java

     
 


/**
  継承の実習サンプルプログラム:親クラス
  @author Fumitaka Makino
*/

public class ExtTestSuper extends java.lang.Object{
  
  /** 名前 */
  public String name = null;
  
  /** 電話 */
  private String tel = null;
  
  /**
    コンストラクタ、名前と電話番号を引数に持つ
    @param String sname 名前
    @param String stel 電話番号
  */

  public ExtTestSuper( String sname , String stel ){
    this.name = sname;
    this.tel = stel;
  }
  
  /**
    電話番号を取得する
    @return String 電話番号
  */

  public String getTel(){
    return this.tel;
  }
  
  /**
    電話番号を設定する
    @param String stel 電話番号
  */

  public void setTel( String stel ){
    this.tel = stel;
  }
  
  
}

 
     

注目してほしいのがpublic class ExtTestSuper extends java.lang.Object{の部分です。extendsとは自分が何のクラスを継承しているかを表す予約語です。つまりこのクラスはObjectクラスを継承していると言うことになります。通常のクラス宣言ではpublic class ExtTestSuper{と記述すれば自動的にObjectクラスを継承したことになります。もちろん、java.lang.Object以外のクラスを継承するときには、下記のようにクラス名の後ろに明示的に継承元のクラス名を記述してやる必要があります。

public class クラス名 extends 継承元のクラス名 {

ではここで単純にExtTestSuperクラスを継承したExtTestSub1クラスを見てみましょう。このクラスはコンストラクタの再宣言以外は何もやっておらず、そのまま親クラスの「資産を相続」しています。

例:ExtTestSub1.java

     
 


/**
  継承の実習サンプルプログラム:子クラス1
  @author Fumitaka Makino
*/

public class ExtTestSub1 extends ExtTestSuper{
  
  /**
    コンストラクタ、名前と電話番号を引数に持つ。内部ではスーパークラスのコンストラクタを実行している。
    @param String sname 名前
    @param String stel 電話番号
  */

  public ExtTestSub1( String sname , String stel ){
    //スーパークラスであるExtTestSuperのコンストラクタを実行
    super( sname , stel );
  }
  
}

 
     

ここで見てほしいのがコンストラクタの部分です。「super(sname,stel)」と記述してありますね。コンストラクタはクラス名で宣言されているため単純に継承できません。そのため親クラスのコンストラクタをこのように呼び出して初期化しているのです。

次に単純に継承するのではなく、相続した資産の一部を変更してしまう(オーバーライド:上書き)というコードを見てみましょう。

例:ExtTestSub2.java

     
  /**
  継承の実習サンプルプログラム:子クラス2
  @author Fumitaka Makino
*/

public class ExtTestSub2 extends ExtTestSuper{
  
  /**
    コンストラクタ、名前と電話番号を引数に持つ。内部ではスーパークラスのコンストラクタを実行している。
    @param String sname 名前
    @param String stel 電話番号
  */

  public ExtTestSub2( String sname , String stel ){
    //スーパークラスであるExtTestSuperのコンストラクタを実行
    super( sname , stel );
  }
  
  
  /**
    電話番号を取得する。オリジナルのgetTelメソッドをオーバーライド(上書き)しています。
    オリジナルではセットした電話番号をリターンしていますが、このメソッドでは冒頭に「TEL:」
    を付加するように変更してあります。
    @return String "TEL:" + [電話番号]
  */

  public String getTel(){
    return "TEL:" + super.getTel();
  }
  
}
 
     

コンストラクタに関してはExtTestSub1と同一でsuperを呼び出しているだけですね。注意してほしいのがgetTelメソッドです。このメソッドはオーバーライドされておりExtTestSub2のインスタンスのgetTelを実行すると、このメソッドが呼ばれます。ExtTestSub2のgetTelは親クラスのgetTelの戻り値に「TEL:」という文字列を付加してリターンしています。

オーバーライドの条件は、メソッド名、引数ともに親クラスのメソッドと同一のメソッドを実装していることです。JavaのVMは、実行時にメソッドを呼び出すときにクラスを継承の順を遡って(サブクラスからスーパークラスへと)探していきます。またメソッド名のみ同じメソッド(*)を実装することをオーバーロードと言います。

*あたりまえですが戻り値のみ違うメソッドは実装することができません。なぜなら呼び出す側がメソッドを区別できないからです。

最後にExtTestSub1を継承したExtTestSub1_subを見てみましょう。

例:ExtTestSub1_sub.java

     
  /**
  継承の実習サンプルプログラム:子クラス1のサブクラス
  @author Fumitaka Makino
*/

public class ExtTestSub1_sub extends ExtTestSub1{
  
  /**
    コンストラクタ、名前と電話番号を引数に持つ。内部ではスーパークラスのコンストラクタを実行している。
    @param String sname 名前
    @param String stel 電話番号
  */

  public ExtTestSub1_sub( String sname , String stel ){
    //スーパークラスであるExtTestSuperのコンストラクタを実行、名前の引数に変更を加える
    super( "[ "+sname+" ]" , stel );
  }
  
}

 
     

このクラスはExtTestSub1と同様にメソッドやフィールドなどの変更はしていません。しかしコンストラクタの部分を見てください。親クラスのコンストラクタを呼び出すときに引数に手を加えています。それ以外の振る舞いはExtTestSub1と全く同一のクラスです。

ではこの継承関係を「型の遺伝」を通して考えてみましょう。下表を見て下さい。

表:型のくくり

この型に属するクラス名
ExtTestSuper ExtTestSuper、ExtTestSub1、ExtTestSub2、ExtTestSub1_sub
ExtTestSub1 ExtTestSub1、ExtTestSub1_sub
ExtTestSub2 ExtTestSub2
ExtTestSub1_sub ExtTestSub1_sub

今回作成した全てのクラスはExtTestSuperを継承、もしくは間接的に継承しています。そのため全てがExtTestSuper型として扱うことが可能です。またExtTestSub1型にはExtTestSub1クラスとそれを継承したExtTestSub1_subクラスが含まれます。あとの二つはサブクラスを持たないので、それぞれ所属しているクラスは一つとなります。

以上、これらのクラスを実際に利用しているクラスがExtTestMain1とExtTestMain2となりますのでソースコードをよく参照してください。

 

・コラム:多重継承

c++などのオブジェクト指向言語にある多重継承の概念はJavaには存在しないとかするとかという話が良く出てきます。これに対して私なりの説明をしたいと思います。

多重継承と言うのは基本的に複数の親を持ち、その親の型や資産を継承します。これは我々プログラマからすると非常に望ましい機能と言えます。ところが同一のメソッド名が存在したときや同一のフィールドが存在したときなど、どちらの親から継承するのかなど少し考えただけで複雑であり、またソースコードから継承元(スーパークラス)を遡っていったときにも煩雑な経路をたどることになります。

図:多重継承
どちらから受け継いだか複雑になる

そのためJavaではクラス構成が汚くなり、シンプルでなくなる危険を排除するために「ある側面での多重継承」を完全に禁止し単一継承のみとしました。これによってあるクラスの親は常に1つとなりクラス構成の見通しが非常に良くなりました。では本当に多重継承はできないのでしょうか?よく市販の本では「インターフェースを用いることにより実現が可能です。」と軽く書いているのを見かけます。しかしこれは必ずしも完全な表現ではないと私は考えています。先ほど記述したようにJavaでは「ある側面での多重継承」を完全に禁止したと書きましたね?つまりインターフェースによって実現可能な多重継承というのは、「もう一つの側面での多重継承」なのです。

勘の良い人はもう気づいていただけたでしょうか?そうなんです。Javaにおいて禁止された多重継承は「資産の相続」であり(*)、「型の遺伝」に関しては禁止されていないのです。そしてインターフェースによって実現することができる多重継承とは「型の遺伝」なのです。以下に簡単なクラス図を示します。

図:「型の遺伝」による多重継承のクラス図

「カローラワゴン」クラスは「カローラ」クラスを継承して作られています。このとき通常の継承なので「資産の相続」「型の遺伝」の両方が行われています。つまり「カローラワゴン」クラスは「カローラ」の資産(エンジンなど)を受け継ぎつつ、さらに「カローラ」型として扱うことが可能であるということです。一方車の区分である「ワゴン車」インターフェースと言うものが存在します。「カローラワゴン」クラスはクラスを宣言するときに、以下のように「ワゴン車」インターフェースをインプリメントすることを宣言したのです。

public class カローラワゴン extends カローラ implements ワゴン車

このことにより、「カローラワゴン」クラスは「ワゴン車」型として扱うことも可能となるわけです。しかし「ワゴン車」は飽くまでインターフェースなので機能を持つことはありません。つまりインターフェースをインプリメントすることにより「型の遺伝」の側面においてのみ継承が実現されているのです。ここまでをまとめてみましょう。

カローラワゴンはカローラの資産を相続し、カローラまたはワゴン車としても扱うことができる。

ということになります。いかがでしょうJavaにおける多重継承を理解していただけたでしょうか?

 

↑上へ 目次へ戻る