feature image

2016年5月20日 | ブログ記事

クラスと継承とコールバック

継承

例えばこういうコードがあったとしましょう。


class Enemy {
int x, y;
int hp;

void attack() {
//攻撃する
}
void useMagic() {
//魔法を使う
}
}

class Player {
int x, y;
int hp;

int itemNum;

void attack() {
//攻撃する
}
void useItem() {
//道具を使う
}
}

...

Player player = new Player();

player.attack();

player.hp = 100;

Enemy enemy = new Enemy();

enemy.useMagic();

enemy.hp = 50;

敵を表すクラスとプレイヤーを表すクラスがあります。

よく見てみると、これら2つには共通する事項が多くみられます。ここからさらにEnemyA, EnemyB, ... EnemyZまで作るとしたらどうでしょう。同じことを何度も書くなんて意味がないですよね。そこで、クラスの継承という機能を使って意味が同じになるように書いてみます。



class Character {
int x, y;
int hp;

void attack() {
//攻撃する
}
}

class Enemy extends Character {

void useMagic() {
//魔法を使う
}
}

class Player extends Character {

int itemNum;

void useItem() {
//道具を使う
}
}

どうでしょうか。クラスの数は増えましたが、共通する事項はすべてCharacterクラスに入ってしまっていて、EnemyやPlayerクラスではそこに含まれない部分のみを記述しています。このように表記することで、EnemyやPlayerにはあたかもint x, y;やint hp;やvoid attack() {...}が書かれているかのように使うことができます。つまり、共通する部分を一回書くだけで何度もそこを使いまわせるというわけです。

このように、あるクラスのメンバ(変数や関数)を別のクラスに持ってくることを継承といいます。継承をする際には、



class 継承するクラス extends 継承されるクラス {

...

}

という風に記述していきます。継承されるクラスを親クラスと呼び、継承するクラスを子クラスと呼びます。

また、継承には特殊な作用があります。普通、Enemyクラスを使うときにはEnemy型の、Playerクラスを使うときにはPlayer型の変数を使い、



Enemy enemy = new Enemy();

Player player = new Player();

といった書き方をします。しかし、先ほどの例のようにEnemy,PlayerクラスがCharacterクラスを継承している場合には、**Character型の変数でEnemyやPlayerを扱うことができます。**つまり、



Character enemy = new Enemy();

Character player = new Player();

といったことができます。

一般に、変数の型を親クラスの型にしておくと、中身に子クラスを入れることができます。

ただし、このときにenemyやplayerといった変数を用いて使えるメンバーはx,y,hp,attack()などのCharacterに書き込まれている(共通部分)だけで、その他のuseItem()やuseMagic()といったものは使えません。



Character enemy = new Enemy();

Character player = new Player();

enemy.attack(); //OK

enemy.hp = 30; //OK

// enemy.useMagic(); //NG

player.x = player.y = 4; //OK

player.attack(); //OK

// player.useItem(); //NG

// player.itemNum = 2; //NG

コンパイラはその変数に何が入っているかまでは確かめられません。その変数の中身が何かを推定する情報はその変数の型のみなので、Character型の変数から呼び出せるメンバもCharacterの範疇にとどまるというわけです。

変数の型と中身の型が一致しなくてもよいという一見奇妙な機能は、実はとても有用なものです。

例えばあなたがシューティングゲームを作っていたとしましょう。ゲームには様々な敵キャラ、弾、アイテムが出現します。

するとあなたはこれらを毎フレーム動かし、描画するために



class EnemyA {

int hp;

void draw() {

}

}

class EnemyB {

int hp;

void draw() {

}

}

class EnemyC {

int hp;

void draw() {

}

}

EnemyA[] enemyAList;

EnemyB[] enemyBList;

EnemyC[] enemyCList;

...

for (int i = 0; i < enemyAList.length; i++) enemyAList[i].draw();

for (int i = 0; i < enemyBList.length; i++) enemyBList[i].draw();

for (int i = 0; i < enemyCList.length; i++) enemyCList[i].draw();

...

といった具合のコードを書くことになります。当然これは敵の種類が増えれば増えるほど変数の種類も増え、for文の個数も増えていきます。

しかし、もしあなたが継承を利用して



class Enemy {

int hp;

void draw() {

}

}

class EnemyA extends Enemy {

}

class EnemyB extends Enemy {

}

class EnemyC extends Enemy {

}

と書いていたらどうでしょう?先ほど書いた通り、変数の型が親クラスのものであれば、中身には子クラスを入れることができます。

つまり、この続きとしては



Enemy[] enemy;

for (int i = 0; i < enemy.length; i++) enemy[i].draw();

で十分ということになります。敵の種類がいくつ増えようとも、それぞれの敵のクラスがEnemyクラスを継承している限りは上の2行ですべてを賄いきれるというわけです。めでたしめでたし。

・・・ってあれ?何かおかしくないですか?先ほどの説明によると、継承とは親クラスのメンバ、つまり変数や関数を子クラス側にコピーするというものでした。ということはEnemyA,B,CのdrawメソッドはEnemyのdrawメソッドとそっくりそのまま同じ動作をするはずです。ということは敵クラスの種類をいくら増やしてもその描画のされ方は同じということですから、見た目は全部同じ・・・ってことですよね?

そんな横暴は許されるはずがありません。そもそも私たちが共通部分として利用したいのは、「drawという関数全部」ではなく「drawという名前の関数がある」というところなのですから。つまり、EnemyA,B,C,...はdrawという関数は持っている。しかし中身はそれぞれ違う。こういう状況にしたいわけです。そこで新たな機能の登場です。



class Enemy {

int hp;

void draw() {

}

}

class EnemyA extends Enemy {

@Override

void draw() {

}

}

class EnemyB extends Enemy {

@Override
void draw() {

}
}

class EnemyC extends Enemy {

@Override

void draw() {

}

}

...

はい。めっちゃ長くなりましたね。前のプログラムと違う点は、各敵のクラスにdrawが書かれていることです。このように、親クラスにすでに出てきているメソッドをもう一度子クラスで宣言してやることを、関数のオーバーライドと言います。同じ名前の関数が親と子のどちらにもある場合、子クラスのものが優先して使われます。こうしてやることで、



for (int i = 0; i < enemy.length; i++) enemy[i].draw();

の部分で、enemy[i]の中身がEnemyAなのかEnemyBなのかEnemyCなのかによって、どのdrawが呼ばれるかが変わってくるということになります。

ちなみに前についている@Overrideはアノテーションと呼ばれるもので、ぶっちゃけ意味ないです。コメントだと思ってください。

長々と書いて来ましたが、まとめるとこういうことです。

class B extends A {...}

で、Aを継承したBというクラスができる。
・親クラスのメソッドと同名のメソッドを子クラスで定義してやると、子クラス側のメソッドが優先して呼び出される(オーバーライド)。

・継承のメリットは2つ。

①いろいろなクラスで共通するメンバをひとまとめにできる

②親クラス型の変数で複数の種類のクラスを管理できるので、処理が楽に書ける

抽象クラス

さて上の例ですが、まずEnemyクラスでdrawを定義し、EnemyA,B,Cでそれをオーバーライドする...という形になっています。

そうなると、「あれ?Enemyのdrawってどうせオーバーライドされたら消えちゃうんだし要らなくね?」ってなります。先ほども書いたように、Enemyでdrawを書いてからオーバーライドする、という形にしているのはEnemyA,B,CをすべてEnemy型の変数で扱うためでした。そしてdrawがEnemyに書いていないと、Enemy型の変数にEnemyA,B,Cを入れてもdrawを呼び出せなくなってしまうのでした。つまり、ここで必要なのは「drawという名前の関数がEnemyにある」という事実だけなのです。中身はどうでもいいのです。

ですから、中身は書かないことにしましょう。




abstract class Enemy {

int hp;

abstract void draw();

}

関数の頭にabstractとつけると、関数の中身を書かなくてもよくなります。つまり、「Enemyはdrawという関数を持ってはいますが、中身をどうするかは子クラスに任せます」という意味になります。このように、中身のない関数のことを抽象関数といいます。抽象関数を持ったクラスは抽象クラスと呼ばれ、頭にabstractをつけなければなりません。

また、抽象クラスはnewできません。


class Enemy {

int hp;

void draw();

}

Enemy enemy = new Enemy(); //OK



abstract class Enemy {

int hp;

abstract void draw();

}

Enemy enemy = new Enemy(); //NG

そりゃそうです。Enemyはdrawの中身がないのですから、未完成なクラスなわけです。したがって、抽象クラスはそれを継承した子クラスがいて初めてまともに動きます。


abstract class Enemy {

int hp;

abstract void draw();

}



class EnemyA extends Enemy {

@Override

void draw() {

//Aを描画

}

}

Enemy enemy = new EnemyA(); //OK

enemy.draw(); //OK

まとめると、こういうことです。

abstract class クラス名

で抽象クラスを定義できる。

abstract 関数宣言;

で抽象関数を宣言できる。抽象関数には中身は書けず、オーバーライドされたときに中身が決定する。
・抽象関数を1つでも持っているクラスは抽象クラスにしなければならない。

・抽象クラスはnewできない。

インターフェース

抽象クラスに似たものとしてインターフェースというものがあります。


inteface Enemy {

void draw();

}

class EnemyA implements Enemy {

@Override

void draw() {

//Aを描画

}

}

抽象クラスは

abstract class クラス名

で始まっていたのに対し。インターフェースは

interface インターフェース名

と書きます。また、抽象クラスを継承する際には

class 子クラス extends 親クラス

と書いていたのに対し、インターフェースを継承する際には

class 子クラス implements 親クラス

と書きます。さらに、抽象クラスで抽象関数を書く際には

abstract 関数宣言;

と書いていましたが、インターフェースでは

関数宣言;

といったようにabstractが要りません

不自然に思うかもしれませんが、これには理由があります。実は、インターフェースに書いた関数は何も書かなくてもすべて抽象関数と解釈されてしまいます。逆に言えば、インターフェースには中身のある関数が書けません。ここがインターフェースと抽象クラスの最大の違いです。

また、インターフェースには変数が書けません

したがって、インターフェース内には基本的に抽象関数しかありません。

まとめると、こういうことです。

interface インターフェース名

でインターフェースを定義できる。

class 子クラス implements インターフェース名

でインターフェースを継承できる。

・インターフェース内には基本的に抽象関数しかない。

・インターフェース内で抽象関数を定義するときにはabstractを書かなくてもよい。

コールバック

例えば、JavaのJFrameでキー入力を受け付けたいときには


JFrame window = new JFrame();

window.addKeyListener(new KeyReceiver());

...

class KeyReceiver implements KeyListener {

@Override

public void keyPressed(KeyEvent e) {

//キーが押されたときの処理

}

@Override

public void keyReleased(KeyEvent e) {

//キーが離されたときの処理

}

@Override

public void keyTyped(KeyEvent e) {

//キーが一定時間内に押して離された時の処理

}

}

といった具合に書きます。KeyReceiverクラスはKeyListenerインターフェースを継承しており、KeyListenerで定義された抽象関数keyPressed, keyReleased, keyTypedをオーバーライドしています。そして、そのようにして定義されたKeyReceiverクラスをJFrameのaddKeyListener関数に渡しています。こうすることで、キーボードに関する何らかの動作が起きた時にそれぞれの状況に応じた関数が自動で呼ばれるようになります。

このように、インターフェースや抽象クラスを継承することで「ある出来事が起きた際にするべき動作」を記述しておき、それをどこかの関数に渡して登録を行う...という形式でのプログラムをイベント駆動型のプログラムと言います。また、その際に「ある出来事が起きた時に呼ばれる関数」のことをコールバック関数と言います。実際のライブラリを使うときにはこのようなコールバック関数をひたすら定義しまくって登録しまくることで楽にプログラムを完成させることができるというわけです。

sobaya007 icon
この記事を書いた人
sobaya007

東工大工学部の異端児

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】 feature image
2018年11月3日
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】
Azon icon Azon
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
2024年4月14日
Spotifyのクライアントを自作しよう
d_etteiu8383 icon d_etteiu8383
2021年12月8日
C++ with JUCEでステレオパンを作ってみた【AdC2021 26日目】
liquid1224 icon liquid1224
2019年4月22日
アセンブリを読んでみよう【新歓ブログリレー2019 45日目】
eiya icon eiya
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記