コンテンツにスキップ

RAT Java勉強会2004 第04回 クラスの継承とポリモーフィズム

Javaの利点を説明する際に「継承」や「ポリモーフィズム」の説明は避けて通れません。
また、Javaの継承機能はかなりの制限があるため、それを解消するために「インターフェース」が存在します。
今回は、「継承」「ポリモーフィズム」「インターフェース」などの意味や使い方を紹介します。

  • タクシーの特徴を挙げてください
  • 料金メーターを持つ
  • 客を乗せられる
  • 客を降ろせる
  • お金を取って運転できる
  • 無線で他のタクシーと通信できる
  • 車である
    • 人が乗れる
    • エンジンを持っている
    • ハンドルを持っている
    • アクセルを持っている
    • ブレーキを持っている
    • ハンドルを操作すると進行方向を変えられる
    • アクセルを踏むと加速する
    • ブレーキを踏むと減速する
  • 料金メーターを持つ - has-a 関係
  • 客を乗せられる - 振る舞い
  • 客を降ろせる - 振る舞い
  • 賃送できる - 振る舞い
  • 無線をもつ - has-a 関係
  • 車である - is-a 関係
  • “is-a関係”にあるクラスは、継承によって特性を引き継げる

    • Taxi is-a Car
    • TaxiはCarの特徴を持つ
  • 特性とは、具体的には以下

    • インスタンスフィールド
    • インスタンスメソッド
class Car {
// Carの一般的特性
...
}
class Taxi extends Car {
// Taxiだけが持つ一般的特性
...
}
  • Tanaka is-a Human
    • TanakaはHumanである(Humanの特性を持ち合わせる)
  • Suzuki is-a Human
    • SuzukiはHumanである(Humanの特性を持ち合わせる)
  • Coffee is-a Drink
    • CoffeeはDrinkである(Drinkの特性を持ち合わせる)
  • Juice is-a Drink
    • JuiceはDrinkである(Drinkの特性を持ち合わせる)
  • Human is-a Object

  • Drink is-a Object

  • 全てのクラスは”java.lang.Objectクラス”を継承している

    • 必ず祖先にObjectがいる
  • Javaでは、2つのクラスを同時に継承できない

    • 1つのクラスしか継承できないので”単一継承”と言う
  • 次の例は、単一継承だと実装が困難である

    • 携帯電話は電話機であり、メーラーでもある
      • 携帯電話 is-a 電話機
      • 携帯電話 is-a メーラー
  • Taxi is-a Car だけど…

    • Taxiが走る(Taxi#drive)のは賃送
    • Carが走る(Car#drive)のは賃送ではない
  • 同じ”走る”でも、振る舞いは違う

  • Taxi extends Car

    • Carからdriveは受け継いでいる
  • driveじゃなくて違う名前にするべきか?

    • オブジェクト指向っぽくない
  • Javaでは、継承した振る舞いを上書きできる

    • Taxiでdriveを”再定義する”ともいえる
  • 振る舞いの上書きを「メソッドのオーバーライド」とも言う

    • 同じ名前でいいので、仕様を覚える手間が省ける
    • 実はオブジェクト指向の最高の機能(個人的意見)
  • Taxi is-a Car という関係より

    • TaxiオブジェクトをCarオブジェクトとして使える
  • Taxi特有の機能ではなく、Carとして扱えば十分という場面で役に立つ

public void wideningCast() {
// Taxi is-a Car
Car car = new Taxi();
car.shiftGears();
...
}
  • あるインスタンスを、親クラスの型として扱う変換をワイドニング変換と呼ぶ
  • 全てのクラスの基底であるObjectクラスへはなんでも代入できる
    • プリミティブ型はだめ
public void wideningObject() {
// Taxi is-a Object
Object obj = new Taxi();
// Array is-a Object
Object aobj = new int[5];
}

Car car = new Taxi();

  • 設計図”Taxi”を工場に送って作ってもらう
  • 作ってもらった製品を世界のどこかに置く
  • 置いた場所と型(Taxi)を記録した紙を作成する
  • その紙の型を”Car”に書き換える
    • Taxi is-a Carなので書き換え可能
  • “Car型”の箱”car”へ作成した紙を入れる
  • ワイドニング変換の逆向きに型を変換しようとすることを、ナローイング変換という
  • 普通、これは禁止されている
    • タクシーは車であるが、車はタクシーであるとは限らない
public void narrowingCoversion() {
// o Taxi is-a Car
Car car = new Taxi();
// x Car is-a Taxi
Taxi taxi = car;
}

暗黙ナローイング変換失敗のイメージ

Section titled “暗黙ナローイング変換失敗のイメージ”

Taxi taxi = car;

  • “Car型”の箱”car”から紙を取り出す
  • その紙の型を”Taxi”に書き換える
    • ここで、Car is-a Taxi が成り立たないので失敗
  • ナローイング変換は明示的に型のキャストを行うことで可能になる。
  • 型キャストに失敗すると、ClassCastExceptionがスローされる
public void downCast() {
// Taxi is-a Car
Car car = new Taxi();
// Car can be a Taxi
Taxi taxi = (Taxi) car;
}

Taxi taxi = (Taxi) car;

  • “Car型”の箱”car”から紙をコピーしてくる
  • その紙の型が”Taxi”に変換できるか調べに行く
    • Explicitなキャストは、実際に現地へ調べにいく
  • その紙の型を”Taxi”に書き換える
  • “Taxi型”の箱”taxi”へその紙を入れる
  • インスタンスがそのクラスから生成されたかどうか調べられる
    • これを多用するプログラムはオブジェクト指向らしくない
public void isInstanceOf() {
// Taxi is-a Object
Object obj = new Taxi();
// print this obj is instanceof Taxi?
System.out.println(obj instanceof Taxi);
}
  • 動物は鳴く
class Animal {
public void sing() {
}
}
  • 犬は動物である
  • 犬はBowwowと鳴く(表示する)
class Dog extends Animal {
public void sing() {
System.out.println("Bowwow");
}
}
  • 猫は動物である
  • 猫はMewと鳴く(表示する)
class Cat extends Animal {
public void sing() {
System.out.println("Mew");
}
}
  • 次の文章をプログラムに変換してみましょう

“犬という動物がいる。その動物は鳴いた。”

“猫という動物がいる。その動物は鳴いた。“

class MainDog {
public static void main(String[] args) {
// 犬という動物がいる。…
Animal animal = new Dog();
// …その動物は鳴いた。
animal.sing();
}
}
class MainCat {
public static void main(String[] args) {
// 猫という動物がいる。…
Animal animal = new Cat();
// …その動物は鳴いた。
animal.sing();
}
}
  • それぞれのanimal.sing()の挙動を考えてみましょう
  • 同じメソッド呼び出しに対して、異なるオブジェクトが異なる操作をすること

    • 対象の振る舞いの詳細を気にせず、名前だけで呼べばよい
  • “その動物は鳴いた” とだけあって、動物が何かを気にしない

    • 犬ならば勝手に “Bowwow” と鳴く振る舞いをする
    • 猫ならば勝手に “Mew” と鳴く振る舞いをする

“犬が2匹、猫が1匹いる。3匹の動物達はそれぞれ鳴いた。“

class AnimalEnsemble {
public static void main(String[] args) {
// 3匹の動物達
Animal[] animals = new Animal[] {
// 犬が2匹
new Dog(), new Dog(),
// 猫が1匹
new Cat()
};
// 3匹の動物達はそれぞれ…
for (int i = 0; i < animals.length; i++) {
// 鳴いた
animals[i].sing();
}
}
}
  • 実際に絵を描いてみると分かりやすい
    • 第02回のメソッド呼び出しを参照

04p31.png

  • オブジェクトの実装を気にせずに使用できる

    • 相手の振る舞いは相手に任せる
    • 相手が変わると振舞いも変わる
  • 処理を一般化できる

    • 覚えることが少なくてすむ
    • “動物は鳴く”というカテゴリの振る舞いのみを知っていれば良い
  • 先ほどのプログラムを考えてみる

    • 犬という実体は世界に存在した
    • 猫という実体は世界に存在した
    • 動物という実体は?
  • 犬も猫も動物だが、“動物”そのものはいない

  • 犬や猫は存在する

    • 具体的なオブジェクトを表している
  • 動物そのものは存在していない

    • 犬や猫というカテゴリを階層化するための”概念”
    • 抽象的な存在 (動物はいないが、動物である犬はいる)
  • 動物は抽象的なクラス(存在ではなく概念)であるといえる
  • しかし、このままでは具象化した存在になれる
public void concretise() {
// 動物を作成
Animal animal = new Animal();
...
}
  • Javaでは抽象的なクラスをプログラムで表現できる
    • インスタンス化できない
    • 継承されて始めて意味を持つ
abstract class Animal {
public void sing() {
}
}
  • 振る舞いを実際に持たない、名前だけのメソッド
  • 抽象クラスは抽象メソッドを持つことができる
    • サブクラスで必ずオーバーライドしなければならない
    • Animalでsingという振る舞いを定義することはできない
      • DogやCatで定義することはできる
      • Animalが持っていないと、Animal型から使えない
abstract class Animal {
abstract public void sing();
}
  • 抽象クラスは通常のメソッドと抽象メソッドを混在させられる

    • カテゴリに共通の処理を抽象クラス内で記述
    • サブクラス特有の処理を抽象メソッドにする
  • サブクラスで特有の処理だけを書くことによって、オブジェクトを定義できる

    • このような実装を”骨格実装”という
  • オブジェクト指向プログラムの美しさは、ここに掛かっている

  • 喫茶店とコーヒーメーカーの関連性はなんでしょうか

    • 喫茶店 is-a 店
    • コーヒーメーカー is-a 機械
  • 現実世界のカテゴリで考えると、関連性はなさそうに見える

  • 喫茶店とコーヒーメーカーで共通する関連性はなんでしょうか

    • 喫茶店はコーヒーを出す
    • コーヒーメーカーはコーヒーを出す
  • 振る舞いだけ見ると”コーヒーを出す”という共通点がある

  • いくつかのクラスが共通の振る舞いを持つ場合、それらは共通のインターフェースを持つ

    • “コーヒーを出すもの” というインターフェース
  • このインターフェースをJavaで記述することができる

  • “コーヒーを出すもの”というインターフェース
interface CoffeeProvider {
// コーヒーを出す作業を持っている
Coffee getCoffee();
}

インターフェースの使用 (CoffeeMaker)

Section titled “インターフェースの使用 (CoffeeMaker)”
  • CoffeeMakerはCoffeeProviderを実際に”実装している” (コーヒーを出すものとして振る舞える)
class CoffeeMaker implements CoffeeProvider {
...
public Coffee getCoffee() {
return new Coffee();
}
}

インターフェースの使用 (CafeShop)

Section titled “インターフェースの使用 (CafeShop)”
  • CafeShopはCoffeeProviderを実際に”実装している” (コーヒーを出すものとして振る舞える)
class CafeShop implements CoffeeProvider {
// CafeShop has-a コーヒーを作る手段
private CoffeeProvider coffeeMaker = ...
public Coffee getCoffee() {
return coffeeMaker.getCoffee();
}
}
  • “コーヒーを入れるもの”という機能でカテゴライズしたが、is-a関係が成り立っている

    • CafeShop is-a CoffeeProvider
    • CoffeeMaker is-a CoffeeProvider
  • あとは、普通の親クラスのようにワイドニング変換して使える

class Caffine {
public static void main(String[] args) {
// コーヒーを提供できる何か
CoffeeProvider cc = new CafeShop();
// 実際に生成
Coffee coffee = cc.getCoffee();
}
}
  • インターフェースを持っているということ

    • 「最低限、この機能だけは使える」という指標
  • 違うカテゴリにいるけど、共通する”界面”を持つということ

    • ここで、界面とは振る舞いのことである

抽象クラス vs. インターフェース

Section titled “抽象クラス vs. インターフェース”
  • 抽象クラス

    • 1つしか継承できない
    • 骨格実装ができる
  • インターフェース

    • いくつでも継承できる
    • 具象メソッドを持てない
  • 状況によって使い分けることが大切

続々・オブジェクト指向プログラミング

Section titled “続々・オブジェクト指向プログラミング”
  • 次の文章をプログラムにしてみましょう

“喫茶店でコーヒーを注文し、Tanakaはそれを飲んだ。喫茶店でジュースを注文し、Suzukiはそれを飲んだ”

  • 喫茶店
  • コーヒー
  • Tanaka
  • ジュース
  • Suzuki
  • カテゴリが近いと思われるオブジェクトを探す

  • 喫茶店

  • コーヒー、ジュース

  • Tanaka、Suzuki

  • 共通点を洗い出して上位クラスを作る

  • 喫茶店 (“店”を上位に持ってきても良い)

  • 飲み物

    • コーヒー (Coffee is-a Drink)
    • ジュース (Juice is-a Drink)
    • Tanaka (Tanaka is-a Human)
    • Suzuki (Suzuki is-a Human)
  • 階層を考えずに、オブジェクトに振る舞いを結びつける

  • 喫茶店

    • コーヒーを注文される (getCoffee())
    • ジュースを注文される (getJuice())
  • Tanaka

    • コーヒーを飲む (drink(Coffee))
  • Suzuki

    • ジュースを飲む (drink(Juice))
  • 振る舞いの引数を一般化する

    • コーヒーを飲む → 飲み物を飲む
    • ジュースを飲む → 飲み物を飲む
  • Tanaka

    • 飲み物を飲む (drink(Drink))
  • Suzuki

    • 飲み物を飲む (drink(Drink))
  • 親クラスに引き上げられる振る舞いを探す

  • Human#drink(Drink)

    • Tanaka#drink(Drink)
    • Suzuki#drink(Drink)
  • ここで、TanakaやSuzukiが特殊な飲み方をするなら、オーバーライドすればよい

  • あとは今までの流れと同様にすればよい
  • 次の文章は第3回で使用した例題である。 “Tanakaは5000円持った状態でタクシーに乗った。タクシーは乗客が乗ると料金メーターをリセットし、1km走るごとに200円ずつ課金する。Tanakaはその車で10km走り、その後に降りた。タクシーは乗客が降りる際に課金された金額を乗客に請求する。”

  • 継承を使って再利用可能にせよ

    • Tanaka is-a 乗客
      • 「“Tanaka”という名前を持つ5000円持った状態にある乗客」としても良い
    • タクシー is-a 車
  • どのクラスにどの振る舞いがあるか注意せよ

  • 課題1で作成したプログラムを流用し、次の文章をプログラムに変換せよ
    • ただし、課題1のプログラムを変更してはならない

“Satoは10000円持った状態でタクシーに乗った。タクシーは乗客が乗ると料金メーターをリセットし、1km走るごとに200円ずつ課金する。Satoはその車で18km走り、その後に降りた。タクシーは乗客が降りる際に課金された金額を乗客に請求する。“

  • 多重継承ができないJavaでは”携帯電話”は表現しにくかった
  • 次のように考えてみてはどうか
    • 電話機は電話できるというインターフェースを持つ
    • メーラーはメールできるというインターフェースを持つ
    • 携帯電話は電話機を内部に持っている (has-a)
    • 携帯電話はメーラーを内部に持っている (has-a)
    • 携帯電話は電話できるというインターフェースを持つ
    • 携帯電話はメールできるというインターフェースを持つ
  • これらのオブジェクトの関連性を説明せよ