ポリモーフィズム

ポリモーフィズム(polymorphism)は、オブジェクト指向プログラにおける概念のひとつです。日本語では「多態性」「多様性」など呼ばれていますが、このままではイメージが掴めません
英文の意味に立ち戻ると「poly」には「複数の」であり、「morph」とは「姿を変える」という意味ですから、ポリモーフィズムは一つの事を複数の違った形で扱えたり、類似したいろいろな物を同じものとして扱える概念です。
ポリモーフィズムによって、プログラムをブラックボックのまま、より柔軟に扱うことが出来ます。

ポリモーフィズムは概念ですから、具体的な機能はいくつか考えられます。
例えば、継承とオーバーライドによって同じメソッド呼び出しであっても、常に同じふるまいではなく、そのときの目的に適したふるまいを自動的に切り替えることが出来るのもポリモーフィズムです。

また、継承関係にある参照変数間の代入や、インスタンスのキャストもポリモーフィズムです。

継承関係にある参照変数間の代入
これはインスタンスを作成する時に、参照変数の型をインスタンスの型ではなく継承関係にあるスーパークラスの型にすれば、そのインスタンスはあたかもスーパークラスの型として扱えるという物です。

継承で使ったNormalTVとSmartTVに在庫情報を追加して、異なる製品を同じ配列で管理する場合を考えてみましょう。

スーパークラス

public class NormalTV2 {
	String serialNo;
	String tvType;
	
	NormalTV2(String tvType, String serialNo) {
		this.tvType = tvType;
		this.serialNo = serialNo;
	}
	
	NormalTV2(String serialNo) {
		this.tvType = "NormalTV";
		this.serialNo = serialNo;
	}
	
	void on() {
		System.out.println("\t電源を入れます。");
	}
	void off() {
		System.out.println("\t電源を切ります。");
	}
	void display() {
		System.out.println("\t番組を視聴します。");
	}
	void channelChange() {
		System.out.println("\tチャネルを切り替えます。");
	}
	void functions() {
		on();
		off();
		display();
		channelChange();
	}
	void printProductInfo() {
		System.out.println(tvType + " : serial = " + serialNo);
	}
}

サブクラス

public class SmartTV2 extends NormalTV2 {
	
	SmartTV2(String serialNo) {
		super("SmartTV ", serialNo);
	}
	
	void internet() {
		System.out.println("\tインターネットにアクセスします。");
	}
	void recode() {
		System.out.println("\t番組を録画します。");
	}
	void dvd() {
		System.out.println("\tDVDを再生します。");
	}
	void functions() {
		super.functions();
		internet();
		recode();
		dvd();
	}
}

スーパークラス、サブクラスを一緒に管理する処理

public class PolymorphismSample {
	public static void main(String[] args) {

		NormalTV2[] tvs = { new NormalTV2("0001"), new SmartTV2("0101"), new NormalTV2("0002"), 
			new NormalTV2("0003"), new SmartTV2("0101") };
		
		for(NormalTV2 tv : tvs) {
			tv.printProductInfo();
		}
	}
}

実行結果

>java PolymorphismSample
NormalTV : serial = 0001
SmartTV  : serial = 0101
NormalTV : serial = 0002
NormalTV : serial = 0003
SmartTV  : serial = 0101

>

これにより、継承関係にあるいろいろな実装を同じ型として扱えますが、インスタンスがどのようなメソッドやフィールドを持っていたとしても、扱っている型で定義されているもの以外は使えません。使おうとするとコンパイルエラーになります。
このポリモーフィズムが成り立つのは、継承関係だけでなく実現関係でも構いません。継承関係も実現関係もない場合にはポリモーフィズムは成立せずコンパイルエラーになります。
※インターフェースはインスタンス化が行えないので、インターフェース自体をポリモーフィズムで扱う事はできません。

インスタンスのキャスト
サブクラスをスーパークラス型に変換するのをアップキャストと呼びます。アップキャストは、型の互換性チェックが簡単に行えるので自動的に行われます。
一方、スーパークラス型で扱っていたインスタンスを、元の型に戻すのはダウンキャストと呼びます。ダウンキャストは型の互換情報が無いので、明示的にキャスト指定する必要があります。
インスタンスが扱えるのは参照変数の型に定義されているものだけです。そこで、アップキャストして作ったサブクラスのインスタンスをダウンキャストすると、サブクラスの差分として定義したメソッドやサブクラスで定義したフィールド(スーパークラスのフィールドと同名の変数がサブクラスにある場合には変数の実体が切り替わる)が使えるようになります。しかし、スーパークラスをダウンキャストしてサブクラスの差分のフィールドやメソッドにアクセスすると実行時に例外が発生します。

NormalTVとSmartTVを使ったアップキャスト、ダウンキャストの例
アップキャスト

public class UpCastSample {
	public static void main(String[] args) {

		NormalTV2 tv = new SmartTV2("0101");
		
		tv.printProductInfo();
		tv.functions();
//		tv.internet();
//		tv.recode();
//		tv.dvd();
	}
}

実行結果

>java UpCastSample
SmartTV  : serial = 0101
        電源を入れます。
        電源を切ります。
        番組を視聴します。
        チャネルを切り替えます。
        インターネットにアクセスします。
        番組を録画します。
        DVDを再生します。

>

アップキャストした参照変数型に含まれるメソッドにアクセスする限りは実行できます。
(オーバーライドしたメソッドはオーバーライドした側のメソッドが使われます。)
しかし、ソースのコメントアウトを外して参照型変数型に含まれないメソッドにアクセスするとコンパイルエラーが出ます。

>javac UpCastSample.java
UpCastSample.java:8: エラー: シンボルを見つけられません
                tv.internet();
                  ^
  シンボル:   メソッド internet()
  場所: タイプNormalTV2の変数 tv
UpCastSample.java:9: エラー: シンボルを見つけられません
                tv.recode();
                  ^
  シンボル:   メソッド recode()
  場所: タイプNormalTV2の変数 tv
UpCastSample.java:10: エラー: シンボルを見つけられません
                tv.dvd();
                  ^
  シンボル:   メソッド dvd()
  場所: タイプNormalTV2の変数 tv
エラー3個

>

ダウンキャスト

public class DownCastSample {
	public static void main(String[] args) {

		SmartTV2 tv = (SmartTV2) new NormalTV2("0001");
		
		tv.printProductInfo();
		tv.functions();
	}
}

実行結果
ダウンキャストを明示しない>場合は以下のコンパイルエラーになります。

>javac DownCastSample.java
DownCastSample.java:4: エラー: 不適合な型: NormalTV2をSmartTV2に変換できません:
                SmartTV2 tv = new NormalTV2("0001");
                              ^
エラー1個

>

ダウンキャストを明示すると、コンパイルは通りますが実行時に例外が発生します。

>java DownCastSample
Exception in thread "main" java.lang.ClassCastException: NormalTV2 cannot be cast to SmartTV2
        at DownCastSample.main(DownCastSample.java:4)

>

アップキャストしたものをダウンキャストする

public class UpDownCastSample {
	public static void main(String[] args) {

		NormalTV2 ntv =  new SmartTV2("0101");
		SmartTV2 stv = (SmartTV2) ntv;
		
		stv.printProductInfo();
		stv.internet();
		stv.recode();
		stv.dvd();
	}
}

実行結果
アップキャスト時にはアクセスできなかったメソッドがアクセスできるようになります

>java UpDownCastSample
SmartTV  : serial = 0101
        インターネットにアクセスします。
        番組を録画します。
        DVDを再生します。

>

つまり、インスタンスのキャストはインスタンスの扱い方が変わるので、参照先のインスタンスが変わるわけではありません。

プログラムの部品化に戻る

抽象クラスとインターフェース

抽象クラスもインターフェースも、書式を理解しても、実際のプログラミングではどのように使い分けたらよいのかはっきりしません。
どのような場合に抽象クラス、そしてどういう時にはインターフェースを使ってプログラミングすれば良いのか、使い分けを考えてみましょう。

抽象クラス

抽象クラスの書式

修飾子 abstrac class クラス 名 {
  フィールド
  コンストラクタ
  メソッド
  抽象メソッド
}
※抽象クラスを継承した抽象クラスは、元の抽象クラスを拡張し、新しい抽象メソッドを追加したり、既存の抽象メソッドをオーバーライドして実装することができます。
※abstractは継承を前提としているため、final修飾子を同時に指定すると、コンパイル・エラーなります。

抽象メソッドの書式

修飾子 abstract 戻り値の型 メソッド名(引数リスト);

抽象クラスを継承するクラス定義の書式

修飾子 class クラス名 extends 抽象 クラス 名 {
  フィールド
  コンストラクタ
  メソッド
  抽象メソッド のオーバライ
}

クラスと抽象クラスの使い分け
クラスはもともと(狭義の)オブジェクトを抽象化して作ります。では抽象クラスは一般的なクラスと何が違うのでしょう。
本題に入る前に「抽象」という言葉を正しく理解しましょう。
「抽象」というと抽象画の印象や、「具象」の反対語だから「曖昧な」といった意味だろうと思っている人もいます。ですが英語のabstractには「曖昧な」という意味はありません。英語の辞書を見ると、動詞として「~を取り除く」「~を概念化する」「~要約する」とあります。

概念化とは「いくつかの事物を一般化してとらえる考え方」ですから、抽象化とは「個別的な物を取り除いて、事物に共通なものを抜き出し、要約する」ことです。
クラスは個々のオブジェクトから個別的な物を取り除き、共通する点を抜き出して、具体的なフィールドとメソッドに要約してスーパークラスとし、各オブジェクト固有の事物をサブクラスで定義します。
一方、抽象クラスはこの概念化(抽象化)を一歩進めて、個別的な物が持つ振舞いの詳細は違っていても共通して持っている振舞いを、詳細を決めずに概念として定義する抽象メソッドを使って定義します。そして、振舞いの詳細はサブクラスに定義させます。

言い換えると、スーパークラスはサブクラスに対し振舞いの定義を強制しません。スーパークラスの振舞いをオーバーライドするかしないかはサブクラスの自由意志です。一方抽象クラスは抽象クラスを実装するサブクラスに対し、詳細は決めずに(詳細を自分では決められない場合を含め)決めなければいけない振舞いを規定して、その詳細を定義することを強制します。つまり、サブクラスが抽象メソッドをオーバーライドしないとコンパイルエラーになります。
また、Javaの場合クラスは一つのスーパークラスしか持てない(単一継承)ので、抽象クラスも単一継承です。

インターフェース
インターフェースはクラスとは別物の、継承構造を持ったクラス群に設定される、備えるべき性質、特性です。
インターフェース自体はインターフェースを継承(多重継承が可能)出来ますが、クラスはインターフェースを継承出来ず、実装(implements)して実現します。
実装は継承と異なり、クラスは複数のインターフェースを実装(多重実装)する事が出来ます。
実体(具体的な値)のないインターフェースは抽象クラスと同じようにインスタンス化することは出来ません

なお、実装によって設定されるインターフェースですが、インタフェース内の抽象メソッドの実現は、実装したクラスが行わなければいけないわけではありません。インターフェースは特定のクラスではなく、クラスの継承構造全体に設定されたものなので、クラスの継承構造のどこかのクラスに具象メソッドがあれば良いのです。

インターフェースの書式

修飾子 interface インターフェス名 {
定数フィールド
抽象メソッド
}

定数フィールドの書式(暗黙にpublic static final)

データ型 フィールド名 = 初期値 ;

抽象メソッドの書式(暗黙にpublic abstract)

戻り値の型 メソッド名 (引数リスト ) ;

インターフェス実装の書式

修飾子 class クラス名 implements インターフェス名 ,インターフェス名 ,… {
  メソッドのオーバーライド定義({ }だけを記述して処理は記述しない)
}

インターフェースは抽象クラスと違って相手に与える定数(相手に変えてもらっては困る)とstaticな初期化された変数(インスタンスを作らなくても使える)及び相手に解放する振舞いとしての抽象メソッドしか記述できません

インタフェース内で宣言されるフィールドは暗黙的にpublic static finalで修飾された定数となります。従ってprotectedやprivateで修飾するとコンパイルエラーになります。また、初期化しない場合もコンパイルエラーになります。

インタフェースは具象クラスに実装してもらって初めて存在意義が出ます。そこで、インタフェースを実現したクラスの具象メソッドのアクセス修飾子はインターフェースと同じかより緩い物でなければいけません。インターフェースは暗黙的にpublicなのでpublicしか使えません

インターフェース

interface InterfaceSample {
	final String teisu = "定数";
	static int intNum = 0;
	
	void interfaceFeldPrint();
}

インターフェースを実装した抽象クラス(但し、実際は何もしていない)

abstract class InterfaceSubClass implements InterfaceSample{ }

抽象クラスを継承したサブクラス(インターフェースの実装を実際に行っている)

class AbstractSubClass extends InterfaceSubClass{
	
	public void interfaceFeldPrint() {
		System.out.println("interfaceFeld : " + teisu + ", " + intNum);
	}
}

抽象メソッドのサブクラスを通して、インターフェースで定義されたメソッドを実行する処理

public class InterfaceAbstractTest {
	public static void main(String[] args) {

		AbstractSubClass asc =new AbstractSubClass( ); 
		asc.interfaceFeldPrint();
	}
}

実行結果

>java InterfaceAbstractTest
interfaceFeld : 定数, 0

>

抽象クラスとインターフェースの使い分け
一般のクラスとは違うといっても抽象クラスもクラスですから、objectからのクラスの階層構造のどこかに位置します。一方、インターフェースは外見的に抽象クラスと似ていてもクラスではないので、この階層構造の中には位置しません。インターフェースは特定のクラスに付随するだけです。このためクラスの継承(extends)に対し、インターフェースでは実装(implements)と言います。

抽象クラスといえどもクラスです。抽象メソッドだけでなく具象メソッドも持つことが出来ますから、抽象メソッドもスーパークラスの性格を引き継いだサブクラスを作る仕組み、という点ではクラスと変わりません。
インターフェースは英語の意味から考えるとインターフェースは「つなぎ合わせる」「調和させる」です。つまり、インターフェースはクラス間をつなぎ合わせる仕組みです。この点こそが抽象クラスとインターフェースの違いであり、使い分けのポイントです。では何をつなぎ合わせ、調和させるのでしょう。

実際の開発では大勢のプログラマがプログラミングを分担し、数多くの機能(振舞い)を開発するので、全ての開発が同期して開発できるわけではありません。つまりある部分を開発し、テストしようとしても関連する別の部分はまだ設計中で結合してテストすることが出来なかったり、開発している場所が離れていてリアルタイムで開発を同期させることが出来なかったり、そもそもシステムが複雑で部分部分を個別にテストしてからでないと結合してテストが出来なかったりします。

このような場合、インターフェースを使えば複雑なシステムをモジュールに分解して、モジュール単位で開発を進め、単体テストが終わったら実装モジュールを実際の物に入れ替えて総合テストを行う事ができます。つまりインターフェースは実装の仕方は問わずに相手にしてもらいたい事だけを定義し、自分と相手モジュールをつなぎ合わせます。

プログラムの部品化に戻る

継承

継承(inheritance)とは、あるクラスの特徴(フィールドとメソッド)を引き継ぎながら、継承元の特徴を一部変更したり、新たな特徴を付加して新しいクラスを作りだす仕組みです。継承によりソフトウェアでも実物の部品と同じようにパーツからユニット、コンポーネントを作る事ができ、階層構造を持ったプログラム部品を作ることによりプログラムの生産性が大きく向上します。

例えば類似のクラスがいくつか必要な場合、共通部分をコピーしていくつかのクラスを作ることも出来ますが、もしコピーした共通部分を変更したくなった場合、コピーした全ての共通部分を修正しなければいけません。これに対し継承を使って類似クラスを作れば、共通部分をクラスとして作りそのクラスを継承すれば、類似クラスが作れます。そして、共通部分の変更が必要になった場合、継承元のクラスを修正しさえすれば、継承して作った全ての類似クラスは何も手をかけることなく共通部分が変更されたものとして使うことが出来ます

継承の元になるクラスをスーパークラス、スーパークラスを継承したクラスはサブクラスと呼びます。

継承の書式

クラス宣言でスーパークラスを明示します。
class サブクラス名 extends スーパークラス名 { }

スーパークラス
通常のクラスとして記述します。

public class NormalTV {
	
	void on() {
		System.out.println("\t電源を入れます。");
	}
	void off() {
		System.out.println("\t電源を切ります。");
	}
	void display() {
		System.out.println("\t番組を視聴します。");
	}
	void channelChange() {
		System.out.println("\tチャネルを切り替えます。");
	}
	void functions() {
		on();
		off();
		display();
		channelChange();
	}
}

サブクラス
スーパークラスの明示と共にスパークラスとのフィールドおよびメソッドの差分を記述します。

public class SmartTV extends NormalTV {
	
	void internet() {
		System.out.println("\tインターネットにアクセスします。");
	}
	void recode() {
		System.out.println("\t番組を録画します。");
	}
	void dvd() {
		System.out.println("\tDVDを再生します。");
	}
	void functions() {
		super.functions();
		internet();
		recode();
		dvd();
	}
}

スーパークラス、サブクラスを利用する処理

public class InheritanceSample {
	public static void main(String[] args) {

		NormalTV ntv = new NormalTV();
		SmartTV stv = new SmartTV();
		
		System.out.println("NomalTVの機能:");
		ntv.functions();
		System.out.println("SmartTVの機能:");
		stv.functions();
	}
}

実行結果

>java InheritanceSample
NomalTVの機能:
        電源を入れます。
        電源を切ります。
        番組を視聴します。
        チャネルを切り替えます。
SmartTVの機能:
        電源を入れます。
        電源を切ります。
        番組を視聴します。
        チャネルを切り替えます。
        インターネットにアクセスします。
        番組を録画します。
        DVDを再生します。

>

これらのクラスの継承関係をクラス図で表すと

継承関係はサブクラスからスーパークラスに白い三角形の実線矢印を引きます。(親を指し示す)

なお、継承によってサブクラスはスーパークラスの特徴を引き継ぎますが、次の2つは引き継げません。

コンストラクタ
privateなフィールドやメソッド

サブクラスはスーパークラスのコンストラクタを引き継がないという事は、サブクラスは独自のコンストラクタを持たなければなりません。そして、サブクラスはスーパークラスとサブクラスの付加部分で構成されているので、サブクラスのインスタンスを作るためにはスーパークラスとサブクラス両方のコンストラクタを持たせる必要があります

そこで、サブクラスでコンストラクタを独自に定義する場合はスーパークラスが持つコンストラクタが先に実行され、サブクラスが持つコンストラクタが後から実行されるようにサブクラスのコンストラクタの先頭に、スーパークラスのコンストラクタを呼び出すコードsuper();を追加しなければいけません。

一方、デフォルトコンストラクタを使う場合はコンパイラが自動的にサブクラスのデフォルトコンストラクタの先頭でスーパークラスのデフォルトコンストラクタを呼ぶコードを追加します。

なお、Javaではクラスの多重継承は禁止されています。(インタフェースはクラスではないので、多重実現は認められています。)

また、継承関係にあるサブクラスのインスタンスの中身は、スーパークラスとサブクラス(スーパークラスとの差分)のインスタンスで構成されています。そして、各インスタンスは独立しておりJVMからは別々に扱われています。従って、スーパークラスとサブクラスで同名のインスタンス変数があったとしても、それらは異なるものです。
そこで、メソッドがインスタンス変数を扱う場合、同一クラス内のインスタンス変数であればthis.を、サブクラスからスーパークラスのインスタンス変数であればsuper.をインスタンス変数名の前に付けて記述します。

プログラムの部品化に戻る