抽象クラス

抽象クラスとは

オブジェクト指向プログラミングでは、複数の異なるクラスで共通の機能や型をまとめたい場合があります。 しかし、共通の親クラスを定義する際、親クラス自体のインスタンスを作成する必要がないこともあります。 また、親クラス側で具体的な処理を記述できないメソッドが存在することもあります。

このような場合に用いられるのが 抽象クラス です。 抽象クラスは、インスタンス化できない未完成のクラスです。 抽象クラスには、処理の実装を持たない 抽象メソッド を定義できます。 抽象メソッドの実装は、その抽象クラスを継承した子クラスに強制されます。

抽象クラスと抽象メソッドの定義

抽象クラスを定義するには、クラス宣言の前に abstract キーワードを指定します。 抽象クラスの中に抽象メソッドを定義する場合も、メソッド宣言の前に abstract キーワードを指定します。 抽象メソッドは具体的な処理の内容を持たないため、中かっこ { } の代わりにセミコロン ; を記述します。

例えば、動物を表す抽象クラス Animal と、鳴き声を出す抽象メソッド makeSound を定義するプログラムは以下のようになります。

Animal.java

abstract class Animal {
  private String name;

  public Animal(String name) {
    this.name = name;
  }

  public String getName() {
    return this.name;
  }

  // 抽象メソッドの定義
  public abstract void makeSound();
}

抽象クラスの継承とインスタンス化

抽象クラスは未完成のクラスであるため、new 演算子を用いて直接インスタンスを生成することはできません。 抽象クラスを利用するためには、別のクラスでその抽象クラスを継承する必要があります。 継承する際は、通常のクラスの継承と同様に extends キーワードを使用します。 このとき、継承先の子クラスでは、親クラスで定義されているすべての抽象メソッドをオーバーライドして、具体的な処理を実装しなければなりません。 実装を行わない場合はコンパイルエラーになります。

Animal クラスを継承して、具体的な動物のクラスを定義するプログラムは以下のようになります。

Dog.java

class Dog extends Animal {
  public Dog(String name) {
    super(name);
  }

  // 親クラスの抽象メソッドを実装する
  @Override
  public void makeSound() {
    println(this.getName() + "がワンワンと鳴きました。");
  }
}

これらのクラスを利用して、オブジェクトを生成しメソッドを呼び出すプログラムは以下のようになります。

void setup() {
  // 抽象クラスは直接インスタンス化できない
  // Animal animal = new Animal("ポチ"); // エラー:抽象クラスはインスタンス化できない

  // 子クラスのインスタンスを生成する
  Dog dog = new Dog("ポチ");
  dog.makeSound();

  // 親クラスの型として扱うこともできる
  Animal myAnimal = new Dog("タロウ");
  myAnimal.makeSound();
}

実行結果

ポチがワンワンと鳴きました。
タロウがワンワンと鳴きました。

Numberクラスの活用

Javaの標準ライブラリには、抽象クラスが効果的に利用されている例が数多くあります。 その代表的な一つが、数値を表すクラスの親クラスである Number クラスです。

IntegerDoubleFloat などのラッパークラスは、すべて Number クラスを継承しています。 Number クラスには、数値をそれぞれのプリミティブ型として取り出すための intValuedoubleValue などの抽象メソッドが定義されています。

Number クラスを用いることで、異なる数値型のオブジェクトを同一の型としてまとめて扱うことができます。 プログラムは以下のようになります。

void setup() {
  // Number型の配列に異なる数値オブジェクトを格納する
  Number[] numbers = new Number[3];
  numbers[0] = 10;    // Integer
  numbers[1] = 3.14;  // Double
  numbers[2] = 5.5f;  // Float

  // 多態性を利用して、一貫した処理を行う
  for (Number n : numbers) {
    println("double値: " + n.doubleValue());
    println("int値: " + n.intValue());
  }
}

実行結果

double値: 10.0
int値: 10
double値: 3.14
int値: 3
double値: 5.5
int値: 5

配列の要素はそれぞれ異なるクラスのオブジェクトですが、共通の親クラスである Number 型の変数として扱うことができます。 また、Number クラスに定義されている doubleValueintValue メソッドを呼び出すことで、それぞれのオブジェクトが持つ本来の値を適切な型で取り出すことができます。

AbstractListクラスの活用

もう一つの代表的な抽象クラスの例が AbstractList クラスです。 AbstractList クラスは、データを一列に並べて管理するリストの動作を実装する際の土台となる抽象クラスです。

AbstractList を継承した子クラスは、要素を取得する get メソッドと、リストの要素数を返す size メソッドの2つを最小限実装するだけで、リストとして機能するようになります。 リストの繰り返し処理や要素の文字列表現などの共通の処理は、AbstractList 側であらかじめ具体的に実装されているためです。

指定した個数の偶数を順に返す、読み取り専用のリストを AbstractList を継承して作成するプログラムは以下のようになります。

EvenList.java

import java.util.AbstractList;

class EvenList extends AbstractList {
  private int maxCount;

  public EvenList(int maxCount) {
    this.maxCount = maxCount;
  }

  // 要素を取得する抽象メソッドの実装
  @Override
  public Integer get(int index) {
    if (index < 0 || index >= this.maxCount) {
      throw new IndexOutOfBoundsException();
    }
    return index * 2;
  }

  // 要素数を返す抽象メソッドの実装
  @Override
  public int size() {
    return this.maxCount;
  }
}

この EvenList クラスを利用するプログラムは以下のようになります。

void setup() {
  EvenList evens = new EvenList(5);

  println("サイズ: " + evens.size());
  println("インデックス2の要素: " + evens.get(2));

  // AbstractListがあらかじめ提供している文字列表現を利用する
  println("リスト全体: " + evens);

  // AbstractListがあらかじめ提供している繰り返し処理を利用する
  for (int num : evens) {
    println(num);
  }
}

実行結果

サイズ: 5
インデックス2の要素: 4
リスト全体: [0, 2, 4, 6, 8]
0
2
4
6
8

EvenList クラス自体には繰り返し処理や [0, 2, 4, 6, 8] のような文字列表現を作る処理は記述していません。 しかし、AbstractList を継承したことで、あらかじめ用意されたこれらの機能を利用できています。 抽象クラスを使用すると、子クラスごとに異なる最小限の処理だけを実装し、共通の処理は親クラスで再利用するという効率的な設計が可能になります。

演習

演習1

抽象クラスと通常のクラスとの違いについて説明しなさい。 また、抽象クラスの中で実装を持たない抽象メソッドを定義する目的を述べなさい。

演習2

分子と分母を持つ有理数を表す Fraction クラスを定義しなさい。 このクラスは、数値としての共通した扱いを可能にするために Number 抽象クラスを継承し、以下の抽象メソッドをオーバーライドしなさい。

  • intValue(): 有理数の値を整数に変換して返す。
  • doubleValue(): 有理数の値を小数に変換して返す。
  • floatValue(): 有理数の値を小数に変換して返す。
  • longValue(): 有理数の値を整数に変換して返す。

また、以下の setup 関数が正しく動作するように Fraction クラスを設計しなさい。

void setup() {
  // 3分の2を表す有理数オブジェクトを生成する
  Fraction fraction = new Fraction(2, 3);

  println("doubleValue: " + fraction.doubleValue());
  println("intValue: " + fraction.intValue());
}

実行結果

doubleValue: 0.6666666666666666
intValue: 0

演習3

フィボナッチ数列の各項を順に返す、読み取り専用のリストを表す FibonacciList クラスを作成しなさい。 フィボナッチ数列は、0, 1, 1, 2, 3, 5, 8, 13, ... のように、前の2つの項の和が次の項になる数列です。 FibonacciList クラスは AbstractList<Integer> を継承し、get メソッドと size メソッドをオーバーライドして実装しなさい。 以下の setup 関数が正しく動作するようにクラスを設計しなさい。

void setup() {
  // 最初から10個のフィボナッチ数を保持するリストを生成する
  FibonacciList fibList = new FibonacciList(10);

  println("リスト全体: " + fibList);

  for (int num : fibList) {
    println(num);
  }
}

実行結果

リスト全体: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
0
1
1
2
3
5
8
13
21
34

演習の解答例

演習1の解答例

違い: 通常のクラスはインスタンスを直接生成できるが、抽象クラスはインスタンスを直接生成できない。 また、通常のクラスには処理の実装がないメソッドを定義できないが、抽象クラスには処理の実装がない抽象メソッドを定義できる。

目的: 子クラスに対して特定のメソッドの実装を強制し、共通の操作手段を保証するため。 これにより、多様な子クラスのオブジェクトを親クラスの型で一貫して操作できるようになる。

演習2の解答例

分子と分母を有理数として扱い、Number 抽象クラスを継承した Fraction クラスの実装例は以下の通りである。 親クラスの抽象メソッドである各数値への変換処理をオーバーライドし、分子を分母で除算した値をそれぞれの戻り値の型で返している。

Fraction.java

class Fraction extends Number {
  private int numerator;
  private int denominator;

  public Fraction(int numerator, int denominator) {
    this.numerator = numerator;
    this.denominator = denominator;
  }

  @Override
  public int intValue() {
    return this.numerator / this.denominator;
  }

  @Override
  public double doubleValue() {
    return (double) this.numerator / this.denominator;
  }

  @Override
  public float floatValue() {
    return (float) this.numerator / this.denominator;
  }

  @Override
  public long longValue() {
    return (long) this.numerator / this.denominator;
  }
}

演習3の解答例

フィボナッチ数列を生成し、AbstractList を継承して実装した FibonacciList クラスの実装例は以下の通りである。 リストの要素数情報を返す size と、要求されたインデックスに対応するフィボナッチ数を繰り返し処理によって計算して返す get メソッドを実装している。

FibonacciList.java

import java.util.AbstractList;

class FibonacciList extends AbstractList {
  private int maxCount;

  public FibonacciList(int maxCount) {
    this.maxCount = maxCount;
  }

  @Override
  public Integer get(int index) {
    if (index < 0 || index >= this.maxCount) {
      throw new IndexOutOfBoundsException();
    }
    if (index == 0) {
      return 0;
    }
    if (index == 1) {
      return 1;
    }
    int a = 0;
    int b = 1;
    for (int i = 2; i <= index; i++) {
      int next = a + b;
      a = b;
      b = next;
    }
    return b;
  }

  @Override
  public int size() {
    return this.maxCount;
  }
}

results matching ""

    No results matching ""