1. 抽象クラスと抽象メソッド
ときどき(プログラミングでも)こう言いたくなることがあります。「うーん、具体的なやり方はまだ分からないけど、これが必要なことは確かだ!」たとえば、すべての動物は音を出せるべきですが、どんな音かは動物ごとに異なります。こうした場面のために Java には抽象クラスと抽象メソッドがあります。
抽象クラスとは、直接インスタンス化できないクラスのことです(Animal が抽象なら、new Animal()とは書けません)。その代わり継承はできます。抽象クラスには通常の(実装済みの)メソッドも、抽象的な(宣言だけで実装のない)メソッドも含められます。
抽象メソッドとは本体のないメソッドです。キーワード abstract を付けて宣言し、サブクラスで必ず実装しなければなりません(サブクラス自体が抽象である場合を除く)。
身近な例
動物園アプリがあるとしましょう。すべての動物に makeSound() メソッドを持たせたいのですが、どんな音かは分かりません。そのときは抽象クラスを用意します。
public abstract class Animal {
public abstract void makeSound(); // 抽象メソッド
}
具体的な動物はそれぞれ独自にこのメソッドを実装します。
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("ワンワン!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("ニャー!");
}
}
ここで誰かが new Animal() を作ろうとすると、コンパイラはすぐに「ごめん、抽象的な動物なんて自然界にはいないよ!」と言って止めてくれます。これは有用で、プログラム内には具体的な振る舞いを持つ具体的な動物だけが存在することを保証できます。
2. 抽象化によるポリモーフィズム
抽象化とは、オブジェクト群に共通するインターフェースを取り出すことです。抽象クラスはまさにその共通インターフェースを定義し、すべてのサブクラスがどのメソッドを必ず実装すべきかを示します。
ポリモーフィズムと抽象化は組み合わせで機能します。抽象クラスは必要なメソッドの存在を保証し、ポリモーフィズムは基底型の参照経由でそれらのメソッドを呼び出せるようにします。
例:動物園を作る
抽象クラス Animal と、いくつかのサブクラスを用意しましょう。
public abstract class Animal {
public abstract void makeSound();
}
public class Cow extends Animal {
@Override
public void makeSound() {
System.out.println("モー!");
}
}
public class Duck extends Animal {
@Override
public void makeSound() {
System.out.println("ガーガー!");
}
}
次に、動物の配列を作れます。
Animal[] zoo = {
new Dog(),
new Cat(),
new Cow(),
new Duck()
};
for (Animal animal : zoo) {
animal.makeSound(); // 各動物で「正しい」メソッドが呼ばれる
}
配列の各要素は具体的な動物ですが、コードからは単なる Animal として扱われます。ポリモーフィズムと抽象化のおかげで、どのオブジェクトにも makeSound() が存在し、正しく動作することが保証されます.
3. ポリモーフィズムのための抽象クラスの活用
より実践的な例を見てみましょう。社員管理アプリを作っているとします。社員には、マネージャー、開発者、テスターなどの種類があります。全員に共通のメソッド work() があり、実行内容はそれぞれ異なります。
抽象クラス Employee
public abstract class Employee {
protected String name;
public Employee(String name) {
this.name = name;
}
public abstract void work();
}
具体的なサブクラス
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " はチームを指揮します。");
}
}
public class Developer extends Employee {
public Developer(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " はコードを書きます。");
}
}
public class Tester extends Employee {
public Tester(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " はアプリをテストします。");
}
}
ポリモーフィズムの利用
社員の配列を作り、各要素に対して work() を呼び出せます。
Employee[] employees = {
new Manager("アンナ"),
new Developer("イワン"),
new Tester("マリア")
};
for (Employee e : employees) {
e.work();
}
結果:
アンナ はチームを指揮します。
イワン はコードを書きます。
マリア はアプリをテストします。
ポイント:ループの中で具体的な社員タイプを知る必要はありませんし、知りたくもありません。ただ work() を呼べば、各オブジェクトが自分の仕事をしてくれます。
4. 役に立つポイント
メソッド実装の保証
抽象クラスは、すべてのサブクラスに必要なメソッドの実装を強制します。サブクラスで抽象メソッドの実装を忘れると、コンパイラがすぐに「それは実装すべきだよ!」と教えてくれます。
汎用インターフェース
抽象型の配列やリスト(Employee[]、List<Animal>)で動くコードはとても汎用的です。新しいサブクラスを追加しても、メインのコードを変更する必要はありません。
「よくわからない」オブジェクトからの防御
抽象クラスは直接生成できないため、必要なメソッドを実装していない「よくわからない」タイプのオブジェクトが、うっかり作られてしまうことを防げます。
理論と構文:抽象クラスと抽象メソッドの宣言方法
- 抽象クラスは class の前に abstract を付けて宣言します。
- 抽象メソッドは abstract を付けて宣言し、本体は持ちません(セミコロンのみ)。
- 少なくとも1つの抽象メソッドを持つクラスは、必ず抽象クラスでなければなりません。
- 抽象クラスを継承するクラスは、その抽象メソッドをすべて実装するか、自身も抽象クラスである必要があります。
スキーマ
public abstract class Animal {
public abstract void makeSound();
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("ニャー!");
}
}
5. 抽象クラスでよくあるミス
エラー1:抽象クラスのインスタンスを生成しようとする。
new Animal() のようなコードはコンパイルできません。抽象クラスは、部品のない家具の組み立て説明書のようなものです。具体的なサブクラスが現れるまで、オブジェクトは組み立てられません。
エラー2:サブクラスで抽象メソッドの実装を忘れる。
抽象メソッドを宣言したのに、継承側で実装しなかった(かつそのクラスを抽象にもしていない)場合、コンパイラはエラーを出します。
エラー3:アクセス修飾子を忘れる。
オーバーライドしたメソッドは、基底クラスより厳しいアクセス修飾子にはできません。たとえば、抽象メソッドが public なら、実装側も public でなければならず(protected や private にはできません)。
エラー4:本体付きの抽象メソッドを使おうとする。
abstract メソッドは本体を持てません。さもないと、コンパイラは「どっちにするの? 抽象か、それとも実装か?」と目を白黒させるでしょう。
エラー5:静的メソッドにはポリモーフィズムが効かない。
ポリモーフィズムが機能するのはインスタンスメソッドだけです。静的メソッドはオーバーライドされず隠蔽されるだけなので、呼び出し時の挙動は実際のオブジェクトではなく変数の型に依存します。
GO TO FULL VERSION