応用第3日目:抽象クラス
類似する概念の集約
サンプルプログラム
以下のサンプルでは、カラスとすずめというクラスを別々に定義しています。
package exday3;
// カラスクラス
public class Crow1{
private String name="カラス";
// 名前を取得
public String getName(){ return name; }
// カラスがなく
public void sing(){ System.out.println("カーカー"); }
}
package exday3;
// すずめクラス
public class Sparrow1 {
private String name="すずめ";
// 名前を取得
public String getName(){ return name; }
// 雀が鳴く
public void sing(){ System.out.println("チュンチュン"); }
}
package exday3;
public class SampleEx301 {
public static void main(String args[]){
Crow1 c = new Crow1(); // カラスクラス
Sparrow1 s = new Sparrow1(); // すずめクラス
// カラスがなく
System.out.print(c.getName()+" : ");
c.sing();
// 雀がなく
System.out.print(s.getName()+" : ");
s.sing();
}
}
すずめ : チュンチュン
Crow1とSparrow1はどちらも「名前を持ち、鳴く」という共通の性質を持っています。しかし、それぞれ別のクラスとして定義されているため、コードが重複しています。このような場合に、共通の性質を抽象クラスとして切り出すことで、設計を整理することができます。(図3-1)
図3-1:「鳥」は、あくまでも抽象的な概念
抽象クラス
抽象クラスとは
共通の性質をまとめるために使用するクラスを抽象クラスと言います。抽象クラスはabstractキーワードをつけて定義します。抽象クラスは直接インスタンスを生成することはできません。
...
}
また、抽象クラスには処理を持たない抽象メソッドを定義することができます。抽象メソッドは子クラスで必ずオーバーライド(上書き)しなければなりません。
サンプルプログラム
package exday3;
// 抽象クラス(鳥)
public abstract class Bird {
// 名前フィールド
private String name;
// 引数つきコンストラクタ
Bird(String name){
this.name = name;
}
// 名前の取得
public String getName(){ return name; }
// 鳴く(抽象メソッド)
abstract void sing();
}
package exday3;
// カラスクラス
public class Crow2 extends Bird{
// コンストラクタ(引数なし)
public Crow2(){
super("カラス"); // Birdクラスの引数つきコンストラクタを呼び出す
}
// カラスがなく
@Override
public void sing(){ System.out.println("カーカー"); }
}
package exday3;
// すずめクラス
public class Sparrow2 extends Bird {
// コンストラクタ(引数なし)
public Sparrow2(){
super("すずめ"); // Birdクラスの引数つきコンストラクタを呼び出す
}
// 雀が鳴く
@Override
public void sing(){ System.out.println("チュンチュン"); }
}
package exday3;
public class SampleEx302 {
public static void main(String args[]){
Crow2 c = new Crow2();
Sparrow2 s = new Sparrow2();
// カラスがなく
System.out.print(c.getName()+" : ");
c.sing();
// 雀がなく
System.out.print(s.getName()+" : ");
s.sing();
}
}
すずめ : チュンチュン
Crow2とSparrow2はBirdクラスを継承しています。コンストラクタ内のsuper("カラス")は、親クラス(Bird)の引数付きコンストラクタを呼び出す命令です。これにより、nameフィールドに「カラス」または「すずめ」が設定されます。(図3-2)
図3-2:superによる、親クラスのコンストラクタの呼び出し
@Overrideアノテーション
Crow2とSparrow2のsing()メソッドには@Overrideアノテーションが付いています。アノテーションとは、クラス・メソッド・フィールドなどに付加情報を与えるJavaの仕組みです。@Overrideは、そのメソッドが親クラスまたはインターフェースのメソッドをオーバーライド(上書き)していることをコンパイラと読み手に明示するアノテーションです。
@Overrideは省略することができますが、付けることを強く推奨します。理由は以下のとおりです。
- コンパイラによる検証:メソッド名や引数の型・数が親クラスと異なる場合、コンパイルエラーとして即座に検出できます。@Overrideなしでメソッド名をスペルミスすると、オーバーライドではなく新しいメソッドの追加と見なされ、気づかないバグの原因になります。
- コードの可読性向上:コードを読む人が、そのメソッドがオーバーライドであることを一目で判断できます。
- 親クラス変更への対応:後から親クラスのメソッドのシグネチャ(名前・引数)が変更された場合、@Overrideが付いていればコンパイルエラーで変更に気づくことができます。
public void メソッド名() {
処理
}
@Overrideの他に、@Deprecated(廃止予定の宣言)や@SuppressWarnings(警告の抑制)など、Javaには様々なアノテーションが用意されています。主なアノテーションとその使い方は資料5.Javaのアノテーションで詳しく紹介しています。
抽象クラスを用いることのメリット
サンプルプログラム
抽象クラスを使うと、親クラスの型の変数でまとめて扱うことができます。
package exday3;
public class SampleEx303 {
public static void main(String args[]){
Bird b[] = new Bird[2]; // Birdクラスの変数の配列を生成
b[0] = new Crow2(); // b[0]に、Crow2クラスのインスタンスを生成
b[1] = new Sparrow2(); // b[1]に、Sparrow2クラスのインスタンスを生成
for(int i = 0; i < b.length ; i++){
System.out.print(b[i].getName()+" : ");
b[i].sing();
}
}
}
すずめ : チュンチュン
Bird型の配列にCrow2とSparrow2のインスタンスをまとめて格納し、ループで処理しています。b[i].sing()を呼び出すと、実際のインスタンスの種類に応じて適切なsing()が実行されます。このような仕組みをポリモーフィズム(多態性)と言います。(図3-3)
図3-3:Birdクラスから、インスタンスクラスのメソッドを呼び出す