本章ではオブジェクト指向プログラミングを行うのに重要な機能の一つであるポリモフィズム(多態性)に関して学習します。
本章で学習する内容を動画としてまとめたものです。最初に一通り見終わった後で、学習に入るようにしてください。
ポリモフィズムは、「多態性」と呼ばれるものです。
「異なるデータ型の引数や異なる数の引数を、同じ名前のメソッドがそれぞれ異なる処理で対応する機能」という意味です。
利用されるクラスから生成された1つのオブジェクトのみ、または、同じような機能を持つ異なるクラスから生成された複数のオブジェクトで、同じ名前のメソッドの処理の多様性を高めるために用います。
これは、オブジェクト指向プログラミングにおける重要な技法です。
ポリモフィズムでは、多様なメソッドの定義をし、それを利用します。主に次の3通りの手法があります。
1つ目は、オーバーロード、2つ目は、オーバーライド、3つ目は、抽象クラスとインターフェースという手法です。
これから、これらの技術を学習していきましょう。
まず最初は、オーバーロードです。
オーバーロードとは、一つのクラスの中に、異なる引数の数または型を持つ同じ名前のメソッドを複数定義することです。
今まで一つのクラスの中に複数のメソッドを定義する場合には、それぞれ異なるメソッド名をつけて定義していましたが、一つのクラス内に同じ名前のメソッドを複数持つこともできます。
ただし、メソッド名は同じでも引数や戻り値が違っていることが条件になります。メソッドの名前と引数の組み合わせを「シグネチャー」と呼びます。
つまり、シグネチャーが同じメソッドは同じクラス内でオーバーロードできないということになります。
ここで、「よく使うクラス」の章で何度か使用したStringクラスのindexOfメソッドを見てみます。JavaDocを確認してみると、以下のように引数の異なるindexOfメソッドが複数存在することが分かります。
「よく使うクラス」の章で、文字列だけを引数で渡した場合にも、文字列と数字を引数で渡した場合にもindexOfメソッドを利用できたのは、オーバーロードを用いていたためです。
さて、それでは実際に、オーバーロードを記述してみましょう。以下がサンプルソースです。
MyClass1クラスの中に、引数の数、型の異なるmyTempというメソッドが3つ定義してあります。
public class MyClass1 {
//引数の無い myTemp メソッド
void myTemp() {
System.out.println(10);
}
//int 型の引数を持つ myTemp メソッド
void myTemp(int x) {
System.out.println(x);
}
// long 型の引数を持つ myTemp メソッド
void myTemp(long x) {
System.out.println(x);
}
}
呼び出し側では、引数の型を変更すれば、同じ型を持ったメソッドが呼ばれます。
public class MainClass1 {
public static void main(String[] args) {
MyClass1 m = new MyClass1();
// 引数無しの myTemp メソッドが呼ばれます
m.myTemp();
// int 型の引数を持つ myTemp メソッドが呼ばれます
m.myTemp(38);
// long 型の引数を持つ myTemp メソッドが呼ばれます
m.myTemp(2147483648L);
}
}
オーバーロードは、引数の数と型を同じにするとコンパイルエラーになります。
これは、メソッド名も引数の型も数も同じにすると、利用するクラスで、そのメソッドを呼び出す時にどちらを実行してよいか分からなくなるからです。
また、戻り値のみ違うメソッドは、シグネチャが同一とみなされるため、オーバーロードできませんので注意しましょう。
public class MyClass2 {
//引数の無い myTemp メソッド
void myTemp() {
}
//戻り値のみ違うメソッドはオーバーロードできないためエラー!
int myTemp() {
return 10;
}
}
コンストラクタをオーバーロードすることもできます。
複数の引数のパターンでコンストラクタを定義することにより、利用する側にとって、利用の自由度があがります。
以下がサンプルソースです。
public class MyClass3 {
// int型の変数xを定義
int x;
// 引数の無いコンストラクタ
MyClass3() {
// 変数xを10で初期化
x = 10;
}
// int型の引数を持つコンストラクタ
MyClass3(int i) {
// 変数xを引数で初期化
x = i;
}
}
スーパークラスのメソッドをオーバーロードすることもできます。以下がサンプルソースです。
public class MySuper1 {
// MySuper1クラスの引数の無いmyTempメソッド
void myTemp() {
}
}
public class MySub1 extends MySuper1 {
// MySub1クラスのint型の引数を持つmyTempメソッド
void myTemp(int x) {
}
}
オーバーロードを利用することにより、引数は違っても同じ機能を持った処理の場合、同じ名前のメソッドにした方がプログラムソースが分かりやすくなります。
また、コンストラクタのオーバーロードの場合、初期値の引渡し方を色々なパターンで定義することにより、利用するクラスにとって、利用しやすくなります。
次にオーバーライドについて学習します。
オーバーライドとは親クラスで定義されているインスタンスメソッドを、継承した子クラスで再定義することを言います。
つまり、親クラスから継承したメソッドの処理内容を子クラス側で書き換えるということです。その際、親クラスの元のメソッドは変更されません。
以下がサンプルソースです。親クラスのMySuper2に定義されたmyTempメソッドを、子クラスのMySub2でオーバーライドしています。
通常、親クラスのmyTempメソッドの処理とは異なる処理が子クラスでオーバーライドされたmyTempメソッドに記述されます。
public class MySuper2 {
// MySuper2クラスのmyTempメソッド
void myTemp() {
System.out.println(10);
}
}
public class MySub2 extends MySuper2 {
// オーバーライドされたmyTempメソッド
void myTemp() {
System.out.println(20);
}
}
オーバーライドが適用されるのは、private以外のインスタンスメソッドです。
privateは、定義されたクラスのみで参照可能なため、子クラスでは、オーバーライドできません。
また、親クラスで定義されているstatic修飾子が付加されたクラスメソッドとクラス変数を子クラスで再定義することは隠蔽と言われ、オーバーライドとは別に扱われます。
メソッドをオーバーライドするときに指定できるアクセス修飾子は、アクセス制限が親クラスと同じか、ゆるくなる様に指定する必要があります。
例えば継承される親クラスのメソッドが、publicの場合にはpublicのみ、protectedの場合にはpublicかprotectedのみがオーバーライドしたメソッドに指定できます。
オーバーライドには、上記のほか、以下のようなルールがあります。
オブジェクト指向では、クラスを設計するときに抽象的(汎用的)な親クラスから子クラスを定義することが自然な方法となります。
抽象クラスは、「共通の機能を持つクラスを分類し、その共通の機能の定義を記述し、子クラスの記述を簡略化すること」が目的です。
抽象クラスには、abstract修飾子をクラスの前に定義します。また、abstract修飾子を前につけたメソッドを抽象メソッドと呼びます。
抽象メソッドは、abstract修飾子をつけ、「名前と型だけのメソッド」で宣言します。この抽象メソッドを含むクラスが抽象クラスと呼ばれ、抽象メソッドを一つでも持つクラスは、必ずabstractでクラス宣言をしなければいけません。
以下がサンプルソースです。抽象メソッドは、通常のメソッドの定義から「{}」(中括弧)の部分を取り除き、「;」(セミコロン)で終了します。
public abstract class MyAbstractClass {
abstract void myTemp();
abstract void myTemp(int x);
}
抽象クラスは、メソッドが中途半端な状態で定義されているので、インスタンス化をすることはできません。
もし、「new MyAbstractClass()」と記述すれば、コンパイルエラーになります。また、抽象メソッドも直接利用することはできません。
抽象クラスは、継承専用のクラスとして利用されます。子クラスに継承することにより、抽象メソッドの処理の部分は、子クラスでオーバーライドして定義されます。
以下がサンプルソースです。上記の抽象クラスであるMyAbstractClassクラスを継承し、抽象メソッドをオーバーライドして処理内容を定義しています。
public class MyClass4 extends MyAbstractClass {
void myTemp() {
System.out.println("おはようございます");
}
void myTemp(int x) {
System.out.println("今日は、" + x + "日です");
}
}
public class MyClass5 extends MyAbstractClass {
void myTemp() {
System.out.println("こんにちは");
}
void myTemp(int x) {
System.out.println("今、" + x + "時です");
}
}
抽象クラスを継承したクラスで、抽象メソッドをオーバーライドしていないと、コンパイルエラーになります。
継承したクラスは、必ず抽象メソッドをオーバーライドしなければいけません。
それでは、上記の2つの子クラスを実行してみましょう。
public class MainClass3 {
public static void main(String[] args){
MyClass4 my4 = new MyClass4();
my4.myTemp();
my4.myTemp(20);
MyClass5 my5 = new MyClass5();
my5.myTemp();
my5.myTemp(20);
}
}
このように、同じような処理を持つMyClass4クラスとMyClass5クラスを定義する場合、共通的な処理の部分を抽象クラスで定義することにより、子クラスの記述の簡略化や仕様の統一を行うことができます。
最後は、インターフェースです。
インターフェースは、抽象クラスと同様の機能を持ち、抽象クラスを完全に抽象化しています。
クラスの定義に似ていますが、「class」ではなく、「interface」で宣言します。
インターフェースのフィールドは、クラスと同様に定義できますが、「public static final」修飾子が自動的に付加されるため、必ず定数となります。
また、メソッドは、抽象メソッドのみ定義でき、abstract修飾子は省略できますが、省略しても、「public abstract」修飾子が自動的に付加されます。
以下がサンプルソースです。
public interface MyInterface1 {
// フィールド(修飾子は付けなくても定数になります)
// 自動的に「public static final」修飾子が付加されるため、以下の記述でも、「public static final int a = 10;」と同義となります。
int a = 10;
// メソッド(abstract が無くても抽象メソッドになります)
// 自動的に「public abstract」修飾子が付加されるため、以下の記述でも、「public abstract void myTemp();」と同義となります。
void myTemp();
}
それでは、インターフェースの利用方法を学習しましょう。
インターフェースは、抽象クラスと同様にそのまま直接は利用できません。
インターフェースに定義してあるフィールドやメソッドを他のクラスに引き継いでもらう必要があります。
インターフェースを元にクラスを定義することを「実装する」と言います。インターフェースを実装するクラスは、「implements」を使って、実装します。
以下がサンプルソースです。MyInterface1インターフェースをimplementsして、MyClassImplを定義しています。
10の値を持つint型の定数aは、そのまま引き継がれます。また、抽象メソッドのmyTempは、実装する必要があります。
抽象メソッドを実装するときにpublicを付加する必要がありますので、注意しましょう。
public class MyClassImpl implements MyInterface1 {
// MyInterface1で宣言されているメソッドの再定義
public void myTemp() {
System.out.println(a); // 定数aの値をコンソールに出力
}
}
最後にこれを利用するクラスを作成します。
public class MainClass4 {
public static void main(String[] args) {
MyClassImpl myImpl = new MyClassImpl();
myImpl.myTemp();
}
}
インターフェースは、継承と違って複数実装することができます。
クラスは多重継承ができませんが、インターフェースであれば、implementsするクラスでメソッドの処理を記述するため、何を引き継いでいるのか分からなくならないので、複数実装を可能にしています。
public class SubClass implements MyInterface1, MyInterface2,・・・ {
// MyInterface1とMyInterface2と・・・で宣言されているメソッドの再定義
}
例えば二つのインターフェースを実装したクラスは以下のようになります。
public interface MyInterface2 {
// フィールド(修飾子は付けなくても定数になります)
int b = 30;
// メソッド(abstract が無くても抽象メソッドになります)
void myTemp2();
}
public class MyClassDoubleImpl implements MyInterface1, MyInterface2 {
// MyInterface1で宣言されているメソッドの再定義
public void myTemp() {
// 定数aの値をコンソールに出力
System.out.println(a);
}
// MyInterface2で宣言されているメソッドの再定義
public void myTemp2() {
// 定数bの値をコンソールに出力
System.out.println(b);
}
}
多数のインターフェースを実装した場合の概念図は以下のようになります。
また、クラスを継承し、インターフェースを実装したクラスを作成する事が可能です。
public class SubClass extends MyExtends implements MyInterface {
// MyInterfaceで宣言されているメソッドの再定義
}
インターフェースを実装しているクラスは、そのインターフェースの型を持った参照変数にインスタンスを代入できます。
以下がサンプルソースです。MyInterface3インターフェースをimplementsしたMyClassImpl2クラスを利用するときに、MyInterface3インターフェース型の参照変数mに、MyClassImpl2クラスのインスタンスを代入しています。
public interface MyInterface3 {
void myTemp();
}
public class MyClassImpl2 implements MyInterface3 {
public void myTemp() {
System.out.println("MyInterface3のmyTempメソッド実装");
}
}
public class MainClass5 {
public static void main(String[] args) {
MyInterface3 m = new MyClassImpl2();
m.myTemp();
}
}
このように、同じインターフェースを持ったインスタンスの場合、同じインターフェイス型の参照変数に代入できます。
つまり、クラスが違っていても同じコードでメソッドが呼び出せ、同じインターフェースをimplementsした他クラスに変更した場合、インスタンスの部分(newの後のクラス名)を修正するのみで済みます。
インターフェースはインターフェースを継承できます。以下がサンプルソースです。
public interface MyInterface4 {
void myTemp();
}
public interface MyInterface5 extends MyInterface4 {
}
インターフェースの学習は、以上です。
インターフェースは、実際に活用するには少し難しい機能です。
間違った使い方をすると分かりにくいソースになるため、基本的な考え方をしっかり理解してから利用するようにしましょう。
それでは、ポリモフィズムの各手法の練習問題です。「インヘリタンス」の章で作成したpetpacパッケージとPetクラス、それとCatクラスとDogクラスを利用していきます。
petpacパッケージにPetInterfaceインターフェースを以下の内容で作成してください。
パッケージ名 | petpac |
---|---|
インターフェース名 | PetInterface |
メソッド | void eat() |
void play() |
package petpac;
public interface PetInterface {
// eat抽象メソッド
void eat();
// play抽象メソッド
void play();
}
インターフェースの定義は、「public interface インターフェース名」で始まります。
インターフェースに定義するメソッドは、抽象メソッドですので、処理は記述しません。
戻り値の型とメソッド名、引数のみを記述します。
抽象メソッドは、通常「public abstract void eat();」というように、最初に「public
abstract」を付けますが、インターフェースで定義するメソッドは全て抽象メソッドですので、「public abstract」を省略することができます。
Petクラスに問題1のPetInterfaceインターフェースをimplementsして、各メソッドを以下の内容で実装してください。
実装後、ImplPetShopクラスを作成し、実行結果のように動作するようにしましょう。
メソッド | void eat() | 「ごはんを食べました。」を画面に出力する |
---|---|---|
void play() | 「遊びました。」を画面に出力する |
package shop;
import catpac.Cat;
import dogpac.Dog;
import petpac.Pet;
public class ImplPetShop {
public static void main(String[] args) {
Pet myPet = new Pet();
ImplPetShop.buyPet(myPet);
System.out.println("---");
Cat myCat = new Cat();
ImplPetShop.buyPet(myCat);
myCat = new Cat("アメリカンショートヘアー","オス", 0);
ImplPetShop.buyPet(myCat);
System.out.println("---");
Dog myDog = new Dog();
ImplPetShop.buyPet(myDog);
myDog = new Dog("ゴールデンリトリバー","オス", 0);
ImplPetShop.buyPet(myDog);
}
static void buyPet(Pet tPet) {
System.out.println(tPet.getSyurui()
+ "の"
+ tPet.getSeibetsu()
+ "、"
+ tPet.getToshi() + "歳を買いました。");
tPet.eat();
tPet.play();
}
}
なぞの生物の性別不明、0歳を買いました。
ごはんを食べました。
遊びました。
---
スコティッシュフォールドのメス、0歳を買いました。
ごはんを食べました。
遊びました。
アメリカンショートヘアーのオス、0歳を買いました。
ごはんを食べました。
遊びました。
---
トイプードルのメス、0歳を買いました。
ごはんを食べました。
遊びました。
ゴールデンリトリバーのオス、0歳を買いました。
ごはんを食べました。
遊びました。
※この問題の解答は掲載しておりません。Tech Fun ITスクールのJava研修では、講師が丁寧に解説しています。
CatクラスとDogクラスに、継承されているPetクラスのeatメソッドとplayメソッドを以下の内容でオーバーライドしてください。
問題2で作成したImplPetShopクラスが実行結果のように動作するようにしましょう。
クラス | Cat | |
---|---|---|
メソッド | void eat() | 「大好きな魚を食べました。」を画面に出力する |
void play() | 「ねずみのおもちゃで遊びました。」を画面に出力する |
クラス | Dog | |
---|---|---|
メソッド | void eat() | 「大好きな肉を食べました。」を画面に出力する |
void play() | 「公園で走って遊びました。」を画面に出力する |
なぞの生物の性別不明、0歳を買いました。
ごはんを食べました。
遊びました。
---
スコティッシュフォールドのメス、0歳を買いました。
大好きな魚を食べました。
ねずみのおもちゃで遊びました。
アメリカンショートヘアーのオス、0歳を買いました。
大好きな魚を食べました。
ねずみのおもちゃで遊びました。
---
トイプードルのメス、0歳を買いました。
大好きな肉を食べました。
公園で走って遊びました。
ゴールデンリトリバーのオス、0歳を買いました。
大好きな肉を食べました。
公園で走って遊びました。
package catpac;
import petpac.Pet;
public class Cat extends Pet {
public Cat() {
super("スコティッシュフォールド", "メス", 0);
}
public Cat(String tSyurui, String tSeibetsu, int tToshi) {
super(tSyurui, tSeibetsu, tToshi);
}
// eatメソッドをオーバーライド
public void eat() {
System.out.println("大好きな魚を食べました。");
}
// playメソッドをオーバーライド
public void play() {
System.out.println("ねずみのおもちゃで遊びました。");
}
}
オーバーライドは、親クラスが持つメソッドを書きかえることです。
親クラスのPetクラスで定義されているeatメソッドとplayメソッドの処理をCatクラスの内容に変更しています。
package dogpac;
import petpac.Pet;
public class Dog extends Pet {
public Dog() {
super("トイプードル", "メス", 0);
}
public Dog(String tSyurui, String tSeibetsu, int tToshi) {
super(tSyurui, tSeibetsu, tToshi);
}
// eatメソッドをオーバーライド
public void eat() {
System.out.println("大好きな肉を食べました。");
}
// playメソッドをオーバーライド
public void play() {
System.out.println("公園で走って遊びました。");
}
}
Catクラスと同様に、eatメソッドとplayメソッドをオーバーライドしています。
「PolymorphismMondai4」は、Catクラスをインスタンス化し、getNameメソッドを呼び出すプログラムです。
以下の実行結果のように動作するように、問題3で使用したCatクラスの中にgetNameメソッドを作成して下さい。
public class PolymorphismMondai4 {
public static void main(String[] args) {
Cat cat = new Cat();
System.out.println("吾輩は猫である。" + cat.getName());
System.out.println("吾輩は猫である。" + cat.getName("ミケ"));
}
}
吾輩は猫である。名前はまだない。
吾輩は猫である。名前はミケという。
※この問題の解答は掲載しておりません。Tech Fun ITスクールのJava研修では、講師が丁寧に解説しています。
抽象クラス「AbstractShop」を作成しましょう。
その後、抽象クラス「AbstractShop」を実装した、以下の3つのクラスを実装してください。
クラス名 | 説明 |
---|---|
DepartmentStore | コンソールに「クレジットカードと現金決済。」と出力する。 |
ConvenienceStore | コンソールに「電子マネー決済と現金決済。」と出力する。 |
Store | コンソールに「現金決済のみ。」と出力する。 |
public abstract class AbstractShop {
// 支払方法をコンソール出力するメソッド。
public abstract void payment();
}
※この問題の解答は掲載しておりません。Tech Fun ITスクールのJava研修では、講師が丁寧に解説しています。
用語の意味などに不明点がある場合は、教材をもう一度読み直すなどして復習しておきましょう。
ポリモフィズムについての説明は、以上です。