ポリモーフィズム
配列とリスト
リストの実装
まず、連結リストの仕組みを用いた List クラスを実装します。
List クラスは、指定した位置のデータを取得する get(i)、データを挿入する insert(i, value)、データを削除する delete(i) の3つのメソッドを持ちます。
List.java
class Node {
int value;
Node next;
Node(int value) {
this.value = value;
this.next = null;
}
}
class List {
private Node head;
public int get(int i) {
Node current = head;
for (int j = 0; j < i; j++) {
current = current.next;
}
return current.value;
}
public void insert(int i, int value) {
if (i == 0) {
Node newNode = new Node(value);
newNode.next = head;
head = newNode;
} else {
Node current = head;
for (int j = 0; j < i - 1; j++) {
current = current.next;
}
Node newNode = new Node(value);
newNode.next = current.next;
current.next = newNode;
}
}
public void delete(int i) {
if (i == 0) {
head = head.next;
} else {
Node current = head;
for (int j = 0; j < i - 1; j++) {
current = current.next;
}
current.next = current.next.next;
}
}
}
この List クラスを利用する関数を実装してみます。
ListTest.pde
void setup() {
List list = new List();
list.insert(0, 10);
list.insert(1, 20);
list.insert(2, 30);
printList(list, 3);
}
void printList(List list, int size) {
for (int i = 0; i < size; i++) {
println(list.get(i));
}
}
配列の実装
次に、配列を用いて同様の機能を実現する Array クラスを実装します。
List クラスと同様に get(i), insert(i, value), delete(i) の3つのメソッドを持たせます。
Array.java
class Array {
private int[] data = new int[100];
private int size = 0;
public int get(int i) {
return data[i];
}
public void insert(int i, int value) {
for (int j = size; j > i; j--) {
data[j] = data[j-1];
}
data[i] = value;
size++;
}
public void delete(int i) {
for (int j = i; j < size - 1; j++) {
data[j] = data[j+1];
}
size--;
}
}
この Array クラスを利用する関数を実装してみましょう。
ArrayTest.pde
void setup() {
Array array = new Array();
array.insert(0, 10);
array.insert(1, 20);
array.insert(2, 30);
printArray(array, 3);
}
void printArray(Array arr, int size) {
for (int i = 0; i < size; i++) {
println(arr.get(i));
}
}
ここで、printList 関数と printArray 関数のプログラムを比較してみてください。
引数の型が List か Array かという点を除けば、全く同じプログラムであることに注目してください。
もし表示の形式を少し変えたい場合、両方の関数を修正しなければなりません。 このように同じロジックが点在していると、一方のロジックを修正したときにもう一方を修正し忘れる可能性があり、バグの原因となります。
インターフェースによる共通化
この問題を解決するために、インターフェース を導入して共通化を行います。
List と Array の両方に共通するメソッドを定義したインターフェースを作成し、それを利用するように関数を書き換えます。
IntList.java
interface IntList {
int get(int i);
void insert(int i, int value);
void delete(int i);
}
このインターフェースを List と Array に実装することで、これらを利用する関数を一つにまとめることができます。
UnifiedTest.pde
void setup() {
IntList list = new List();
IntList array = new Array();
list.insert(0, 10);
array.insert(0, 100);
printAll(list, 1);
printAll(array, 1);
}
// 引数の型をインターフェースにする
void printAll(IntList container, int size) {
for (int i = 0; i < size; i++) {
println(container.get(i));
}
}
関数を一つにまとめることができたため、ロジックの修正が必要な場合も一箇所の修正で済み、修正ミスが起こりにくくなったことが確認できます。
ポリモーフィズムとJavaのインターフェース
ポリモーフィズム
先ほどの例のように、異なるクラスのインスタンスを共通の型(インターフェース)で扱うことができる性質を ポリモーフィズム(多態性) と呼びます。
ポリモーフィズムとは、同じ命令を送っても、受け取るオブジェクトの種類によって異なる挙動を示す仕組みのことです。
今回の例では、printAll 関数は container.get(i) という命令を出していますが、中身が List なら連結リストの探索が走り、Array なら配列の要素アクセスが走ります。
操作する側はその中身を意識することなく、共通の方法でデータを扱うことができます。
インターフェースの定義と実装
Javaでポリモーフィズムを実現するための重要な仕組みがインターフェースです。
インターフェースを定義するには interface キーワードを使用します。
インターフェース内には、そのインターフェースを持つクラスが実装すべきメソッドの「型(名前、引数、戻り値)」だけを記述します。
interface インターフェース名 {
戻り値の型 メソッド名(引数...);
}
具体的な定義の例は以下の通りです。
PaymentMethod.java
interface PaymentMethod {
void pay(int amount);
}
クラスにインターフェースを適用するには implements キーワードを使用します。
インターフェースを実装したクラスは、そのインターフェースで定義されているメソッドを全て実装(具体的な処理を記述)しなければなりません。
class クラス名 implements インターフェース名 {
// メソッドの実装
}
具体的な実装の例は以下の通りです。
PaymentMethod インターフェースを実装した CreditCard クラスと QRCode クラスを定義し、それぞれ異なる支払い処理を実装しています。
PaymentTest.pde
interface PaymentMethod {
void pay(int amount);
}
class CreditCard implements PaymentMethod {
public void pay(int amount) {
println("クレジットカードで" + amount + "円支払いました。");
}
}
class QRCode implements PaymentMethod {
public void pay(int amount) {
println("QRコード決済で" + amount + "円支払いました。");
}
}
void setup() {
PaymentMethod p1 = new CreditCard();
PaymentMethod p2 = new QRCode();
checkout(p1, 1000);
checkout(p2, 500);
}
void checkout(PaymentMethod method, int amount) {
method.pay(amount);
}
実行結果
クレジットカードで1000円支払いました。
QRコード決済で500円支払いました。
演習
演習1
ポリモーフィズムの目的やメリットを踏まえ、プログラムにおいてポリモーフィズムが望ましい具体的な場面を挙げ、その理由を述べなさい。
演習2
引数として IntList を受け取り、その中の全要素の平均値を返す関数 calcAverage(IntList list, int size) を作成しなさい。
作成した関数が、 List インスタンスと Array インスタンスの両方に対して動作することを確認しなさい。
演習3
プログラムで扱っている配列のデータを、異なる形式の文字列に変換して出力する処理を考えます。
整数型の配列を受け取り、特定の形式の文字列に変換して返す DataExporter インターフェースを定義しなさい。
このインターフェースは String export(int[] data) というメソッドを持つものとします。
次に、このインターフェースを実装した以下の2つのクラスを作成しなさい。
CsvExporter: 配列の要素をカンマ(,)で区切った文字列を返すクラス(例:"10,20,30")JsonExporter: 配列の要素をカンマで区切り、全体を角括弧([])で囲んだJSON形式の文字列を返すクラス(例:"[10,20,30]")
さらに、以下の setup 関数が正しく動作することを確認しなさい。
void setup() {
int[] data = {10, 20, 30};
DataExporter exporter1 = new CsvExporter();
DataExporter exporter2 = new JsonExporter();
println("CSV形式: " + exporter1.export(data));
println("JSON形式: " + exporter2.export(data));
}
実行結果
CSV形式: 10,20,30
JSON形式: [10,20,30]
演習の解答例
演習1の解答例
場面: 描画アプリケーションにおける複数の図形(円、四角形など)の描画処理
理由: 図形の種類によって描画の具体的な手順は異なるが、「描画する」という操作自体は共通しているため。 ポリモーフィズムを利用して共通のインターフェースを通すことで、呼び出し側は図形の種類を意識せずに一括して描画処理を実行できる。 また、新しい種類の図形を追加する際にも、呼び出し側のプログラムを変更せずに済むようになる。
演習2の解答例
calcAverage.pde
float calcAverage(IntList list, int size) {
if (size == 0) return 0;
int sum = 0;
for (int i = 0; i < size; i++) {
sum += list.get(i);
}
return (float)sum / size;
}
演習3の解答例
配列のデータを指定の形式に変換するための DataExporter インターフェースを定義し、CSV形式とJSON形式のそれぞれに対応するクラスを実装する。
各クラスでは配列の要素をループで順に取り出し、カンマや括弧を適切に結合して文字列を組み立てている。
実装例は以下の通りである。
DataExporter.pde
interface DataExporter {
String export(int[] data);
}
class CsvExporter implements DataExporter {
public String export(int[] data) {
if (data.length == 0) return "";
String result = "" + data[0];
for (int i = 1; i < data.length; i++) {
result += "," + data[i];
}
return result;
}
}
class JsonExporter implements DataExporter {
public String export(int[] data) {
if (data.length == 0) return "[]";
String result = "[" + data[0];
for (int i = 1; i < data.length; i++) {
result += "," + data[i];
}
result += "]";
return result;
}
}
void setup() {
int[] data = {10, 20, 30};
DataExporter exporter1 = new CsvExporter();
DataExporter exporter2 = new JsonExporter();
println("CSV形式: " + exporter1.export(data));
println("JSON形式: " + exporter2.export(data));
}