継承
定義
親クラスをベースにして子クラスを定義すること。これによって、継承元(親)のクラス構造をすべて継承先(子)が持つことになり、親クラスと同じメンバの定義は省略できる。継承はオブジェクト指向言語でしかできない
有用性
コードの重複を懸念することなく類似したクラスを作成できる
差分コーディングをするために有用
ここでの説明時、先生がさらっとしかしていないのにクラス図を当たり前のように書くので、ここで再確認。
以下geminiにUMLで継承関係の書き方を教えてくださいと質問した回答
UML(クラス図)において、継承関係は「実線」と「白抜きの三角矢印(三角形の矢じり)」を使って表現します。UMLの正式な用語では「汎化(はんか)」と呼びます。
1. 矢印の向きと線の種類
- 線の種類: 実線(インターフェースの実装は点線なので混同しないよう注意)。
- 矢印の形: 中が白い(白抜きの)大きな三角形。
- 矢印の向き: 「子クラス(派生クラス)」から「親クラス(スーパークラス/基底クラス)」に向けて矢印を引きます。
2. 植物や動物を例にした具体例
例えば、「動物(Animal)」という親クラスを、「犬(Dog)」や「猫(Cat)」が継承する場合、以下のように作図します。複数の子クラスがある場合は、矢印の根元を1本にまとめるのが一般的です。
3. テキスト(PlantUMLなど)で書く場合
開発現場でよく使われるテキストベースの作図ツール Qiita などでは、以下のような記号(<|--)で表現します。
' 親クラス <|-- 子クラス
Animal <|-- Dog
Animal <|-- Cat4. 記述のポイント
- 属性・メソッドの省略: 子クラスは親クラスの性質(属性やメソッド)を自動的に引き継ぐため、子クラス側に親と同じ属性やメソッドをわざわざ書き直す必要はありません。子クラスが独自に持つ追加の属性・メソッドや、オーバーライド(上書き)するものだけを記載します。
- 抽象クラスの表現: もし親クラスがインスタンス化しない「抽象クラス(Abstract Class)」である場合は、クラス名をイタリック体(斜体)にするか、クラス名の上に <> と記述します。
継承を学ぶ上で重要な用語:オーバーライド
定義
親クラスのメンバを子クラス側で上書きすること
方法
親クラスを継承して子クラスのメンバを宣言するときに、シグネチャ(引数の個数や型とその並び順)が同じであることが条件
注:逆にオーバーロードはシグネチャが異なることが条件だった。
注意点
オーバーライドは親クラスのメソッドを無くすものではないので、オーバーライドしても子クラスからsuperを使うことで呼び出すことができる。ただし、オーバーライドするクラス内でのみ可能。
superは「今より1つ内側のインスタンス部分」を表す予約語とのこと。
ただし、これで親クラスのメンバにはアクセスできるが、さらに上の継承元である親の親のメンバにはアクセスできない
継承でのコンストラクタの動き
まず覚えておくべきこととして親クラスを継承しても親クラスのコンストラクタは継承されない。それゆえ、classを継承させると継承する子クラスのコンストラクタに明示的に親クラスのコンストラクタを記述(super())していない場合、コンパイラによって自動的に親クラスのコンストラクタを作動させる以下のコードが挿入される。
// 子クラスのコンストラクタを定義している場合
public クラス名() {
super(); // このコードが子クラスのコンストラクタの最初に挿入される
// 定義していた処理
}
// 子クラスのコンストラクタを定義していなかった場合
public クラス名() {
super();
}
このようなことが起こるため、インスタンス生成時に親クラスのコンストラクタが作動し、次に子クラスのインスタンスが作動するため、見た目上コンストラクタが継承されたように見える。しかし、これによって以下のような問題が発生することに留意する必要がある。
例として以下のようなコードを実行する。
// Item.java
public class Item {
String name;
int price;
public Item(String name) {
this.name = name;
this.price = 0;
}
public Item(String name, int price) {
this.name = name;
this.price = price;
}
}
// Weapon.java
public class Weapon extends Item {
}
// Main.java
public class Main {
public static void main(String[] args) {
Weapon w = new Weapon();
}
}
このコードは結果として以下のコンパイルエラーが発生する
.\Weapon.java:3: エラー: Itemに適切なコンストラクタが見つかりません(引数がありません)
super();
^
コンストラクタ Item.Item(String)は使用できません
(実引数リストと仮引数リストの長さが異なります)
コンストラクタ Item.Item(String,int)は使用できません
(実引数リストと仮引数リストの長さが異なります)
エラー1個
このコードの問題点はWeaponクラスがItemクラスを継承して宣言されているが、宣言の中にコンストラクタがない。そのため、コンパイラがWeaponクラスにコンストラクタとして以下のコードを追加した。
public Weapon() {
super();
}
しかし、MainクラスでWeaponのオブジェクトを生成するコードが上記のコードで引数なしの親クラス(Item)のコンストラクタを呼び出したことで、Itemのコンストラクタには引数ありのコンストラクタしかなく、必要な引数が渡されていないためにコンパイルエラーを起こすことが要因となっている。
このようにsuper()が記述されず自動的に実行されるとコンパイルエラーの元となることがあるので、今回の例で言えば以下のようにsuperを記述し、親クラスのコンストラクタに合わせて引数を渡す方がコードとして安全といえる。
// Weapon.java
public class Weapon extends Item {
public Weapon() {
super("ななしの剣");
}
}
継承で留意すべき点
-
親クラスの多重継承はjavaでは禁止されている
-
classを宣言するときfinalを付加するとそのクラスは継承できなくなる
-
クラスを継承させるにしても一部メソッドのオーバーライドを禁止したいとき、その宣言にfinalを付加することで、そのメソッドは子クラスでオーバーライドできなくなる
is-aの原則(is-aの関係)
オブジェクト指向でコードを記述していくときに遵守すべきルールのようなもので概念的説明。親クラスは広義的なもの(例:車)であるべきで、子クラスは狭義的(例:バス、乗用車、トラック)なものであるべきということを記述してあると思われる。だからこそ、クラス図は数学的な表記と同じで矢印が子クラスから親クラスへ一方向に伸びていると思われる。
復習...has-aの関係
- あるクラスが別のクラスをフィールドとして利用している関係のこと
- クラス図では矢印でなく、◇-のようにひし形で表現する
これによって、子クラスになるほど特化していき、親クラスになるほど汎化していく。