WisdomSoft - for your serial experiences.

5.1 クラスの継承

クラスは別のクラスを継承して、その機能と役割を引き継ぐことができます。完成された部品の中身に手を加えることなく新しい機能を追加でき、コードの再利用性を高めます。オブジェクト指向によるシステム設計を理解するための重要な概念となります。

5.1.1 機能の拡張

オブジェクト指向プログラミングでは、既存のクラスに新しいフィールドやメソッドを加えたり、既存のメソッドを書き換えることで、より具体的で固定的な処理をクラスに与えることができます。つまり、クラスの機能を拡張することができるのです。これを継承と呼びます。

クラスを継承するとき、基となるクラスをスーパークラスと呼び、継承したクラスをサブクラスと呼びます。サブクラスは、スーパークラスのメンバを引き継ぐため、スーパークラスのフィールドやメソッドに this からアクセスすることができます。クラスを継承するには、クラス宣言で extends キーワードを使って次のように記述します。

クラスの継承
class サブクラス名 extends スーパークラス { ...

クラス名を指定するところまではこれまでと同じです。extends キーワードの後には、スーパークラスとなるクラスの型を指定します。クラスを継承する理由は、プログラムの設計上の都合で様々だと思われますが、一般には機能の拡張や具体化のために行います。機能の拡張とは、例えば 2 次元座標を表す x と y フィールドを持った Point クラスを、3次元の座標を表すために Point3D に継承し z フィールドを追加するというものです。

class Point { int x , y; }
class Point3D extends Point { int z; }

具体化とは、提供する機能の論理的な概念は統一されているが、複数の結果が存在する場合に用いられます。スーパークラスでは必要最小限の機能だけを提供し、サブクラスで実用化できるレベルに組み立てるというのはこの方式です。例えば、哺乳類クラスから猫クラスや犬クラスを構築するようなものです。犬や猫は哺乳類なので、哺乳類という範囲で共通の機能を持ちます。そこで、哺乳類という抽象的なクラスを共有することで操作の統一を図るのです。

class Mammals { ... }
class Kitty extends Mammals { ... }
class Puppy extends Mammals { ... }

基盤となるプログラムコードが同一であれば、スーパークラスを共有することでよりスマートにプログラムを記述することができます。継承をうまく利用することで、似たようなコードを何度も書き直したりする手間を省略し生産性を向上させたり、バグのない完成されたスーパークラスを使いまわすことで安全性を確保することができます。

コード1
class Color { int r , g , b; }
class ColorEx extends Color {
	int a;
	void setColor(int color) {
		a = (color & 0xFF000000) >>> 24;
		r = (color & 0x00FF0000) >>> 16;
		g = (color & 0x0000FF00) >>> 8;
		b = (color & 0x000000FF);
	}
}

class Test {
	public static void main(String args[]) {
		ColorEx color = new ColorEx();
		color.setColor(0xFFFFAA80);
		System.out.println(
			"A = " + color.a + " : R = " + color.r +
			" : G = " + color.g + " : B = " + color.b
		);
	}
}
実行結果
>java Test
A = 255 : R = 255 : G = 170 : B = 128

コード1では、色を表す Color クラスを継承した ColorEx サブクラスを宣言しています。ColorEx は Color クラスのサブクラスとなるため、暗黙的に Color クラスのフィールドを保持していると考えられます。そのため、SetColor() メソッド内では、ColorEx クラス自体では宣言していない r、g、b フィールドにアクセスしていますが、問題はないのです。main() メソッドの ColorEx 型のオブジェクトからフィールドにアクセスしていることからも、ColorEx サブクラスが Color スーパークラスのメンバを継承していることが確認できます。

この例では 1 段階の継承しか行っていませんが、継承は何段階でも行うことができます。ColorEx をさらに拡張するために継承しても問題はありません。ColorEx クラスのサブクラスは、ColorEx クラスと Color クラスの機能を暗黙的に保持することになります。

class A { ... }
class B extends A { ... }
class C extends B { ... }
class D extends C { ... }

このような継承関係も可能であり、なんら問題はありません。D クラスは A、B、C すべての機能を継承します。このような場合、D にとって A、B、C すべてがスーパークラスであると表現できます。また、D クラスにとって extends で直接指定している C クラスを直接のスーパークラスと呼び、C クラスにとって D クラスを直接のサブクラス(直系のサブクラス)と表現します。

5.1.2 参照型の変換

数値型の変数やリテラルは、代入変換やキャスト演算子によって、ナローイング変換やワイドニング変換、そして恒等変換を行うことができました。参照型もこれと同様に変換することが可能です。つまり、クラス型も型変換することができます。

恒等変換の概念は参照型も、同じ型への変換を表します。Color クラス型の変数を Color クラス型に変換すれば恒等変換となりますが、明示的に行うことに意味はありません。

参照型の変換は、継承関係にある場合に型変換することができます。例えば、コード1の ColorEx クラスは、直接のスーパークラス Color の機能を継承しています。このことから、 ColorEx オブジェクトを Color オブジェクトとして扱っても、フィールドやメソッドに安全にアクセスすることが保証されます。そのため、サブクラス型からスーパークラス型への変換にリスクはありません。これを参照型のワイドニング変換と呼びます。

何らかの理由でワイドニング変換されたオブジェクトを、元のサブクラスの型に戻すには、スーパークラス型のオブジェクトをサブクラス型のオブジェクトに変換します。これは、参照型のナローイング変換と呼びます。参照型のナローイング変換は、参照型のワイドニング変換とは異なりリスクを伴います。なぜならば、オブジェクトがサブクラスのインスタンスであることをコンパイルの時点では保証できないためです。

ColorEx オブジェクトを Color 型に変換し、再び ColorEx 型に復元する場合は問題ありません。インスタンスは ColorEx のものなので、ColorEx クラスの情報を含んでいます。しかし、インスタンスが Color の参照を ColorEx に変換した場合、インスタンスは ColorEx の情報を持っていません。この場合は実行時例外となってしまうでしょう。

コード2
class A {}
class B extends A {}
class C extends B {}

class Test {
	public static void main(String args[]) {
		A aa = new A();	//恒等変換
		A ab = new B();	//ワイドニング
		A ac = new C();	//ワイドニング
		B bb = (B)ab;	//ナローイング
		B bc = (B)ac;	//ナローイング
		C cc = (C)bc;	//ナローイング

		System.out.println(aa + "\n" + ab +"\n" + ac);
		C ca = (C)aa;	//例外
	}
}
実行結果
>java Test
A@f6a746
B@5ff48b
C@affc70
Exception in thread "main" java.lang.ClassCastException: A
        at Test.main(Test.java:15)

コード2は参照型の変換を行っています。A クラスの直接のサブクラス B と、B クラスの直接のサブクラス C という階層関係にある 3 つのクラスでは、C クラス型からスーパークラス型である B や A クラス型に、B クラス型から A クラス型に変換することができます。これは、リスクのない参照型のワイドニング変換なので、代入変換で変換することができます。

次に、B や C クラス型のオブジェクトを格納した ab や ac 変数をサブクラス型である B や C に変換しています。この作業はナローイング変換となるため、キャスト演算子で明示的に行わなければなりません。もちろん、C クラスのインスタンスならば B クラス型として扱うこともできるため、ac 変数を bc 変数にナローイング変換していることは問題ではありません。

参照型変数をスーパークラスの型にワイドニング変換しても、実体であるインスタンスは変化しません。つまり、インスタンスの情報が削られるようなことはないので安心してください。ワイドニング変換は、単純にインスタンスが保有するメンバへの参照範囲を狭くすることであって、インスタンスを変換することではないのです。println() メソッドでオブジェクトを表示すると、A クラス型に変換されても、B クラスや C クラスのインスタンスであることを確認することができます。参照型の変換でインスタンスが情報を失うことはないのです。

最後の参照型のナローイング変換は A クラスのインスタンスを参照する aa 変数を C クラス型に変換しようと試みています。しかし、A クラスのインスタンスは C クラスの情報を持っていません。C クラスを表現するために必要なメモリ領域も確保していません。インスタンスに互換性がない以上、不正な変換とされるため実行時に ClassCastException という例外が発生するのです。