Files
java-cui-em-oop/doc/Day03_継承・抽象クラス・インターフェース・ポリモーフィズム.md

29 KiB
Raw Blame History

CUI_EM_OOP ミッション課題【3日目】継承・抽象クラス・インターフェース・ポリモーフィズム

このミッションについて

このミッションは、最終的に 「社員メンテナンス(ファイルシステム版)」(CUI_EM_OOP) を完成させることを目標にした、複数回に分かれたミッション課題の3日目です。

【今日からのルール変更】 1〜3段階目(基礎確認ゾーン)は、今日からは前回までに作ったファイル(Book・Item・Student・EmployeeData・Mainなど)を一切使わず、新しいクラスだけで完結する練習問題にします。毎回その日の内容だけを、他の実装に引きずられずシンプルに確認できるようにするためです。(4〜6段階目「CUI_EM_OOP制作ゾーン」は、これまで通り前回までの続きから作業します。)

まだ以下の内容は習っていないため使いません。今後のミッションでさらにバージョンアップしていきます。

  • コレクション(ArrayListなど)
  • 例外処理
  • ファイルの読み書き

今日の講義内容

  • 継承(extends)
  • superキーワードの構文
  • @Overrideによるメソッドオーバーライド(自分で作った親クラスに対するオーバーライド)
  • 抽象クラスとインターフェース
  • ポリモーフィズムの基本(親クラス型の変数に子クラスのインスタンスを代入する意味)

基本の書き方(文法メモ)

今日はじめて出てくる書き方をまとめておきます。各段階で迷ったときは、ここに戻って確認してください。

継承(extends)

public class 親クラス名 {
	// フィールド・メソッド
}

public class 子クラス名 extends 親クラス名 {
	// 親クラスのフィールド・メソッドがそのまま使える
	// 独自のフィールド・メソッドを追加できる
}

子クラスは、親クラスのpublicなフィールドやメソッドを、そのまま自分のものとして使うことができます(privateなフィールドは、親クラスの中でしか直接アクセスできないという規則は継承していても変わりません)。

super(コンストラクタの呼び出し)

public class 親クラス名 {
	public 親クラス名(型 引数名) {
		// ...
	}
}

public class 子クラス名 extends 親クラス名 {
	public 子クラス名(型 引数名) {
		super(親クラスのコンストラクタに渡す値); // 必ず1行目に書く
		// 子クラス独自の初期化処理
	}
}
  • super(...)は、親クラスのコンストラクタを呼び出す書き方です。書く場合はコンストラクタの中の一番最初の行に書く必要があります。
  • 親クラスに「引数なしコンストラクタ」が1つもない場合、子クラスは必ずsuper(...)で親クラスのどのコンストラクタを呼ぶかを指定しなければ、コンパイルエラーになります。
  • 逆に、親クラスに引数なしコンストラクタしかない場合(今回のBaseDataのように、コンストラクタを1つも書いていない場合も含む)は、子クラス側でsuper()を書かなくても、Javaが自動的に親クラスの引数なしコンストラクタを呼び出してくれます。

super(メソッドの呼び出し)

public class 子クラス名 extends 親クラス名 {
	@Override
	public 戻り値の型 メソッド名() {
		戻り値の型 親の結果 = super.メソッド名(); // オーバーライドする前の、親クラスの処理を呼び出す
		// 親の結果を使って、独自の処理を追加する
	}
}

super.メソッド名()と書くと、オーバーライドする前の親クラスが元々持っていた処理を呼び出せます。「親の処理はそのまま活かしつつ、少しだけ追加したい」というときによく使います。(1段階目のCircleクラスで実際に使います)

抽象クラス(abstract class)

public abstract class 親クラス名 {
	public abstract 戻り値の型 メソッド名(); // 中身を書かない(抽象メソッド)
}
  • abstractをつけたクラスは、そのままではnewでインスタンス化できません。必ずextendsした子クラスを作り、その子クラスをインスタンス化して使います。
  • abstractをつけたメソッド(抽象メソッド)は中身({ })を書きません。「これを継承する子クラスは、必ずこのメソッドを実装(オーバーライド)してください」という約束を表します。
  • 抽象メソッドを1つでも実装し忘れると、その子クラスはコンパイルエラーになります。

インターフェース(interface)

public interface インターフェース名 {
	戻り値の型 メソッド名(); // 中身を書かない
}

public class クラス名 implements インターフェース名 {
	@Override
	public 戻り値の型 メソッド名() {
		// 実装
	}
}
  • インターフェースは抽象クラスとよく似ていますが、extendsではなくimplementsというキーワードを使います。
  • インターフェースの中に書くメソッドは、すべて中身を書かない「約束」だけのメソッドです。
  • クラスの継承(extends)は1つの親クラスしか指定できませんが、インターフェースはimplements インターフェースA, インターフェースBのように複数同時に実装できます。

ポリモーフィズム(親クラス型の変数に子クラスのインスタンスを代入する)

親クラス名 変数名 = new 子クラス名();
  • 変数の型は「親クラス」ですが、中に入っているのは「子クラス」のインスタンスです。
  • この変数からメソッドを呼び出すと、実際に代入されている子クラス側でオーバーライドされた内容が実行されます(親クラスの元々の中身ではありません)。
  • この仕組みのおかげで、親クラス名[]の配列に、いろいろな種類の子クラスのインスタンスをまとめて入れて、同じように扱うことができるようになります。
  • 注意:変数の型が親クラスの場合、親クラスに書かれているメソッドしか直接は呼び出せません。子クラスだけが持つ独自のメソッドを呼びたい場合は「キャスト」という書き方が必要になります(7〜9段階目で扱います)。

publicについて

これまでと同様、コンストラクタ・ゲッター・セッター・オーバーライドするメソッドはpublicで宣言してください(詳しい理由は2日目の文法メモを参照)。

すすめかた

  1. 1段階目から順番に取り組んでください。むずかしそうに見えても、飛ばさないでください。
  2. 各段階の「お題」をよく読んで、「何を作るのか」を最初に理解してから手を動かしましょう。
  3. コードが書けたら、必ず実行して**「実行例」と同じ結果になるか**確認しましょう。
  4. わからない言葉が出てきたら、まず自分で調べてみましょう。調べてもわからないときは、講師に質問してください。
  5. コピー&ペーストではなく、自分の手で入力して理解しましょう。

プロジェクトの作り方

これまで使っているプロジェクトに、新しいファイルを追加していく形で進めてください。パッケージ分けはまだ行いません。

提出物

  • 今日修正・作成した.javaファイル一式

【基礎確認ゾーン】1〜3段階目

このゾーンは、CUI_EM_OOPとは直接関係のない練習問題です。前回までに作ったBook・Item・Student・EmployeeDataは一切使いません。 すべて新しいクラスを使って、今日の内容がしっかり身についているかを確認しましょう(1〜3段階目の中でお互いに作ったクラスを使い回すのはOKです)。

1段階目(継承とsuperでシンプルな図形クラスを作ろう)

お題

「図形」を表すShapeクラスと、それを継承したCircle(円)クラスを作りましょう。

<作成フローとポイント>

  1. Shape.javaを新規作成し、以下の内容で作成してください(コンストラクタ・メソッドはすべてpublicで宣言してください)。
    • フィールド:name(String型、private)
    • コンストラクタ:引数String nameを1つ受け取り、nameフィールドに代入する(引数なしコンストラクタは作りません)
    • メソッドarea:引数なし、戻り値double、中身はreturn 0.0;(本来は図形ごとに違う値になるはずですが、今の時点では仮の値です)
    • メソッドshowInfo:引数なし、戻り値なし(void)、中身はthis.name + "の面積は" + this.area() + "です"をSystem.out.printlnで出力する
  2. Circle.javaを新規作成し、以下の内容で作成してください。
    • Shapeクラスを継承する(extends)
    • フィールド:radius(double型、private)
    • コンストラクタ(public):引数double radiusを1つ受け取る
      • 中でsuper("円")を呼び出してください(Shapeに引数なしコンストラクタがないため、このsuper(...)を書かないとコンパイルエラーになります)
      • radiusフィールドにも引数の値を代入する
    • areaメソッドをpublic・@Overrideにし、Math.PI * radius * radius(円の面積の公式)を返すようにする
      • ※Shape側のareaがpublicなので、オーバーライドする側も必ずpublicにする必要があります(publicより狭い書き方をするとコンパイルエラーになります)
    • showInfoメソッドもpublic・@Overrideしてください。中身は以下の2行にしてください。
      • 1行目:super.showInfo();(オーバーライドする前の、ShapeのshowInfoをそのまま呼び出す)
      • 2行目:"(半径:" + this.radius + ")"をSystem.out.printlnで出力する
  3. Day3Check1.javaを新規作成し、mainメソッドの中で以下を行ってください。
    • radiusが2のCircleインスタンスを作成する
    • 作成したインスタンスのshowInfoメソッドを呼び出す

<実行例>

円の面積は12.566370614359172です
(半径:2.0)

面積の値は小数点以下が長く表示されますが、Math.PIを使って正しく計算されている証拠なので、そのままでOKです。

2段階目(抽象クラスとインターフェースを使ってみよう)

お題

Shapeクラスを抽象クラスに変え、新しい図形Rectangle(長方形)を追加します。また、まったく別の練習として、インターフェースも使ってみましょう。

<作成フローとポイント>

①Shapeを抽象クラスにする

  1. Shape.javaを修正してください。
    • クラス宣言をpublic abstract class Shapeに変える
    • areaメソッドを、中身のない抽象メソッドに変える(public abstract double area();)
    • ※Circleクラスはすでにareaを@Overrideして実装済みなので、修正は不要です(そのままコンパイルが通ることを確認しましょう)。
  2. Rectangle.javaを新規作成してください。
    • Shapeクラスを継承する
    • フィールド:width、height(どちらもdouble型、private)
    • コンストラクタ(public):引数double width, double heightを受け取り、super("四角形")を呼び出したうえで、2つのフィールドに代入する
    • areaメソッドをpublic・@Overrideにし、width * heightを返すようにする(Circleのときと同じ理由でpublicが必須です)

②インターフェースを使ってみる(①とは無関係の練習です)

  1. Greeting.javaを新規作成し、以下の内容のインターフェースを作成してください。

    public interface Greeting {
    	String hello();
    }
    
  2. JapaneseGreeting.javaを新規作成してください。

    • Greetingインターフェースをimplementsする
    • helloメソッドをpublic・@Overrideにし、"こんにちは"を返すようにする
      • ※インターフェースのメソッドは元々publicとして扱われるため、実装する側も必ずpublicにする必要があります(toStringのときと同じルールです)
  3. Day3Check2.javaを新規作成し、mainメソッドの中で以下を行ってください。

    • widthが3、heightが4のRectangleインスタンスを作成し、showInfoメソッドを呼び出す
    • Greeting型の変数にJapaneseGreetingのインスタンスを代入し、helloメソッドの戻り値を出力する

<実行例>

四角形の面積は12.0です
こんにちは

<余裕がある人向け(任意)>

LoudGreetingという、もう1つのインターフェース(String shout();)を作り、JapaneseGreetingクラスにGreetingとLoudGreetingの両方を実装させてみましょう(implements Greeting, LoudGreeting)。クラスは1つしか継承できませんが、インターフェースは複数実装できることを確認してみてください。

3段階目(ポリモーフィズムを体感しよう)

お題

1〜2段階目で作ったShape・Circle・Rectangleを使って、「親クラス型の変数に子クラスのインスタンスを代入する」とはどういうことかを確認しましょう。

<作成フローとポイント>

  1. Day3Check3.javaを新規作成し、mainメソッドの中で以下を行ってください。
    • Shape型の変数shape1に、radiusが2のCircleインスタンスを代入する(Shape shape1 = new Circle(2);)
    • shape1のshowInfoメソッドを呼び出す
      • 変数の型はShapeなのに、Circleで上書きしたareaの内容(円の面積の計算)が使われることを確認しましょう
    • Shape型の変数shape2に、widthが3、heightが4のRectangleインスタンスを代入する(Shape shape2 = new Rectangle(3, 4);)
    • shape2のshowInfoメソッドを呼び出す
      • 同じShape型の変数なのに、今度はRectangleで上書きしたareaの内容(長方形の面積の計算)が使われることを確認しましょう

<実行例>

円の面積は12.566370614359172です
(半径:2.0)
四角形の面積は12.0です

ポイント①:shape1とshape2はどちらも同じShape型の変数ですが、代入したインスタンスがCircleかRectangleかによって、showInfo()の結果(特にarea()の計算内容)が変わっています。これがポリモーフィズムの効果です。 ポイント②:CircleはshowInfoをオーバーライドして半径の情報を追加していますが、RectangleはshowInfoをオーバーライドしていないため、ShapeのshowInfoがそのまま使われています。「オーバーライドは、変えたいメソッドだけ行えばよい」ということも確認できます。


【CUI_EM_OOP制作ゾーン】4〜6段階目

ここからは、前回までに作ったEmployeeData・Mainを、今日習った道具でバージョンアップしていきます。

4段階目(BaseDataを作り、EmployeeDataに継承させよう)

お題

社員データ以外のデータ(後日追加する部署データなど)も含めて共通で使える、BaseDataという抽象クラスを作りましょう。

<作成フローとポイント>

  1. BaseData.javaを新規作成し、以下の内容で作成してください。
    • public abstract class BaseDataとして宣言する(フィールドはありません)
    • 以下の3つの抽象メソッドを宣言する(すべて中身なし)
      • public abstract String toString();
      • public abstract String toCSV();
      • public abstract String getPrimaryKeyValue();
    • ※toStringはObjectクラスのメソッドを上書きする形にもなりますが、ここではまだ中身のない抽象メソッドとして宣言するだけなので、@Overrideはつけなくて構いません(つけても間違いではありませんが、今回は付けない形で統一します)。
  2. EmployeeData.javaを修正し、BaseDataクラスを継承してください(public class EmployeeData extends BaseData)。
    • 2日目にすでに@Override public String toString()を作成済みなので、そのままでBaseDataのtoStringの約束を満たしています(修正不要です)。
    • EmployeeDataのコンストラクタにsuper(...)を書く必要はありません。BaseDataにはコンストラクタを1つも書いていないため、Javaが自動的に引数なしの親コンストラクタを呼び出してくれるためです。
  3. EmployeeData.javaに、以下の2つのメソッドを追加してください(どちらもpublic・@Override)。
    • toCSV:引数なし、戻り値String。6つのフィールドの値を、ゲッターを使ってカンマ区切り(",")でつなげてreturnする
      • 例:getIdEmployee() + "," + getNmEmployee() + ...
    • getPrimaryKeyValue:引数なし、戻り値String。idEmployeeフィールドの値をそのままreturnする
  4. Main.javaで、4段階目(1日目)から作っているemployee変数を使って、employee.toCSV()とemployee.getPrimaryKeyValue()の結果をそれぞれ出力し、正しく動いているか確認してください。

<実行例>

10000,山田龍也,ヤマダタツヤ,yamada@hoge.jp,aaa,10
10000

(出力の形式は自由です。上の例のように2行に分けて出力すればOKです。)

5段階目(Mainでポリモーフィズムを使おう)

お題

Main.javaの中で、EmployeeData型を使っていた変数・配列を、BaseData型に変更してみましょう。

<作成フローとポイント>

  1. Main.javaを見直し、以下の宣言を変更してください。
    • EmployeeData employee = ... → BaseData employee = ...
    • EmployeeData[] employees = new EmployeeData[5]; → BaseData[] employees = new BaseData[5];
    • ※右辺でnew EmployeeData(...)しているところは変更不要です。「EmployeeDataのインスタンスを、BaseData型の変数・配列に入れる」という変更です。
  2. 変更すると、isMatchIdメソッドを呼び出している箇所が、すべてコンパイルエラーになります。BaseData型にはisMatchIdというメソッドが存在しないためです。呼び出し箇所は複数あるので、探し忘れに注意してください。
    • 1日目7段階目で書いた動作確認用の呼び出し(employees[2].isMatchId("10002")など)
    • 1日目8〜9段階目で書いた検索処理の中の呼び出し
    • これらの何か.isMatchId(引数)という呼び出しを、すべて何か.getPrimaryKeyValue().equals(引数)という書き方に直してください。
      • 例:employees[2].isMatchId("10002") → employees[2].getPrimaryKeyValue().equals("10002")
      • 検索処理の中の呼び出し(employees[i].isMatchId(入力されたID)のような形。入力されたIDの部分は、8段階目でScannerから読み取った自分がつけた変数名に置き換えてください)→ employees[i].getPrimaryKeyValue().equals(入力されたID)
    • getPrimaryKeyValueはBaseDataで約束されているメソッドなので、BaseData型のままでも呼び出せます。
    • isMatchIdメソッド自体はEmployeeData.javaに残しておいて構いません(今後使わなくなりますが、消さなくても動作に影響はありません)。
  3. Main.javaのすべての動作(全件表示・ID検索など)が、今まで通り同じ結果になることを確認してください。

<実行例>

これまでと表示内容は変わりません。1日目の実行例、2日目6段階目の実行例と見比べて確認してください。

ポイント:isMatchIdはEmployeeData専用のメソッドでしたが、getPrimaryKeyValueはBaseDataを継承したクラスなら何でも使える共通の仕組みです。今回のように「専用メソッド」を「共通の仕組み」に置き換えられるのが、抽象クラスとポリモーフィズムの強みです。

6段階目(DepartmentDataを作って、ポリモーフィズムの威力を実感しよう)

お題

社員データとは別に、部署データを表すDepartmentDataクラスを作り、EmployeeDataと一緒くたに扱えることを確認しましょう。

<作成フローとポイント>

  1. DepartmentData.javaを新規作成し、EmployeeDataと同じ要領で以下の内容で作成してください。
    • BaseDataクラスを継承する
    • フィールド(すべてprivate・String型):idDepartment(部署ID)、nmDepartment(部署名)
    • 2つのフィールドそれぞれにゲッター・セッターを作成する(getIdDepartment/setIdDepartment、getNmDepartment/setNmDepartment)
    • コンストラクタは何も書かなくて構いません(Javaが自動的に用意してくれる「引数なし・中身なし」のコンストラクタをそのまま使います)
    • 以下の3つのメソッドをpublic・@Overrideで作成する
      • toString:getIdDepartment() + "\t" + getNmDepartment()を返す
      • toCSV:getIdDepartment() + "," + getNmDepartment()を返す
      • getPrimaryKeyValue:getIdDepartment()を返す
  2. Main.javaに、以下のデータをすべて格納できるBaseData型の配列allDataを用意してください(BaseData[] allData = new BaseData[11];)。
    • allData[0]〜allData[4]:5段階目でBaseData型に変えたemployees配列の中身(employees[0]〜employees[4])をそのまま代入する

    • allData[5]〜allData[10]:以下の部署データを、それぞれ新しくDepartmentDataインスタンスを作成して代入する

      10,役員
      11,人事・総務部
      12,経理部
      13,システム開発事業部
      14,教育事業部
      15,営業企画部
      

      <部署データを入れるときの注意> allDataはBaseData型の配列です。allData[5] = new DepartmentData();のように先に配列へ代入してしまうと、そのあとでallData[5].setIdDepartment(...)のようなDepartmentData専用のセッターを呼び出せなくなります(BaseData型にはセッターの約束がないためです)。必ず次の順番で進めてください。

      1. DepartmentData型の変数(例:dept)を用意し、new DepartmentData()でインスタンスを作成する
      2. deptのセッターを使って、部署ID・部署名を設定する
      3. 設定が終わったdeptをallData[5]のように代入する
  3. for文を使って、allDataの中身を順番にSystem.out.printlnで出力してください。

<実行例>

10000	山田龍也	ヤマダタツヤ	yamada@hoge.jp	aaa	10
10001	高橋準	タカハシジュン	jtaka@hoge.jp	bbb	10
10002	麻枝史明	マエダフミアキ	fmak@hoge.jp	ccc	11
10003	丸戸章介	マルトショウスケ	marusyo@hoge.jp	ddd	12
10004	三宅武	ミヤケタケシ	miya@hoge.jp	eee	13
10	役員
11	人事・総務部
12	経理部
13	システム開発事業部
14	教育事業部
15	営業企画部

ポイント:allDataはBaseData型の配列ですが、中身はEmployeeDataとDepartmentDataというまったく別のクラスのインスタンスが混ざっています。それでも同じfor文・同じSystem.out.printlnで、それぞれ正しい形式で表示できました。これがポリモーフィズムの一番の効果です。


【挑戦問題ゾーン】7〜9段階目

ここからは応用問題です。今日習った内容に加えて、自分で少し調べながら進める部分があります。難しく感じても焦らず、じっくり調べながら取り組んでみましょう(経験者の方でも、多少時間がかかる内容です)。

7段階目(instanceofで型を確認しよう)

お題

6段階目で作ったallData配列の中身が、実際にはEmployeeDataとDepartmentDataのどちらなのかを調べる方法を調査し、それぞれの件数を数えてください。

<お題の条件>

  • allDataをfor文で1件ずつ確認し、その要素が実際にはEmployeeDataのインスタンスなのか、DepartmentDataのインスタンスなのかを判定する
  • 社員データの件数と、部署データの件数を、それぞれ数えて出力する

<考え方のヒント>

  • 「ある変数の中身が、実際にはどのクラスのインスタンスか」を調べる演算子があります。「Java instanceof」で調べてみましょう。

<実行例>

社員データ:5件
部署データ:6件

8段階目(キャストでサブクラス限定の機能を使おう)

お題

allDataはBaseData型の配列なので、そのままではEmployeeDataだけが持っているgetMailAddressメソッドを呼び出せません。「キャスト」という方法を調べて、全社員のメールアドレスを表示してください。

<お題の条件>

  • allDataをfor文で1件ずつ確認し、7段階目のinstanceofを使ってEmployeeDataかどうかを判定する
  • EmployeeDataだった場合のみ、EmployeeData型にキャストしたうえでgetMailAddressメソッドを呼び出し、結果を出力する

<考え方のヒント>

  • 「親クラス型の変数の中身を、実際の子クラスの型として扱いなおす」書き方を「Java キャスト」で調べてみましょう。書き方の形は(型名) 変数名です。
  • instanceofで確認せずにキャストすると、実際の型と違う型にキャストしようとしてClassCastExceptionというエラーになることがあります。必ずinstanceofで確認してからキャストしましょう。

<実行例>

yamada@hoge.jp
jtaka@hoge.jp
fmak@hoge.jp
marusyo@hoge.jp
miya@hoge.jp

9段階目(部署名から社員を検索する集計プログラムを作ろう)

お題

7〜8段階目で調べたinstanceofとキャストを組み合わせて、「部署名」から「その部署に所属する社員」を検索するプログラムを作ってください。

<お題の条件>

  1. allDataの中から、nmDepartmentが"システム開発事業部"であるDepartmentDataを探し、そのidDepartment(部署ID)を調べる
  2. 1で調べた部署IDと、idDepartmentが一致するEmployeeDataをallDataの中からすべて探す
  3. 見つかった社員のnmEmployee(社員名)とmailAddress(メールアドレス)を、タブ区切りで出力する

※nmDepartment・idDepartment・nmEmployee・mailAddressはすべてprivateなフィールドなので、実際にはそれぞれのゲッター(getNmDepartment()など)を使って値を取得してください。

<実行例>

三宅武	miya@hoge.jp

<余裕がある人向け(任意)>

部署名を"システム開発事業部"のように固定で書くのではなく、Scannerを使ってキーボードから入力できるようにしてみましょう。存在しない部署名が入力された場合の表示(例:「該当する部署が見つかりませんでした」)も考えてみてください。


まとめ

今日は「継承」「super」「抽象クラス」「インターフェース」「ポリモーフィズム」という、オブジェクト指向の中心となる仕組みを学びました。EmployeeDataはBaseDataを継承する形になり、DepartmentDataという仲間も加わって、BaseData型でまとめて扱えるようになりました。これは最終的に完成するシステムの土台となる、とても重要な変更です。

今日書いたコードも消さずに残しておいてください。次回以降のミッションで、コレクション・例外処理・ファイル入出力などを習いながら、最終的に本物の「社員メンテナンス(ファイルシステム版)」システムに近づけていきます。