【Java】继承(Inheritance)的意思是通过创建一个新类,从现有的类中继承属性和方法
继承的意思是
-
- 元になるクラスのメンバーを引き継ぎ、新たなクラスを定義すること
基底クラス(スーパークラス、親クラス):継承元
派生クラス(サブクラス、子クラス):継承してできたクラス
機能の共通した部分を切り出して差分だけを書く仕組み
cf: 委譲:特定の役割を別のクラスとして切り出しそのインスタンスをフィールドとして保存
继承的用法
-
- class 派生クラス extends 基底クラス{}
-
- extends省略するとObjectクラスを継承したことになる
-
- 現在のクラスで要求されたメンバーを検索し、存在しない場合上位クラスで定義されたメンバーを呼び出す!
基底、派生クラス両方にあるメソッドは派生クラスを優先して呼ぶ
注意:
Javaでは基底クラスは1個だけ(多重継承認不可)
基底クラスで定義されたメンバーを派生クラスで削除できない
=派生クラスは基底クラスの全性質を含む
public class Person {
public String name;
public int age;
public String show() {
return String.format("%s(%d歳)です。", this.name, this.age);
}
}
//Personを継承したBusinessPersonクラス作成
public class BusinessPerson extends Person {
//派生クラス独自のworkメソッド定義
public String work() {
return String.format("%d歳の%sは今日も元気に働きます。", this.age, this.name);
}
}
public class InheritBasic {
public static void main(String[] args) {
//BusinessPersonのみ呼び出す
var bp = new BusinessPerson();
bp.name = "佐藤一郎";
bp.age = 30;
//showメソッドがbpクラスのメンバーのように呼べる!
System.out.println(bp.show()); //佐藤一郎(30歳)です。
System.out.println(bp.work()); //30歳の佐藤一郎は今日も元気に働きます。
}
}
-
- ポイント
親クラスと子クラスにis-aの関係があれば継承をつかう
is-a:SubClass is a SuperClass
Person⊃BusinessPerson
BusinessPersonがPersonに全て含まれる関係
派生クラスは基底クラスの特化
基底クラスは派生クラスの汎化
场地的隐藏
-
- 継承ではメンバーによって変更可否が異なる
オーバーライド:メソッドが可能
隠蔽:フィールド、入れ子クラス/インターフェースが可能
隐匿
基底クラスの同名フィールドを派生クラスで定義した場合、基底クラスのフィールドが見えなくなる
データ型が異なっても名前が同じなら隠蔽される
予約変数superを用いることで隠蔽フィールドにアクセス可能
注意:基底クラスのフィールドがprivateではアクセス不可能
import java.time.ZonedDateTime;
public class Person {
public String name;
public ZonedDateTime birth = ZonedDateTime.now();
}
import java.time.LocalDateTime;
public class BusinessPerson extends Person {
//基底クラスのbirthフィールドを隠蔽
public LocalDateTime birth = LocalDateTime.now();
public void show() {
//隠蔽フィールドにアクセス
System.out.println(super.birth);
}
}
public class HideBasic {
public static void main(String[] args) {
var bp = new BusinessPerson();
//BusinessPerson.birthを表示
System.out.println(bp.birth);
bp.show();
//Person.birthフィールドを表示
Person p = new BusinessPerson();
System.out.println(p.birth);
}
}
方法的重写
-
- 基底クラスで定義されたメソッドを派生クラスで上書き(再定義)
-
- 見るのは名前のみではない(隠蔽と異なる)
メソッド名:完全一致
仮引数:データ型/個数一致
基底の(CharSequence s)を派生の(String s)でオーバーライド不可
戻り値:型一致/派生型
アクセス修飾子:一致/基底型より緩い
public>protected>無指定>private
privateはオーバーライド不可
throw句(例外):一致/派生型
→@Overrideアノテーションを使えばOK
基底クラスのメソッドをオーバーライドしていることを明示的に宣言
public class Person {
public String name;
public int age;
public String show() {
return String.format("%s(%d歳)です。", this.name, this.age);
}
}
public class BusinessPerson extends Person {
public BusinessPerson() {}
//基底クラスの同名showメソッドを上書き
@Override
public String show() {
return String.format("会社員の%s(%d歳)です。", this.name, this.age);
}
public String work() {
return String.format("%d歳の%sは今日も元気に働きます。", this.age, this.name);
}
}
基类引用(super)
- superによるメソッド呼び出しは派生クラスのメソッド定義の先頭
//BusinessPersonクラスを継承
public class EliteBusinessPerson extends BusinessPerson {
@Override
//基底クラスのworkメソッドを呼び出し、独自の処理を追加する
public String work() {
//派生クラスの先頭でsuperメソッド呼び出し
var result = super.work();
return String.format("%sいつでも笑顔で!", result);
}
}
public class InheritBaseCall {
public static void main(String[] args) {
var ebp = new EliteBusinessPerson();
ebp.name = "山田太郎";
ebp.age = 35;
System.out.println(ebp.work()); //35歳の山田太郎は今日も元気に働きます。いつでも笑顔で!
}
}
派生类的构造函数
-
- コンストラクターはメソッドと同じようには引き継がれない
-
- 継承関係にあるクラスでは上位クラスから順にコンストラクタが呼ばれ、最終的に現在のクラスのコンストラクタが呼ばれる
基本クラスの初期化は基本クラスのコンストラクタ
派生クラスの初期化は派生クラスのコンストラクタ
1.全フィールドを規定値で初期化
2.基底クラスの初期化ブロック実行
3.基底クラスのコンストラクタ実行
4.派生クラスの初期化ブロック実行
5.派生クラスのコンストラクタ実行
public class MyParent {
public MyParent() {
System.out.println("親です。");
}
}
public class MyChild extends MyParent {
public MyChild() {
System.out.println("子です。");
}
}
public class InheritConstruct {
public static void main(String[] args) {
var c = new MyChild(); //親です。 子です。
}
}
-
- 上位クラスで暗黙的に呼ばれるのは引数なしコンストラクタのみ
明示的にコンストラクタを定義(=引数付き)した場合、デフォルトコンストラクタは自動生成されない
→引数付きコンストラクタを、派生クラスのコンストラクタからsuperで呼ぶ
必ずコンストラクタの先頭文で呼ぶ
//上位クラス(引数付きコンストラクタ)
public class MyParent {
public MyParent(String name) {
System.out.printf("%sの親です。\n", name);
}
}
public class MyChild extends MyParent {
public MyChild(String name) {
//派生クラスのコンストラクタから上位クラスの引数なしコンストラクタを呼ぶ
super(name);
//基底クラス→派生クラスの順でコンストラクタが呼ばれる
System.out.printf("子の%sです。\n", name); //山田太郎の親です。\n山田太郎の子です。
}
}
public class InheritConstruct {
public static void main(String[] args) {
var c = new MyChild("山田太郎");
}
}
禁止继承或覆盖
-
- 継承可能クラスの実装や修正は派生クラスへの影響も考慮するべき
- 継承/オーバーライドを想定しないクラス/メソッドの場合、final修飾子で禁止する
public class Person {
String name;
int age;
public Person() {}
//showメソッドをオーバーライド禁止
public final String show() {
return String.format("%s(%d歳)です。", this.name, this.age);
}
}
//BusinessPersonクラスを継承禁止
public final class BusinessPerson extends Person {
public String intro() {
return "会社員です。";
}
}
参考类型的类型转换
- 型同士が継承/実装の関係にあること
提升转换
派生クラスから基底クラスへの変換のこと
BusinessPerson型をPerson型に変換
派生クラスは基底クラスのメンバを全部含んでいる
派生クラスのインスタンスは基底クラスのインスタンスとして利用可能
この場合変換の型はPerson、オブジェクトの型はBusinessPerson(後述の 変数の型/オブジェクトの型 参考)
//アップキャスト
public class CastUp {
public static void main(String[] args) {
//BusinessPersonオブジェクトを基底クラスPerson型の変数に代入して変換
Person bp = new BusinessPerson();
bp.name = "山田太郎";
bp.age = 20;
System.out.println(bp.show());
}
}
请将「ダウンキャスト」翻译成中文。
基底クラスから派生クラスへの変換のこと
Person型をBusinessPerson型に変換
基底クラスは常に派生クラスとして振る舞えない
キャスト構文で明示的に型変換が必要
Person p = new BusinessPerson();
BusinessPerson bp = (BusinessPerson)p;
变量的类型/对象的类型
-
- PersonクラスのshowメソッドをBusinessPersonでオーバーライド
-
- しかし以下コードではBusinessPersonクラスのworkメソッドを呼べない
変数pはPerson型の変数
変数pがworkメソッドを呼び出せない
変数pのオブジェクトの型(実体)はBusinessPerson
BusinessPersonのshowメソッドは呼び出される
public class BusinessPerson extends Person {
public BusinessPerson() {}
@Override
public String show() {
return String.format("会社員の%s(%d歳)です。", this.name, this.age);
}
public String work() {
return String.format("%d歳の%sは、働きます。", this.age, this.name);
}
}
public class TypeDifference {
public static void main(String[] args) {
Person p = new BusinessPerson();
p.name = "山田太郎";
p.age = 30;
// System.out.println(p.work()); //エラー
System.out.println(p.show()); //会社員の山田太郎(30歳)です。
}
}
-
- つまり、、、
キャストは変数の型を差し替える
BusinessPerson変数型をPerson変数型に変換
オブジェクトの型(実体)はキャストしても変化しない
呼び出されるメソッドはオブジェクトの型が決める
PersonではBusinessPersonの振る舞いができない
类方法/字段的隐藏
-
- クラスメソッドはクラス名.メソッド名(…)で呼ぶ
変数の型/オブジェクトの型が存在しない
常に指定クラスのメソッドが呼ばれるのでオーバーライド概念がない
フィールドの選択は変数の型で決まる
public class HideBasic {
public static void main(String[] args) {
var bp = new BusinessPerson();
System.out.println(bp.birth);
bp.show();
//変数pはPerson型
Person p = new BusinessPerson();
//PersonクラスのbirthフィールドはZonedDateTime型
System.out.println(p.birth); //ZonedDateTime型
}
}
判断形状
-
- ダウンキャスト時にはオブジェクトの型チェックが必要
例えばこんなことが。。。
BusinessPerson/StudentはPerson派生クラスである
Person p = new BusinessPerson(); //OK
BusinessPerson bp = (BusinessPerson)p; //OK
Student st = (Student)p; //NG
変数pの実体(オブジェクト型)はBusinessPerson
コンパイル時はPersonとBusinessPerson/Student継承関係しか分からないのでコンパイルエラーにならないが、実行時にエラーになる!
→instanceof演算子を使う
変数に格納されたオブジェクト型が指定型に変換できる場合true
//型チェック
if(p instanceof Student){
Student st = (Student)p;
//正しくキャストできたときの処理
}
获得类型
getClassメソッドでオブジェクトの型を取得
変数の型によらずオブジェクトの型を取得
public class Main {
public static void main(String[] args) {
Person p1 = new Person();
System.out.println(p1.getClass()); //class Person
Person p2 = new BusinessPerson();
System.out.println(p2.getClass()); //class BusinessPerson
}
}
让渡
-
- 継承は基底/派生クラスが密結合
-
- 継承を使うタイミング
基底/派生クラスがis-a関係
リフコフの置換原則:基底クラスの変数にその派生クラスのインスタンスを代入してもコードの妥当性が損なわれない
拡張を前提とし、その旨を文書化している
継承が不適な例
import java.util.Random;
public class Roulette extends Random {
//ルーレット上限
private int bound;
public Roulette(int bound) {
this.bound = bound;
}
//boundフィールド上限とする値を取得し乱数生成
@Override
public int nextInt() {
return nextInt(this.bound);
}
//他の不要メソッドは無効化している(リフコフの置換原則に反する)
@Override
public boolean nextBoolean() {
throw new UnsupportedOperationException();
}
@Override
public long nextLong() {
throw new UnsupportedOperationException();
}
}
import java.util.Random;
public class RouletteClient {
public static void main(String[] args) {
Random rou = new Roulette(10);
System.out.println(rou.nextBoolean()); //UnsupportedOperationException
}
}
委譲では再利用したい機能をもつオブジェクトを現クラスのフィールドとして取り込む
委譲はインスタンス同士の動的な関係である
RouletteクラスのrandomフィールドにRandomオブジェクトを保持(has)し必要に応じてRandomクラスのメソッドを利用できる
has-a関係という
メリット:
クラス同士の関係が緩くなる
publicメンバーを利用するので内部実装に左右しない
フィールドでインスタンス保持するので関係が固定しない
後から委譲先変更可能
複数クラスに処理委譲可能
インスタンス単位で委譲先変更可能
import java.util.Random;
public class Roulette {
private int bound;
//以上先のオブジェクトをフィールドに保持
private Random random = new Random();
public Roulette(int bound) {
this.bound = bound;
}
//必要に応じて処理を委譲
public int nextInt() {
return this.random.nextInt(this.bound);
}
}