初心者入門レベルの【Javaの勉強】ポリモーフィズムは難しくない
2020/04/02
今回は、Javaにおける「ポリモーフィズム」について勉強してみます。
「ポリモーフィズム」とは
「ポリモーフィズム」は、日本語訳で「多様性」とか「多態性」などとも言いますが、オブジェクト指向に含まれる概念のようなものです。
簡単に言うと、一つの「メソッド」名によって異なる機能を実現する仕組みとも言えますが、こういった説明だけでは分かりにくいですね。
例えば“動物”という「クラス」があったとして、その中に“鳴く”という「メソッド」があったとします。
この“動物”の「クラス」を継承して“犬”と“猫”の「クラス」を作って「インスタンス化」してから、それぞれの“鳴く”の「メソッド」を実行した場合。
同じ“鳴く”という「メソッド」でも、“犬”のほうは“ワン”と鳴いて、“猫”のほうは“ニャー”と鳴くようにする仕組みが、「ポリモーフィズム」によって実現できる機能の一種と言えます。
もう一つ例を挙げてみると、“乗算”という「クラス」があったとします。
この中に“計算する”という「メソッド」があったとして、これを“3”という一つの引数だけで呼び出した場合、その引数を二乗した値の“9”を結果として返す仕組みになっているとします。
一方で、その“計算する”という「メソッド」を“4”と“5”という二つの引数を渡して呼び出したときは、それぞれを掛け算した“20”を結果として返す仕組みになっているとします。
このように同じ「メソッド」を異なる引数の形で呼び出したときに、違う動作をするような仕組みも「ポリモーフィズム」によって実現できる機能の一種と言えるんですね。
何となく、イメージがつかめたでしょうか?
それでは実際にJavaにおいては、どのようにして「ポリモーフィズム」を実現しているのか、一緒に学んでいきましょう。
「ポリモーフィズム」によって使いやすい「クラス」ができる
最初に、ごく単純な例として以下のプログラムを作ってみます。
class English {
public void say() {
System.out.println("Hello");
}
}
class Japanese {
public void say() {
System.out.println("こんにちは");
}
}
public class TestPolymorphism01 {
public static void main(String[] args) {
English inEnglish = new English();
Japanese inJapanese = new Japanese();
inEnglish.say();
inJapanese.say();
}
}
これは、2つの異なる「クラス」を利用していながら、同じ名前の「メソッド」を使うことができるという例です。
当たり前と言えば当たり前とも思えますが、これも別の機能を同じ名前の「メソッド」で実現するという「ポリモーフィズム」の概念を使った一つの例となります。
例えばここで“Chinese”という新しい「クラス」があったとき、同様にして“say”という「メソッド」を利用できるのではないか?と類推できるでしょう。
このように「ポリモーフィズム」の概念に沿って作られた「クラス」は、利用者にとってその機能を憶えやすかったり使いやすかったりするようにできるんですね。
「オーバーロード」について
Javaには「ポリモーフィズム」を実現する方法の一つとして、「オーバーロード」という仕組みが用意されています。
「オーバーロード」とは、一つの「クラス」の中に、名前が同じで引数の種類や数が異なる、複数の「メソッド」があることを指します。
これを「メソッド」の多重定義とも言いますが、例として以下のプログラムを作ってみましょう。
class Multiplication {
public void calc(int x) {
System.out.println("x * x = " + (x * x));
}
public void calc(int x, int y) {
System.out.println("x * y = " + (x * y));
}
}
public class TestPolymorphism02 {
public static void main(String[] args) {
Multiplication mult = new Multiplication();
mult.calc(3);
mult.calc(4, 5);
}
}
これは冒頭に例としてふれた“乗算”の「クラス」をプログラムにしたモノですが、同じ“calc”という名前の「メソッド」が、引数の数を変えて複数定義されています。
引数の数のほかに、その型の種類(“int”や“double”など)を変えたり、あるいは並び順などを変えて宣言することができますが、このように「メソッド」を識別する要素を(「メソッド」の名前の違いも含めて)「シグネチャ」と呼びます。
このように「シグネチャ」を変えることで、同じ名前で機能の異なる複数の「メソッド」を定義することを「オーバーロード」すると言いますが、Javaのライブラリでもそれは多く利用されています。
例えばこれまでもよく使っていた“println”なども、その引数として“int”型や“String”型の変数を渡していますが、それぞれ呼び出されている「メソッド」が異なっているわけです。
「オーバーライド」について
同様に、Javaには「ポリモーフィズム」を実現する方法の一つとして、「オーバーライド」という仕組みも用意されています。
「オーバーライド」というのは、「継承」において「スーパークラス」の「メソッド」を「サブクラス」で“上書き”してしまうという仕組みです。
当然、その「スーパークラス」と「サブクラス」の「メソッド」は「シグネチャ」が同じになりますが、例として以下のプログラムを作ってみましょう。
class SuperClass {
public void show() {
System.out.println("スーパークラスです。");
}
}
class SubClass extends SuperClass {
public void show() {
System.out.println("サブクラスです。");
}
}
public class TestPolymorphism03 {
public static void main(String[] args) {
SubClass sub = new SubClass();
sub.show();
}
}
この例では、「スーパークラス」の“show”という「メソッド」を「サブクラス」で「オーバーライド」しています。
実際に実行してみると“サブクラスです。”と表示されて、“show”という「メソッド」は「サブクラス」のほうが動いていることが分かります。
つまり「スーパークラス」の「メソッド」を「オーバーライド」で上書きすることによって、その機能を自由に変更することができるわけです。
さらに同じ“show”という名前でありながら、「スーパークラス」と「サブクラス」では違う機能の「メソッド」を持つということで、「オーバーライド」によて「ポリモーフィズム」を実現しているということにもなります。
続けて、このプログラムを以下のように少し変えてみます。
class SuperClass {
public void show() {
System.out.println("スーパークラスです。");
}
}
class SubClass extends SuperClass {
public void show() {
System.out.println("サブクラスです。");
}
}
public class TestPolymorphism04 {
public static void main(String[] args) {
SuperClass sub1 = new SuperClass();
SuperClass sub2 = new SubClass();
sub1.show();
sub2.show();
}
}
ここで注意したいのは「サブクラス」を「インスタンス化」している行で、「サブクラス」の「インスタンス」なのにその参照変数が「スーパークラス」になっている点です。
SuperClass sub2 = new SubClass();
なぜこのようなことができるかというと、「オーバーライド」を利用した場合、プログラムの実行時に呼び出される「メソッド」が決定され、それは参照変数の型のほうではなく生成された「インスタンス」のほうになるからです。
ちなみにこれを「動的リンク」と呼び、逆にコンパイル時に呼び出される「メソッド」が決定されることを「静的リンク」と呼びます。
これを利用して、以下のようなプログラムを作ることもできます。
class SuperClass {
public void show() {
System.out.println("スーパークラスです。");
}
}
class SubClass01 extends SuperClass {
public void show() {
System.out.println("1つ目のサブクラスです。");
}
}
class SubClass02 extends SuperClass {
public void show() {
System.out.println("2つ目のサブクラスです。");
}
}
public class TestPolymorphism05 {
public static void main(String[] args) {
SuperClass[] sub = {new SubClass01(), new SubClass02()};
for (int i = 0; i < 2; i++) {
sub[i].show();
}
}
}
ただし気をつけなければならないのは、このように「スーパークラス」の型で「サブクラス」の「インスタンス」を生成した場合、「サブクラス」で新たに追加した「メソッド」は使えないということです。
つまり利用できるのは、「スーパークラス」に存在する「メソッド」だけということです。
「抽象クラス」について
それでは最後に、「抽象クラス」を使った「オーバーライド」についてふれておきましょう。
「抽象クラス」というのは、中身が定義されていない空の「メソッド」を持つ未完成の「クラス」です。
そのため、これを単独でそのまま「インスタンス化」しようとすると、コンパイル時にエラーとなります。
エラーにならないようにするためには、この「抽象クラス」を「継承」した「サブクラス」で、空の「メソッド」の中身を定義する必要があります。
例として、以下のプログラムを作ってみましょう。
abstract class SuperClass {
abstract void show();
}
class SubClass01 extends SuperClass {
public void show() {
System.out.println("1つ目のサブクラスです。");
}
}
class SubClass02 extends SuperClass {
public void show() {
System.out.println("2つ目のサブクラスです。");
}
}
public class TestPolymorphism06 {
public static void main(String[] args) {
SuperClass[] sub = {new SubClass01(), new SubClass02()};
for (int i = 0; i < 2; i++) {
sub[i].show();
}
}
}
ここで「スーパークラス」の“SuperClass”の中の「メソッド」“show”には中身がありませんが、つまりこの“SuperClass”が「抽象クラス」ということになります。
そして「抽象クラス」を定義する場合は、修飾子の“abstract”を付けるという点にも注目しましょう。
さらに中身の無い「メソッド」を「抽象メソッド」と呼びますが、こちらにも同様に修飾子の“abstract”を付けます。
「抽象メソッド」を1つでも持つ「クラス」は「抽象クラス」になりますので、必ずその定義には“abstract”を付ける必要があるので要注意です。
このように「抽象クラス」は「オーバーライド」を前提とした仕組みですが、これによって「サブクラス」で「オーバーライド」を忘れるという間違いを防ぐこともできます。
つまり「抽象クラス」を使わない「スーパークラス」を使っていた場合、ウッカリ「サブクラス」のほうで「オーバーライド」を忘れると、Javaの仮想マシンは特に警告も無く「スーパークラス」の「メソッド」を実行することになります。
そういった、エラーにはならないけれども意図した動作とは違う間違いは、気づいたり発見するのが難しいため、できるだけ避けるようにプログラムを作ることが重要です。
「抽象クラス」を使えば、このような間違いはコンパイル時にエラーとして発見されるので、その意味でも便利な仕組みと言えるでしょう。
まとめ
今回は「ポリモーフィズム」と、それを実現するための「オーバーロード」や「オーバーライド」、そして「抽象クラス」について勉強してみました。
概念としての「ポリモーフィズム」は難しく感じがちですが、実際にプログラムをそれに沿って作ってみると、意外に難しくないと分かるのではないでしょうか。
またここで登場した「オーバーロード」「オーバーライド」「抽象クラス」といった仕組みは、プログラム作成の際にとても便利に利用できるものなので、しっかりと理解しておきたいですね。
それでは、今回はここまでです。
《 【Javaの勉強】一覧 》