page8(update:2017/11/28)


[ prev | next | index ]

8. クラスの継承

目次  

8.1 クラスの継承

 

継承は一般的なクラスを継承して特殊化したクラスを記述出来るようにする仕組みです。似たようなクラスを沢山作るときには、同じ部分を汎化クラスに記述し、これを継承することで追加部分のみの記述で複数の特殊化したクラスを作ります。同じではないがよく似たクラスが沢山必要なプログラムでは継承は便利な仕組みです。

さらに、継承の仕組みは、クラスの分類に使えます。例えば、脊椎動物クラスを継承して哺乳類や鳥類クラスを作り、さらに哺乳類クラスを継承して犬、猫、人クラスを作ることで、人は哺乳類や脊椎動物にも分類できるようになります。

継承では継承元のメンバを継承先も必ず持っているので、人インスタンスを哺乳類と見なしてメンバを参照しても、必ず参照ができます。従って、java言語では人インスタンスの参照は、哺乳類や脊椎動物の参照変数にも代入できます。

上記について以下に説明します。

8.1.1 クラス間の関係

沢山のクラスが関係するシステムでは、クラス間の関係を整理することも重要です。ここでは、代表的な関係、汎化と集約を紹介したいと思います。

汎化

クラスを分類する方法に汎化と呼ばれるものがあります。例えば犬クラスと猫クラスに対して一般化された哺乳類クラスを考え、哺乳類クラスを継承して犬や猫クラスを作れば、犬クラスと猫クラスを共に哺乳類クラスに分類できます。哺乳類を特殊化したものとして犬や猫があることになります。この関係はよく is a の関係といわれます。

汎化: 一般化 <-> 特殊化 A is a B      例:犬 is a 哺乳類(犬は哺乳類の一種)

ここで、犬や猫は哺乳類クラスの全ての要件を引き継いで、その上で何かが付け足されたり、変更されたりして特殊化したクラスと考えます。UMLでは 特殊化側から汎化側に中空の矢を引いてこの関係を示します。

※矢の向き
矢の向きは犬から哺乳類、哺乳類から脊椎動物で、参照ができる向きです。特殊化されたクラスは元になった一般化クラスのメンバを内部に持っているので、特殊化側から一般化側のメンバを参照できます。

 

集約

集約は全体と部分の関係で、汎化がis aの関係であるのに対して、集約はhas aの関係と言われます。

たとえば、自動車はタイヤやエンジンなどの部品を集めて組み立てた物ですが、自動車のインスタンスがタイアやエンジンなど他クラス のインスタンスを 部分として持つ関係になっています。

集約 全体 <-> 部分関係 A has a B    例:自動車 has a タイヤ

UMLでは全体の側に菱形の付いた線で表します。菱形を塗り潰したものはコンポジション(Composition:合成集約)といって通常の集約とは区別します。コンポジションは全体と部分が一体で、それらのオブジェクト寿命が一致するような集約です。上記の図もタイヤやエンジンを交換したり、廃車時に外して再利用するようならコンポジションではなくて通常の集約になります。
 javaプログラムでの集約の記述は、全体側のメンバとして部分側への参照変数をもつ形になります。部分側から全体側を参照できるようにするか否かは実装依存で,参照可能にするには部分側に全体側を参照する変数を持たせる必要があります。

細胞内共生説?

UMLのクラス図でこんなものも書けないかなという、あそびです^^

 

8.1.2 クラスの継承

汎化は生物の分類で使われる系統樹のようにクラスを整理することのできる重要なアイディアです。java言語などオブジェクト指向言語の多くがこれを継承という形で実装しています。
 継承は親クラス(汎化側)の全てを引き継いだ上で、若干の変更を加えた子クラス(特殊化側)を作る仕組みです。

1)子クラスを定義するときに継承する親クラスを宣言し、

2)親クラスからの変更部分のみを子クラスの定義に記述。

継承の宣言

java言語の場合、親クラスを1個のみ継承します。継承を記述しないとjava.lang.Objectクラスを継承します。また、2個以上のクラスを継承することはできません。C++言語など複数の継承親を持つ場合を多重継承、javaの様に1個しか継承親をもてない場合を単一継承といいます。

※単一継承なので、java言語の継承は生物進化の系統樹のように クラスの構造を引き継いで変更を加え、クラスの継承樹を作るのに適しています。

継承を行うこときは、クラス名の後にextendsを付けて継承親のクラス名を書きます。

/*C.java*/
class A //何も継承していないように見えますが実は extends Object の記述が省略されているだけです
{
public A(){System.out.println("Aのコンストラクタを実行");}
}

class B extends A
{
public B(){System.out.println("Bのコンストラクタを実行");} } public class C extends B { static public void main(String args[]){ C obj=new C(); } public C(){System.out.println("Cのコンストラクタを実行");} }

このプログラムをUMLのクラス図にすると

 

繰り返しますが、図の中で継承を示す中空三角形の向きは子(継承先)から親(継承元)です。 UMLでは矢の向きは時間的順番ではありません。誘導可能性を示します。

誘導可能性:情報をたどることのできる向き。子クラスから親クラスのメンバを参照できますが、親クラスからはどんな子クラスが在るか、どんなメンバが在るかを知ることはできません。親クラスから子クラスで追加された新しい名前のメンバを参照することはできません。(中身が変わっていても,同じ名前なら参照は可能)

継承があるときの初期化(後で詳しく説明)

CクラスのmainメソッドでCのインスタンスを作るときCのコンストラクタが呼ばれる前に、Bのコンストラクタ、Bのコンストラクタが呼ばれる前に、Aのコンストラクタ と遡って呼ばれます。実行すると、A,B,Cの順番でコンストラクタは実行されたことが実行結果で確認できます。

>java C
Aのコンストラクタを実行
Bのコンストラクタを実行
Cのコンストラクタを実行

子クラスで可能な記述

 継承は親を継承して子を作ります。このとき、

 この条件に従えば、親で参照できたメンバは子でも必ず参照できます。そこで、java言語では

 親を参照する参照変数に子の参照を代入することが許されています。

※ 親クラスのインスタンスだと見なして参照を行っても不都合が発生しないように継承での追記には条件が付けられているわけです。

※言い換えると、同じインスタンスが自分のクラスに分類できるだけでなく、継承元のクラスにも分類できることになります。

次の例は、哺乳類を継承して猫クラスを作ってみたものです。

例題8a

/*Cat.java*/
class Mammal
{//哺乳類
	private java.util.Date birthday;
	public Mammal(){birthday=new java.util.Date();}
	public String getBirthday(){return birthday.toString();}
	public void print()
	{
		System.out.println("哺乳類のインスタンス:誕生日は"+getBirthday());
	}
}
public class Cat extends Mammal
{//猫

	//追加メンバ
	private String name="名なし";
	public Cat(String name){this.name=name;}
	
	//親メンバの上書き。 アノテーション@Overrideでコンパイラに上書きを知らせている。これは省略可です
	@Override
	public void print()
	{
		System.out.println(
			"猫のインスタンス:誕生日は"+getBirthday()+" 名前は"+name
		);
	}
	
	//----------------------------------
	//動作テスト用main関数
	static public void main(String[] args)
	{
		Mammal m1=new Mammal();
		Mammal m2=new Cat("チャチャ");
		m1.print();
		m2.print();
	}
}

アノテーション  javaにはコンパイラなどへの付加情報を記述するアノテーションと呼ばれる機能があります。上記のプログラムではアノテーションの一つ@Overrideを使っています。

@Overrideはコンパイラに次のメソッドは継承親のメソッドを上書きすることを教えます。もし、メソッド名を間違えて上書きになっていない場合、コンパイラが注意してくれるので安心できます。このアノテーションは省略可能なので書かないことも多いが、その場合はメソッド名を間違えていないか注意が必要です。

哺乳類を参照する変数m2に猫インスタンスの参照を代入しています。このように書いてもコンパイルできますし、実行すると上書きした方のprint関数を実行してくれます。

実行結果
>java Cat
哺乳類のインスタンス:誕生日はWed Nov 19 16:34:24 JST 2008
猫のインスタンス:誕生日はWed Nov 19 16:34:24 JST 2008 名前はチャチャ

UMLのクラス図にすると

(まとめ)クラス継承の仕組みはプログラミングの上でも非常に役立ちます。子クラスの記述量が減って楽になるということ以上に、子のインスタンスを親のインスタンスの代わりに使えることが重要です。

GUIのような多数のインスタンスから作られた既存システムが有るとき、変更したいインスタンスのクラスを継承して作ったクラスのインスタンスで元のインスタンスを置き換えることができます。この手法で、プログラムのちょっとした変更なら容易に出来てしまいます。(次回の演習)

 目次

8.2 継承がある時のインスタンスの生成

継承は親クラスの内容に後から追加する形で行われています。new演算子でインスタンスを生成する場合も、同様に親クラスの内容に後から追加する形で行われています。

このためにインスタンスは継承元のインスタンスメンバをそのまま含んでいます。上書きされて隠れることも有りますが存在はしています。

8.2.1 初期化の順番

初期化は親クラスで定義されたメンバの部分が先に行われます。

まず継承親クラスのフィールドが初期設定され、継承親のコンストラクタが呼ばれ初期化を行います。

次に継承で追加や上書きされたフィールドが初期設定され、継承子のコンストラクタを呼んで初期化を完了します。

(注)
クラスのコンストラクタに、継承元で使うコンストラクタをブロックの先頭でsuper(引数)の形で指定できます。指定が無い場合は継承元の引数無コンストラクタが選択されます。(この場合,引数無コンストラクタが親にないとコンパイルエラー)

確認のための例題プログラム

試しに次のようなプログラムを動かしてみました。Testクラスのコンストラクタの初めに継承親の初期化で使うコンストラクタを指定しています。

実行結果をみるとTestのコンストラクタからTest0のコンストラクタを呼び出しますが、ここでTest0のフィールドが先に初期化され、その後でTest0のコンストラクタが呼ばれています。

Test0の初期化が完了して、Test部分の初期化が行われますが、この時もTestのフィールドの初期化がされてからTestのコンストラクタのsuper呼び出しの次から実行が再開されました。

class Test0
{
	public Test0()
	{
		System.out.println("Test0のコンストラクタ1を実行");
	}
	public Test0(Object i)
	{
		System.out.println("Test0のコンストラクタ2を実行");//3番目
	}
	Object o=System.out.printf("Test0のフィールド初期設定\r\n");//2番目
}

public class Test extends Test0
{

	static public void main(String args[])
	{
		Test obj=new Test();
	}
	public Test()
	{
		super(System.out.printf("Testのsuper()呼び出し\r\n"));//1番目親クラスコンストラクタの呼び出し
		System.out.println("Testのコンストラクタ実行");//5番目
	}
	Object o2=(o=System.out.printf("Testのフィールド初期設定\r\n"));//4番目
}
/* 実行結果

Testのsuper()呼び出し
Test0のフィールド初期設定
Test0のコンストラクタ2を実行
Testのフィールド初期設定
Testのコンストラクタ実行

*/

8.2.2 Objectクラスの継承

javaではプログラマが継承の記述を省くとクラスはjava.lang.Objectを継承する決まりになっています。 従って、全てのクラスが継承の親を遡ると最後はObjectクラスにたどり着きます。

 初期化は継承親の要素から先に行われるので、コンストラクタは最初に親のコンストラクタを 呼び出す。ここで、親の引数無しのコンストラクタを呼び出す場合は何も記述する必要はない。しかし、引数付きのコンストラクタを呼ぶ場合はsuperを用いて明示的に呼び出す必要がある。

 
誕生日を設定できるように書き換えた例を次に示す。例では哺乳類クラスに誕生日を設定できるコンストラクタを追加し、継承の子、犬クラスでこれを利用するコンストラクタを用意した。

/*Dog.java*/
import java.util.Date;
import java.util.Calendar;
class Mammal
{//哺乳類
	private	Date birthday;
	public Mammal()
        {
                birthday=new java.util.Date();
        }
	public Mammal(Date date)//引き数付きコンストラクタ
        {
                birthday=date;
        }
	public String getBirthday(){return birthday.toString();}
	public void print()
	{
		System.out.println("哺乳類のインスタンス:誕生は"+getBirthday());
	}
}
public class Dog extends Mammal
{//犬

	//追加メンバ
	private String name=null;
	public Dog(String name){this.name=name;}//親コンストラクタの呼び出し記述を省略
	public Dog(String name,Date date)
	{
		super(date);//親の引き数付きコンストラクタ呼び出し。最初に書くこと。
		this.name=name;//
	}
	//親メンバの上書き
	@Override 
	public void print()
	{
		System.out.println(
			"犬のインスタンス:誕生は"+getBirthday()+" 名前は「"+name+"」"
		);
	}

	//----------------------------------
	//動作テスト用main関数
	static public void main(String[] args)
	{
		Calendar calendar=Calendar.getInstance();//実行環境でのカレンダーを用意
		calendar.set(2000,1,1,8,30,10);//カレンダーの日付を2000年1月1日8時30分10秒に設定
		Date date=calendar.getTime();//Dateインスタンスに値を変換
		
		Mammal m1=new Dog("タロー");
		Mammal m2=new Dog("ぽち",date);
		m1.print();
		m2.print();
	}
} 

実行結果
>java Dog
犬のインスタンス:誕生はWed Nov 19 17:54:25 JST 2008 名前は「タロー」
犬のインスタンス:誕生はTue Feb 01 08:30:10 JST 2000 名前は「ぽち」

 目次

8.3 継承がある時のインスタンス・メンバの参照

インスタンス・メンバの参照は参照変数名.メンバ名と記述します。自分のメンバを参照 する場合はメンバ名だけでいいが、名前が仮引数と重複するような場合はthisを使ってthis.メンバ名と 記述し区別することが可能です。継承があるときも名前の重複が無ければメンバ名だけで参照できます。

しかし、継承では親のメンバを上書きする場合がある 。

このときは親のメンバをsuperを使ってsuper.メンバ名で参照する。さらに、何段もの継承をさかのぼる場合は

((要素を参照したい継承親クラス名)this).メンバ名

this継承親の参照型にキャストすることで参照できる。

class A1
{
    String name="a1";
}

class A2 extends A1
{
    String name="a2";//フィールドの上書き
}

class A3 extends A2
{
    String name="a3";//フィールドの上書き
}

public class A4 extends A3
{
    String name="a4";//フィールドの上書き
	
    public void printName()
    {
        System.out.println(name+"-"+super.name+"-"+((A2)this).name+"-"+((A1)this).name);
    }
    public static void main(String args[])
    {
        A4 a=new A4();
        a.printName();
    }
}

 目次

課題7(初期ファイルP7.java) 作図ツールへ向けて

前回はSVGテキストを読み込んでブラウザーに表示してみました。今回はSVGのテキストとオブジェクトとを対応させる試みです。

※SVG(Scalable Vector Graphics):XMLを利用して2次元の図をテキスト形式で表現する手法の一つ。

※XMLやSVGについてはいろいろ探して情報を集め、自分で学んでください。

ここでは継承を利用してSVGのタグをオブジェクトの木構造と対応させています。

まず,XMLのタグの汎化クラスをXML_Tagとします。これを継承して具体的なコメントのタグCommentTagや四角形のタグSVG_Rectなどのクラスを作っています。

XML_Tagを中身として持つタグもXML_Tagを継承したContainerTagクラスで作り,これをさらにContainerTagを継承してSVGの親のタグSVG_Rootを作りました。

課題で要求する内容

問題は線分に対応するSVG_Lineクラスとテキストに対応するSVG_Textクラスを作ることです。
継承を上手く使うと合わせても30数行にしかなりません。やり方はXML_Tag.javaを解析して考えてください。

P7.java XML_Tag.java Text.java の3つのファイルを同じフォルダーに置いて初期ファイルP7.javaに2つのクラスを記述してください。実行すると以下のようなテキストを標準出力に書き出すようにしてください。

<?xml version="1.0" encoding="Shift-JIS" standalone="no" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg height="200" widthl="640" viewBox="0 0 640 200" xmlns="http://www.w3.org/2000/svg" >
    <!--コメント-->
    <rect x="50" height="50" width="50" y="50" />
    <rect x="100" height="50" width="50" fill="red" y="100" />
    <rect x="75" height="50" width="50" stroke-width="4" fill="none" stroke="crimson" y="75" />
    <text x="100" stroke-width="2" font-size="80" stroke="lightgreen" y="80" >
        aあ亜
    </text>
    <line x2="600" x1="100" y2="200" y1="100" stroke-width="4" stroke="blue" />
</svg>

これをP7.svgのようなテキストファイルにしてブラウザーに読み込むと図形として表示されます。

 目次

メモ: クラスの書きかた

 ここでクラスの書式をまとめておきます。継承に関係する部分も多いので注意してみてください

書式

[クラス修飾子] class クラス名 [extends 継承元クラス名] [implements インターフェース名,,]
{ クラスの本体 }

1 クラス修飾子

空白で区切り組み合わせて記述可能

2 継承の宣言

3 インターフェイス実装の宣言

java言語にはインターフェイスと呼ばれるものがあり、複数のインタフェースを実装できる。(9章で説明)

4 クラスの中に記述できるもの

フィールドの宣言

[フィールドの修飾子] 変数の型名 変数名 [初期化子];
フィールドの修飾子

可視性(public,private,protected)と合わせて空白で区切り組み合わせて記述

初期化子

クラスメンバの場合はクラスの初期化時に初期化される。(書いた順に上から初期化)
インスタンスメンバの場合はインスタンス生成時に初期化される。(コンストラクタよりも先に初期化)

メソッドの宣言


[メソッドの修飾子] 戻り値の型名 メソッド名(引数並び)[throws 例外]
{ 関数の本体 }
メソッドの修飾子

可視性(public,private,protected)と合わせて空白で区切り組み合わせて記述

同じ名前のメソッドについて

オーバロード(overloaded)  
名前が同じで引数が異なるメソッドを定義すること。識別子に引数並びが含まれるためメソッドは区別できる。

上書き(override)
継承元と同じ名前と引数並びのメソッドを継承先で再定義すること。 上書きされたメソッドが優先する。下に書かれたメソッドを呼ぶときは、継承が在るときのメンバ参照を利用する。

※可視性、戻り値、例外などで継承元の制限を壊すような上書きはコンパイルエラーとなる。

★クラスの中にクラスを記述できる。

入れ子クラス static付で親クラスの内側に定義されるクラス、
親の非公開メンバを参照可能。クラ ス名が親クラス.自クラスと入れ子の名前になる。

内部クラス static無しで親クラスの内側に定義されるクラス
親インスタンスの内部に作られるインスタンスのクラスで、親インスタンスを指定してインスタンスを生成する必要がある。
(次週に例を示します)

無名クラス:インスタンスの生成と一体になった内部クラスの記述方法。クラス名を記述しない。

目次


[ prev | next | index ]