クラスの継承

共通の処理とコードの重複

プログラミング言語の標準ライブラリでは、配列やリストといった多様な「データ構造」が提供されています。 例えば、順番にデータを保存するリスト(IntArrayList)と、最後に追加したデータから取り出すスタック(IntStack)を考えてみましょう。

プログラムは以下のようになります。

NaiveCollections.java

class IntArrayList {
  private int[] data = new int[100];
  private int length = 0;

  public void add(int value) {
    this.data[this.length] = value;
    this.length++;
  }

  public int size() {
    return this.length;
  }

  public void clear() {
    this.length = 0;
  }
}

class IntStack {
  private int[] data = new int[100];
  private int length = 0;

  public void push(int value) {
    this.data[this.length] = value;
    this.length++;
  }

  public int pop() {
    this.length--;
    return this.data[this.length];
  }

  public int size() {
    return this.length;
  }

  public void clear() {
    this.length = 0;
  }
}

このように別々のクラスとして定義すると、内部の配列(data)やデータ数(length)の管理、そして sizeclear といった共通メソッドの実装が重複してしまいます。

extendsキーワードによるクラスの継承

実際の標準ライブラリでは、このようなコードの重複を防ぐために、クラスの継承が活用されています。 共通するフィールドやメソッドを1つの親クラス(例えば AbstractIntCollection)にまとめ、それを各データ構造の子クラスが引き継ぎます。 子クラスから親クラスのフィールドに直接アクセスできるようにするため、アクセス修飾子には private ではなく protected を使用します。

プログラムは以下のようになります。

AbstractIntCollection.java

class AbstractIntCollection {
  protected int[] data = new int[100];
  protected int length = 0;

  public int size() {
    return this.length;
  }

  public void clear() {
    this.length = 0;
  }
}

この親クラスを継承して、リストとスタックのクラスを作成します。 プログラムは以下のようになります。

Collections.java

// リストクラス
class IntArrayList extends AbstractIntCollection {
  public void add(int value) {
    this.data[this.length] = value;
    this.length++;
  }
}

// スタッククラス
class IntStack extends AbstractIntCollection {
  public void push(int value) {
    this.data[this.length] = value;
    this.length++;
  }

  public int pop() {
    this.length--;
    return this.data[this.length];
  }
}

以下のプログラムを実行して、継承したクラスがどのように機能するかを確認してみましょう。

CollectionTest.pde

class AbstractIntCollection {
  protected int[] data = new int[100];
  protected int length = 0;

  public int size() {
    return this.length;
  }

  public void clear() {
    this.length = 0;
  }
}

class IntArrayList extends AbstractIntCollection {
  public void add(int value) {
    this.data[this.length] = value;
    this.length++;
  }
}

class IntStack extends AbstractIntCollection {
  public void push(int value) {
    this.data[this.length] = value;
    this.length++;
  }

  public int pop() {
    this.length--;
    return this.data[this.length];
  }
}

void setup() {
  IntArrayList list = new IntArrayList();
  list.add(10);
  list.add(20);
  println("リストのサイズ: " + list.size());

  IntStack stack = new IntStack();
  stack.push(5);
  println("スタックのサイズ: " + stack.size());
}

実行結果

リストのサイズ: 2
スタックのサイズ: 1

IntArrayListIntStack のクラス内には size メソッドを書いていませんが、親クラスから引き継いで正しく機能していることがわかります。

メソッドのオーバーライド

継承した子クラスでは、親クラスで定義されたメソッドと同じ名前・同じ引数のメソッドを定義し直すことができます。 これをメソッドのオーバーライド(上書き)と呼びます。

例えば、リストにデータが追加される際、「重複する値は追加しない」という特別な性質を持つ IntUniqueList クラスを作成したいとします。 この場合、IntArrayList クラスを継承し、add メソッドだけを独自の処理に上書きすることで、簡単に新しいデータ構造を作ることができます。 子クラスの中から親クラスのメソッドを呼び出す場合は、super キーワードを使用します。

プログラムは以下のようになります。

OverrideTest.pde

class AbstractIntCollection {
  protected int[] data = new int[100];
  protected int length = 0;
  public int size() { return this.length; }
}

class IntArrayList extends AbstractIntCollection {
  public void add(int value) {
    this.data[this.length] = value;
    this.length++;
  }
  public int get(int index) {
    return this.data[index];
  }
}

class IntUniqueList extends IntArrayList {
  // 親クラスのaddメソッドを上書きする
  public void add(int value) {
    // 重複チェック
    for (int i = 0; i < this.length; i++) {
      if (this.data[i] == value) {
        return; // すでに存在する場合は追加を中止
      }
    }
    // 親クラスのaddメソッドを利用して追加
    super.add(value);
  }
}

void setup() {
  IntUniqueList uniqueList = new IntUniqueList();

  uniqueList.add(10);
  uniqueList.add(20);
  uniqueList.add(10); // 重複データなので追加されない

  println("ユニークリストのサイズ: " + uniqueList.size());
}

実行結果

ユニークリストのサイズ: 2

オーバーライドを活用することで、基本となる配列管理の仕組みは親クラスに任せつつ、特定の条件や振る舞いだけを変更した新しいデータ構造を柔軟に実装できるようになります。

演習

演習1

標準ライブラリ等のデータ構造において、クラスの継承やオーバーライドを活用する目的とメリットを述べなさい。

演習2

辞書(連想配列)の基本的な機能を表す AbstractIntDictionary クラスを作成しなさい。 このクラスは、キーを保存する protected String[] keys = new String[100]; と、対応する値を保存する protected int[] values = new int[100];、およびデータ数を管理する protected int length = 0; フィールドを持ちなさい。 また、現在のデータ数を返す int size() メソッドを定義しなさい。

演習3

演習2で作成した AbstractIntDictionary クラスを継承して、IntDictionary クラスを作成しなさい。 このクラスには、キーと値のペアを追加する void put(String key, int value) メソッドを追加しなさい。 また、指定したキーに対応する値を返す int get(String key) メソッドを追加しなさい(見つからない場合は -1 を返すこと)。 ※同じキーが存在する場合の上書き処理は考えなくてよい。

演習4

IntDictionary クラスを継承して、キーが追加されたときにコンソールにログを出力する LoggingIntDictionary クラスを作成しなさい。 このクラスでは put メソッドをオーバーライドし、"ログ: キー[" + key + "] に値[" + value + "] を追加しました" と出力してから、親クラスの put メソッドを呼び出してデータを保存しなさい。 また、setup 関数内で LoggingIntDictionary クラスのインスタンスを生成し、動作を確認しなさい。

演習の解答例

演習1の解答例

場面: リストやスタックなど、複数のデータ構造で配列の管理やサイズ取得といった共通の処理が存在する場面。

理由: 共通部分を AbstractCollection のような親クラスにまとめることでコードの重複を排除でき、バグの修正が容易になるためである。また、オーバーライドを活用することで、既存のリストの機能を引き継ぎながら「重複を許さない」などの独自の機能を持つ新しいデータ構造を効率よく作成できるためである。

演習2の解答例

解答は以下の通りである。

AbstractIntDictionary.java

class AbstractIntDictionary {
  protected String[] keys = new String[100];
  protected int[] values = new int[100];
  protected int length = 0;

  public int size() {
    return this.length;
  }
}

演習3の解答例

親クラスを継承し、独自のメソッドを追加する。

IntDictionary.java

class IntDictionary extends AbstractIntDictionary {
  public void put(String key, int value) {
    this.keys[this.length] = key;
    this.values[this.length] = value;
    this.length++;
  }

  public int get(String key) {
    for (int i = 0; i < this.length; i++) {
      if (this.keys[i].equals(key)) {
        return this.values[i];
      }
    }
    return -1;
  }
}

演習4の解答例

put メソッドをオーバーライドし、ログ出力後に super を用いて親クラスの処理を呼び出す。

LoggingIntDictionaryTest.pde

class AbstractIntDictionary {
  protected String[] keys = new String[100];
  protected int[] values = new int[100];
  protected int length = 0;

  public int size() {
    return this.length;
  }
}

class IntDictionary extends AbstractIntDictionary {
  public void put(String key, int value) {
    this.keys[this.length] = key;
    this.values[this.length] = value;
    this.length++;
  }

  public int get(String key) {
    for (int i = 0; i < this.length; i++) {
      if (this.keys[i].equals(key)) {
        return this.values[i];
      }
    }
    return -1;
  }
}

class LoggingIntDictionary extends IntDictionary {
  public void put(String key, int value) {
    println("ログ: キー[" + key + "] に値[" + value + "] を追加しました");
    super.put(key, value);
  }
}

void setup() {
  LoggingIntDictionary dict = new LoggingIntDictionary();

  dict.put("apple", 100);
  dict.put("banana", 200);

  println("appleの値段: " + dict.get("apple"));
  println("データ数: " + dict.size());
}

実行結果

ログ: キー[apple] に値[100] を追加しました
ログ: キー[banana] に値[200] を追加しました
appleの値段: 100
データ数: 2

results matching ""

    No results matching ""