Javaによるオブジェクト指向講座 第3回
< 第2回
オブジェクト指向講座の最後です。今回は継承とポリモーフィズムを解説します。
第3回 継承
クラスの継承
本クラスがすでにあったとして、さらに日記クラスを作ろうと考えます。本クラスは各ページに書いてある文字列があるだけですが、日記クラスではさらに各ページごとの日付が記録できるようになっています。日記クラスは本クラスにあるすべての機能を持っているのですが、本クラスの中身をそのままコピペして作るのはタイヘンですよね。そこで使えるのが継承です。本クラスのすべての機能を引き継いで、新たな日記クラスを作ることができます。
まずは本クラスだけ作っておきましょう。
下ではpublicとprivateの他にprotectedというのが出てますが、後述するのでとりあえず一旦はprivateみたいなものとして読んでください。
// 【Book.java】
class Book{
// ページ数
protected int pageNum;
// 各ページのテキスト内容
protected String[] text;
public Book(int pageNum){
this.pageNum = pageNum;
this.text = new String[pageNum];
for(int i = 0; i < pageNum; i++){
this.text[i] = "";
}
}
// 本を読む
public void read(int pageId){
System.out.println("【" + pageId + "】ページ:" + this.text[pageId]);
}
// 本に書き込む
public void write(int pageId, String text){
this.text[pageId] = text;
}
// 本のページ数を取得
public int getPageNum(){
return this.pageNum;
}
}
// 【Main.java】
class Main{
public static void main(String args[]){
Book book = new Book(10);
book.write(0, "この本は0ページから始まるのだ");
book.write(1, "これが1ページで");
book.write(2, "これが2ページである");
book.write(3, "続きはまだ書いていない");
book.write(5, "あっ飛ばして書いてしまったわ");
for(int i = 0; i < book.getPageNum(); i++){
book.read(i);
}
}
}
10ページの本を作り、各ページに文字列を記載しています。配列が0番から始まるので、ここでは簡単化するため0ページから始まる本としましょう^^;
それでは、この本クラスを継承して、各ページの日付を記録できるようにした日記クラスを作ります。
// 【Diary.java】
class Diary extends Book{
// 作成年月日
protected int[] year;
protected int[] month;
protected int[] day;
public Diary(int pageNum){
super(pageNum);
year = new int[pageNum];
month = new int[pageNum];
day = new int[pageNum];
}
}
クラスの定義を行うときに「extends Book」とすれば、Bookクラスを継承するという意味になります。継承すると、すべての変数・メソッドを引き継いで使えるようになります。クラスを拡張できるわけですね。
継承される元のBookクラスみたいなもののことをスーパークラス(親クラス)といい、継承した先のDiaryクラスみたいなもののことをサブクラス(子クラス)といいます。
ただし、スーパークラスでprivate宣言されているものはサブクラスも使うことができません。アクセス権限を与えるアクセス修飾子は3種類あります。
public:どこからでもアクセス可能
protected:自分のクラスとそのサブクラスだけアクセス可能
private:自分のクラスだけアクセス可能
本当はもう一つありますけど、ここでは割愛します。
Bookの変数をprotectedとしていたのは、サブクラスであるDiaryは普通に使えますが、外部のクラスであるMainは使うことができない、という意味で使っていたのでした。
もう一つ、Diaryのコンストラクタに次のような命令があります。
super(pageNum);
これは、スーパークラスのコンストラクタを引数pageNumとして実行するというものです。
Bookクラスのコンストラクタでページ数のセットや各ページの文字列の初期化をしていましたが、それをDiaryクラスのコンストラクタにコピペするのはめんどくさいので、そのように書きます。
これでDiaryクラスの基本は作れましたので、実際に各ページに年月日を書き込めるようにしましょう。年月日の変数それぞれの読み書きメソッド(アクセッサ)を作れば実現できますが、なんだかめんどくさいですよね。writeYear, writeMonth, writeDay, readYear, readMonth, readDayと6種類もあります。Mainの方も1ページ書くごとに書き込みメソッドを全部呼ぶ必要があるので面倒です。
そこで、Bookクラスにもともとあるwriteとreadを上書きして、年月日を書き込むことを考えます。Bookクラスをいじるのではなく、Diaryクラスに追記する形で上書きをします。
// 【Diary.java】
class Diary extends Book{
// 作成年月日
protected int[] year;
protected int[] month;
protected int[] day;
public Diary(int pageNum){
super(pageNum);
year = new int[pageNum];
month = new int[pageNum];
day = new int[pageNum];
}
// 日記を読む
public void read(int pageId){
super.read(pageId);
System.out.println("作成年月日:" + year[pageId] + "年" + month[pageId] + "月" + day[pageId] + "日");
}
// 日記に書き込む
public void write(int pageId, String text, int year, int month, int day){
super.write(pageId, text);
this.year[pageId] = year;
this.month[pageId] = month;
this.day[pageId] = day;
}
}
Diaryクラスにもreadとwriteのメソッドを用意しました。superはそれぞれスーパークラスを意味しています。
このように、スーパークラスにあるメソッドをサブクラスで再定義すると、サブクラスのオブジェクトからは再定義後のメソッドが優先して呼ばれます。readメソッドのようにメソッドを完全に再定義してしまうことをオーバーライドといいます。
writeメソッドの方は引数がBookクラスのものと違うので、同じ名前でも違うメソッドとして扱われます。そのため、実はwriteメソッドはBookクラスのものが残っており、2つのメソッドがあります。このように、同じ名前でも引数が違えば別のメソッドとして扱えて、そのように新しいメソッドを定義することをオーバーロードといいます。
名前が似ていますが、割と別物です。注意してください。
// 【Main.java】
class Main{
public static void main(String args[]){
Diary diary = new Diary(10);
diary.write(0, "この本は0ページから始まるのだ", 2017, 8, 10);
diary.write(1, "これが1ページで", 2017, 8, 11);
diary.write(2, "これが2ページである", 2017, 8, 12);
diary.write(3, "続きはまだ書いていない", 2017, 8, 13);
diary.write(5, "あっ飛ばして書いてしまったわ", 2017, 8, 15);
for(int i = 0; i < diary.getPageNum(); i++){
diary.read(i);
}
}
}
Mainで日記を書けるように書き換えました。Bookクラスをいじることなく、Bookの機能を引き継いだDiaryが作れましたね。
static
今までクラスはオブジェクトの設計図と紹介しており、クラスで宣言されている変数やメソッドはオブジェクト化して初めて生まれるものと紹介しました。
次のように時計クラスを作って時刻を表す変数timeを持たせておくと、時計オブジェクトそれぞれが時刻を持つようになりますね。
// 【Watch.java】
class Watch{
private String time;
public Watch(String time){
this.time = time;
}
public void printTime(){
System.out.println("時計は" + this.time + "を指している");
}
}
// 【Main.java】
class Main{
public static void main(String args[]){
Watch w1 = new Watch("11:15");
Watch w2 = new Watch("12:20");
Watch w3 = new Watch("13:45");
w1.printTime();
w2.printTime();
w3.printTime();
}
}
それぞれの時計が別々の時刻を表しています。
しかし、時計なので何個時計があろうと時刻は同じになって欲しいという気持ちもあります。時刻はオブジェクト固有ではなくクラス固有のものと考えて、クラス固有の変数として時刻を決めることにします。
次のように書き換えます。
// 【Watch.java】
class Watch{
private static String time;
public static void setTime(String time){
Watch.time = time;
}
public void printTime(){
System.out.println("時計は" + Watch.time + "を指している");
}
}
// 【Main.java】
class Main{
public static void main(String args[]){
Watch.setTime("11:15");
Watch w1 = new Watch();
Watch w2 = new Watch();
Watch w3 = new Watch();
w1.printTime();
w2.printTime();
w3.printTime();
}
}
変数timeの頭にstaticを付けました。これにより変数timeはWatchオブジェクトそれぞれが持つ変数なのではなく、Watchオブジェクトなら共通で利用可能なクラス固有の単一の変数になります。このようなクラス固有の変数をクラス変数といい、メソッドの場合はクラスメソッドといいます。ここではsetTimeメソッドをクラスメソッドにしていますね。
クラス変数をいじるときはthisではなくクラス名を使います。this.timeではなく、Watch.timeです。thisは自分自身のオブジェクトのことで、クラス変数はオブジェクトではないためです。
こうすれば、すべての時計で同じ時刻を表すようになります。
クラス固有なので、継承してもオーバーライドされないのですね。以下ではWatchを継承したAppleWatchを作ってそっちでも変数timeを宣言していますが、どちらもstaticなので、それぞれのクラスが指す時刻は異なります。
// 【Watch.java】
class Watch{
private static String time;
public static void setTime(String time){
Watch.time = time;
}
public void printTime(){
System.out.println("時計は" + Watch.time + "を指している");
}
}
// 【AppleWatch.java】
class AppleWatch extends Watch{
private static String time;
public static void setTime(String time){
AppleWatch.time = time;
}
public void printTime(){
System.out.println("りんご時計は" + AppleWatch.time + "を指している");
}
}
// 【Main.java】
class Main{
public static void main(String args[]){
Watch.setTime("11:15");
AppleWatch.setTime("12:30");
Watch w1 = new Watch();
Watch w2 = new AppleWatch();
w1.printTime();
w2.printTime();
}
}
抽象クラス
継承は終わったので、ここからはポリモーフィズムの話に入ります。ポリモーフィズムまで理解すれば、オブジェクト指向はもう掌の中にあります。
食べ物クラスを継承して、うどんクラスとオムライスクラスを作りましょう。食べ物なので食べられるわけですが、その食べ方がうどんとオムライスで異なります。すなわち、食べ物クラスの段階では「食べる」という行為が未定義なわけです。
食べ物が食べれるのは確実なのですが、どうやって食べればいいのか分かりません。継承したうどんとかオムライスの方で定義してやることになります。このとき、食べ物クラスの食べるメソッドは抽象メソッドとして定義します。
// 【Food.java】
abstract class Food{
abstract public void eat();
}
// 【Udon.java】
class Udon extends Food{
public void eat(){
System.out.println("箸でうどんを食べた");
}
}
// 【Omurice.java】
class Omurice extends Food{
public void eat(){
System.out.println("スプーンでオムライスを食べた");
}
}
// 【Main.java】
class Main{
public static void main(String args[]){
Udon udon = new Udon();
Omurice omurice = new Omurice();
udon.eat();
omurice.eat();
}
}
Foodクラスを継承して、UdonクラスとOmuriceクラスを作りました。
Foodのeatメソッドは未定義のままにしておき、継承先で改めて定義して欲しいので、eatメソッドの先頭にabstractというキーワードを付けています。これは抽象メソッドにするという意味で、Foodはeatというメソッドは持っているものの、内容は未定義という意味になります。
一つでも抽象メソッドを含んでいるクラスは抽象クラスと呼ばれ、classの先頭にもabstractキーワードが付くことになります。
抽象クラスには「抽象的な概念」という意味があり、このままでは実体化できないクラスとなります。ですから、Mainの方で「Food food = new Food();」などと書くとエラーになります。抽象クラスはオブジェクト化することが出来ません。これを継承し、抽象メソッドをオーバーライドしたクラスでないとオブジェクトにならないのです。
このままだと食べ物の概念に基づいてクラスを作っただけであり、こんなのいつ使うのと思われるでしょう。それは次に説明するポリモーフィズムを活用するために使います。
ポリモーフィズム
食べ物を食べようとしている人は、単に「食べろ」と言われれば、食べ物によって無意識に食べ方を変えることができます。このように、実体が何なのかによって処理を自動で切り替える仕組みのことをポリモーフィズム(多態性)といいます。
さっきのFood、Udon、Omuriceクラスをそのまま利用して、Mainを次のように書き換えます。
// 【Main.java】
class Main{
public static void main(String args[]){
Food udon = new Udon();
Food omurice = new Omurice();
udon.eat();
omurice.eat();
}
}
変わったのは、udonとomuriceの型だけです。
Food型の変数にUdonとOmuriceのオブジェクトを代入しました。そうすると、udonとomuriceは中身はそれぞれ異なるけれど、さながらFoodクラスのオブジェクトかのように扱うことが可能になります。そこでeatメソッドを呼び出してみると、中身はそれぞれのクラスでオーバーライドしてますから、それぞれのオブジェクトに合った食べ方で食べることになります。これがポリモーフィズムです。
ポリモーフィズムを利用すると、配列を使ってこんなようなこともできます。
// 【Main.java】
class Main{
public static void main(String args[]){
Food food[] = {new Udon(), new Omurice(), new Udon(), new Udon(), new Omurice()};
for(int i = 0; i < food.length; i++){
food[i].eat();
}
}
}
food.lengthとは、配列foodの要素数を表します。ここでは5ですね。単に食べ物のeatメソッドを呼んでいるだけなのに、食べ物によって食べ方を変えて実行されることになります。こうすれば、Mainの方で条件分岐しなくて良くなり、何より直観的に扱えるようになるので便利です。コードもすっきり整理されますし、オブジェクト主体でやり取りしている感が増します。
なかなかどういう場面でポリモーフィズムを活用しようか、慣れないうちは難しいと思いますが、強力な仕組みですのでぜひぜひ活用していきましょう。
その他
Javaには他にも色々な機能があります。
例えばJavaでは二つ以上のクラスを継承することが禁止されています。(C++は出来ます)スーパークラスが2つもあるとそれぞれどう扱えばいいのか複雑になるためです。
しかし完全に禁止されてはかえって不便なこともあり、代わりにインターフェースという機能制限されたクラスが用意されています。これは、メソッドの中身が全部空の状態で定義されるクラスであり、継承(インターフェースの場合は実装といいます)してメソッドを再定義して作る必要があるものです。
なんでそんなものを用意するんだという話になりますが、詳細は割愛します。インターフェースなら何個でも実装して良くなり、Javaではよく活用されています。文字通りクラス間のインターフェースになる部分を作るためにあるんですね。
他にも、ここで宣言したものはもう書き換えないよという宣言を行うキーワードにfinalがあります。例えば、public final int a = 5;と変数を宣言すると、a=5と宣言されたのを最後に書き換え不可能になります。この後にa = 3;とかやるとエラーになります。変数にfinalを付ければ定数と見なせるようになるわけです。
finalはメソッドにも付けることができ、そうするとそのクラスを継承した先でオーバーライドできなくなります。これ以上メソッドの内容を書き換えることはできない、これで定義は完了だよ、という意味です。一種のアクセス制限ですが、わざと制限を掛けていくことによって簡単化し、バグりづらくさせるのですね。
以上でオブジェクト指向講座はおわりです。割といろいろ省略していきましたが、とりあえず最低限これらを知っていれば、オブジェクト指向プログラミングは十分できると思います。
> おまけ