要約:
この記事では、オブジェクト指向継承とJavaクラスの再利用に基づくポリモーフィズムの2つの主要な機能について包括的に紹介します。まず、継承の本質と重要性を紹介し、クラスの再利用という観点から、継承、合成、プロキシの類似点と相違点を探りました。次に、継承に基づくポリモーフィズムを導入し、その実装メカニズムと具体的なアプリケーションを紹介しました。さらに、継承とポリモーフィズムをより深く理解するために、final キーワードについて包括的に紹介しました。これに基づいて、Java でのクラスのロードと初期化シーケンスを導入しました。最後に、オブジェクト指向設計における 3 つの非常に重要な概念、オーバーロード、カバーと隠蔽について詳しく説明しました。
キーポイント:
継承
構成、継承、プロキシ
ポリモーフィズム
最後のキーワード
クラスロードと初期化シーケンス
リロード、カバーリング、
継承はすべての OOP 言語に不可欠な部分であり、Java では、extends キーワード が継承関係を表現するために使用されます。 クラスを作成するときは、継承元のクラスを明示的に指定しない場合、常にルート クラス Object から暗黙的に継承されます。 2 つのクラス間に継承関係がある場合、サブクラスは親クラスのメソッドと変数を自動的に継承し、親クラスのメソッドと変数をサブクラスで直接呼び出すことができます。 Java では、単一継承のみが許可される、つまり、クラスは最大でも 1 つの親クラスからのみ明示的に継承できることに注意してください。ただし、クラスは複数のクラスによって継承できます。つまり、クラスは複数のサブクラスを持つことができます。 さらに、以下の点に特に注意する必要があります: 1.
メンバ変数の継承 サブクラスがあるクラスを継承する場合、親クラスのメンバ変数は使用できますが、それは使用できません。すべてのメンバー変数を完全に継承します。具体的な原則は次のとおりです:
サブクラスは親クラスの public および protected メンバー変数を継承できますが、 は親クラスの private メンバー変数を継承できません。ただし、親クラスの プライベート メンバー変数を渡すことはできます。クラスの対応する getter/setter メソッドは、親クラス のパッケージ アクセス許可メンバー変数にアクセスするために使用されます。 ,
;サブクラスが継承できる親クラスのメンバー変数については、メンバーの場合同じ名前の変数がサブクラスに出現すると、隠れ
キーワードを使用してreferenceを行う必要があります。 2. メンバーメソッドの継承 同様に、サブクラスが特定のクラスを継承する場合、親クラスのメンバーメソッドを使用できますが、サブクラスは親クラスのすべてを完全に継承するわけではありません。方法。具体的な原則は次のとおりです:
サブクラスは親クラスの public および protected メンバー メソッド
private メンバー メソッド を継承できません。 class; 親クラス のパッケージ アクセス メンバー メソッドの場合、サブクラスと親クラスが同じパッケージ内にある場合、サブクラスは継承できます。それ以外の場合、サブクラスは継承できません。
サブクラスが継承できる親クラスのメンバーメソッドについて、サブクラス内に同名のメンバーメソッドが出現した場合、上書き、つまりサブクラスのメンバーメソッドが上書きすることになります。同じ名前のクラスのメンバー メソッド。親クラスの同名のメンバメソッドにサブクラスでアクセスしたい場合は、superキーワードを使用して参照する必要があります。
プログラム例:
class Person { public String gentle = "Father"; }public class Student extends Person { public String gentle = "Son"; public String print(){ return super.gentle; // 在子类中访问父类中同名成员变 } public static void main(String[] args) throws ClassNotFoundException { Student student = new Student(); System.out.println("##### " + student.gentle); Person p = student; System.out.println("***** " + p.gentle); //隐藏:编译时决定,不会发生多态 System.out.println("----- " + student.print()); System.out.println("----- " + p.print()); //Error:Person 中未定义该方法 } }/* Output: ##### Son ***** Father ----- Father *///:~
非表示と上書きは違います。 非表示 はメンバー変数と static メソッド の場合は ですが、 オーバーライド は通常のメソッド の場合は です。
3. 基本クラスの初期化とコンストラクター
エクスポートされたクラスは、基本クラスと同じ インターフェース を持つ新しいクラスのようなものであり、追加のメソッドとフィールドがある可能性があることがわかっています。 ただし、継承は基本クラスのインターフェースをコピーするだけではありません。 エクスポートされたクラス オブジェクトを作成すると、そのオブジェクトには基本クラスの子オブジェクトが含まれます。 このサブオブジェクトは、基本クラスを使用して直接作成したオブジェクトと同じです。 2 つの違いは、後者は外部から取得されるのに対し、基本クラスのサブオブジェクトは派生クラス オブジェクトの内部にラップされることです。
したがって、 基本クラスのサブオブジェクトの正しい初期化が重要であり、Java はこれを保証するための対応するメソッドも提供します: 派生クラスは、初期化を実行するためにコンストラクター内で基本クラスのコンストラクターを呼び出す必要があります 、そして基本クラスのコンストラクターは、基本クラスの初期化を実行するために必要なすべての知識と機能を備えています。 基底クラスにデフォルト コンストラクターが含まれている場合、コンパイラーはどのパラメーターを渡すかを考慮する必要がないため、Java は派生クラスのコンストラクターに基底クラスのデフォルト コンストラクターへの呼び出しを自動的に挿入します。 ただし、親クラスにデフォルトのコンストラクターが含まれていない場合、または派生クラスがパラメーターを使用して親クラスのコンストラクターを呼び出したい場合は、 super キーワードを使用して、派生クラスのコンストラクター内の対応する基本クラスを明示的に呼び出す必要があります。 のコンストラクターと呼び出しステートメントは、派生クラス コンストラクターの最初のステートメントである必要があります。
Java では、組み合わせ、継承、プロキシはすべてコードの再利用を実現できます。
(1) 組み合わせ(has-a)
合成は、既存のクラスのオブジェクトを新しいクラスに追加することで実現できます。 つまり、新しいクラスは既存のクラスのオブジェクトで構成されます。 この手法は通常、インターフェイスではなく、既存のクラスの機能を新しいクラスで使用したい場合に使用されます。 言い換えると、必要な機能を実現できるようにオブジェクトを新しいクラスに埋め込みますが、新しいクラスのユーザーには、埋め込まれたオブジェクトのインターフェースではなく、新しいクラスに対して定義されたインターフェースのみが表示されます。
(2) 継承(is-a)
継承を使用すると、既存のクラスの型に基づいて新しいクラスを作成できます。 つまり、既存のクラスの形式をとり、そこに新しいコードを追加します。通常、これは、一般的なクラスを受講し、それを特別なニーズに特化することを意味します。 本質的に、合成と継承の両方で、新しいクラスにサブオブジェクトを配置できます。合成ではこれを明示的に行い、継承では暗黙的に行います。
(3) プロキシ(継承と合成の黄金比:合成のように既存クラスの機能を使い、継承のように既存クラスのインターフェースを使う)
プロキシJava はそれを直接サポートしていません。プロキシでは、 構築中のクラスにメンバー オブジェクトを配置します (合成など) が、同時にそのメンバー オブジェクトのインターフェイス/メソッドを新しいクラスで公開します (継承など)。
プログラム例:
// 控制模块public class SpaceShipControls { void up(int velocity) { } void down(int velocity) { } void left(int velocity) { } void right(int velocity) { } void forward(int velocity) { } void back(int velocity) { } void turboBoost() { } }
宇宙船には制御モジュールが必要です、そして、 宇宙船を構築する1つの方法は、継承を使用することです:
public class SpaceShip extends SpaceShipControls { private String name; public SpaceShip(String name) { this.name = name; } public String toString() { return name; } public static void main(String[] args) { SpaceShip protector = new SpaceShip("NSEA Protector"); protector.forward(100); } }
然而,SpaceShip 并不是真正的 SpaceShipControls 类型,即便你可以“告诉” SpaceShip 向前运动(forward())。更准确的说,SpaceShip 包含 SpaceShipControls ,与此同时, SpaceShipControls 的所有方法在 SpaceShip 中都暴露出来。 代理(SpaceShip 的运动行为由 SpaceShipControls 代理完成) 正好可以解决这种问题:
// SpaceShip 的行为由 SpaceShipControls 代理完成public class SpaceShipDelegation { private String name; private SpaceShipControls controls = new SpaceShipControls(); public SpaceShipDelegation(String name) { this.name = name; } // 代理方法: public void back(int velocity) { controls.back(velocity); } public void down(int velocity) { controls.down(velocity); } public void forward(int velocity) { controls.forward(velocity); } public void left(int velocity) { controls.left(velocity); } public void right(int velocity) { controls.right(velocity); } public void turboBoost() { controls.turboBoost(); } public void up(int velocity) { controls.up(velocity); } public static void main(String[] args) { SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector"); protector.forward(100); } }
实际上,我们使用代理时可以拥有更多的控制力,因为我们可以选择只提供在成员对象中方法的某个子集。
许多编程语言都需要某种方法来向编译器告知一块数据是恒定不变的。有时,数据的恒定不变是很有用的,比如:
一个永不改变的编译时常量;
一个在运行时被初始化的值,而你不希望它被改变。
对于编译期常量这种情况,编译器可以将该常量值带入任何可能用到它的计算式中,也即是说,可以在编译时执行计算式,这减轻了一些运行时负担。在Java中,这类常量必须满足两个条件:
是基本类型,并且用final修饰;
在对这个常量进行定义的时候,必须对其进行赋值。
此外,当用final修饰对象引用时,final使其引用恒定不变。一旦引用被初始化指向一个对象,就无法再把它指向另一个对象。然而,对象本身是可以被修改的,这同样适用于数组,因为它也是对象。
特别需要注意的是,我们不能因为某数据是final的,就认为在编译时就可以知道它的值。例如:
public class Test { final int i4 = rand.nextInt(20); }
1、空白final
Java允许生成 空白final , 即:声明final但又未给定初值的域。但无论什么情况,编译器都会确保空白final在使用前被初始化。但是,空白final在关键字final的使用上提供了更大的灵活性: 一个类中的 final域 就可以做到根据对象而有所不同,却又保持其恒定不变的特性。例如,
必须在域的定义处或者每个构造器中使用表达式对final进行赋值,这正是 final域 在使用前总是被初始化的原因所在。
2、final参数
final参数 主要应用于局部内部类和匿名内部类中,更多详细介绍请移步我的另一篇文章:Java 内部类综述。
3、final方法
final关键字作用域方法时,用于锁定方法,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。
对于成员方法,只有在明确禁止覆盖时,才将方法设为final的。
4、final类
クラスをfinalとして定義すると、そのクラスを継承するつもりがなく、他の人が継承することを許可しないことを示します。 言い換えると、何らかの理由で、このクラスの設計を変更する必要がないか、セキュリティの理由から、このクラスにサブクラスを持たせたくありません。
final クラスのフィールドは、実際の状況に応じて、final になるように選択できることに注意してください。 Final として定義されているかどうかに関係なく、final として定義されているフィールドには同じルールが適用されます。 ただし、final クラスは継承を禁止しているため、final クラス内のすべてのメソッドはオーバーライドできないため、暗黙的に Final として指定されます。最終クラスのメソッドに最終的な変更を追加できますが、これには何の意味も追加されません。
5. Finalとprivate
クラス内のすべてのプライベートメソッドは暗黙的にfinalとして指定されます。 プライベートメソッドにアクセスできないため、オーバーライドできません。プライベート メソッドに最終的な変更を追加することはできますが、これによってメソッドに追加の意味が追加されるわけではありません。
override は、メソッドが基本クラス インターフェイスの一部である場合にのみ 表示されることに注意することが重要です。メソッドがプライベートである場合、それは基本クラス インターフェイスの一部ではなく、クラス内に隠された単なるプログラム コードです。 ただし、エクスポートされたクラス内に同じ名前で非プライベート メソッドが生成された場合、この時点ではメソッドを上書きせず、新しいメソッドのみを生成します。 プライベート メソッドはアクセスできず、事実上隠すことができるため、それが属するクラスの組織構造によって存在する場合を除き、他の状況では考慮する必要はありません。
6. Final と static
static 変数を変更する場合、その にはデフォルト値 があり、 は変更可能 で、その はメンバー変数とメンバー メソッドのみを変更できます。
A static Final フィールド は、変更できない記憶領域のみを占有し、宣言されたときにのみ初期化できます。 final であるため、デフォルト値はなく、静的であるため、クラスがインスタンス化されていないときに値が割り当てられており、宣言されたときにのみ初期化できます。
継承により、オブジェクトを独自の型またはその基本型として扱うことができるため、上記の違いなしに同じコードをこれらの異なる型で実行できることがわかっています。その中で、ポリモーフィック メソッド呼び出しでは、これらの型が同じ基本クラスから派生している限り、ある型が他の同様の型とは異なる動作をすることができます。 したがって、ポリモーフィズムの役割は主に2つの側面に反映されます:
ポリモーフィズムは、何を行うか、どのように行うかを分離することで、別の観点からインターフェースと実装を分離し、実装が変化します。変更されていないものと変更されていないものを分離する
型間の結合関係を削除する (同様に、Java では、クラスまたはメソッド間の結合関係を削除するためにジェネリックも使用されます)およびタイプ間の使用される結合関係)。
1.実装メカニズム
メソッドのオーバーライドがポリモーフィズムをよく表していることはわかっていますが、基本クラス参照を使用してオーバーライド メソッドを呼び出す場合、どのメソッドを正しく呼び出す必要がありますか?
メソッド呼び出しを同じメソッド本体に関連付けることをバインディングと呼びます。プログラムの実行前にバインディングが実行される場合、それは早期バインディングと呼ばれます。ただし、コンパイル時にコンパイラは上記の基本クラス参照がどのオブジェクトを指しているのかを認識できないため、このメカニズムでは上記の問題を解決できないことは明らかです。解決策は遅延バインディング (動的バインディング/実行時バインディング): 実行時にオブジェクトの特定の型に従ってバインディングすることです。
実際、Java では、静的メソッドと最終メソッド (プライベート メソッドは最終メソッドです) を除き、他のすべてのメソッドは遅延バインディングです。 このようにして、メソッドが Final 宣言された後、他の人がそのメソッドをオーバーライドするのを防ぐことができますが、より重要なのは、そうすることで動的バインディングを効果的にオフにすることができる、つまり、動的バインディングを無効にする必要がないことをコンパイラーに伝えることができるということです。動的にバインドされ、最終的なメソッド呼び出しのより効率的なコードを生成します。
動的バインディング メカニズムに基づいて、基本クラスのみを処理するコードを作成でき、これらのコードはエクスポートされたすべてのクラスに対して正しく実行できます。 言い換えると、オブジェクトにメッセージを送信し、何をするかをオブジェクトに決定させます。
2. ダウンキャストと実行時型識別
上方変換では特定の型情報が失われるため、下方変換によっても型情報を取得できるはずだと考えるかもしれません。ただし、基本クラスには派生クラスより大きなインターフェイスがないため、アップキャストは安全であることがわかっています。したがって、基本クラス インターフェイスを通じて送信するメッセージは受け入れることができますが、下方変換については保証できません。
この問題を解決するには、間違った型に性急にキャストして、オブジェクトが受け入れられないメッセージを送信しないように、下向きキャストの正確さを保証する何らかの方法が必要です。 Java では、Runtime Type Identification (RTTI) メカニズムがこの問題に対処でき、Java でのすべての変換が確実にチェックされます。したがって、括弧で囲まれた通常の 型変換 を実行するだけであっても、ランタイムに入ると、それが実際に必要な型であるかどうかがチェックされます。そうでない場合は、型変換例外 ClassCastException が発生します。
3. ポリモーフィックアプリケーションの例
親クラスの静的コード ブロック -> 親クラスの非静的コード ブロック ->クラス コンストラクター ->サブクラスの非静的コード ブロック->サブクラス コンストラクター つまり、まず、プログラム内に出現する順序で親クラスの静的メンバー変数と静的コード ブロックを初期化します。 、サブクラスを初期化します。静的メンバー変数と静的コード ブロックは、プログラム内で出現する順序で初期化されます。次に、親クラスの通常のメンバー変数とコード ブロックを初期化し、親クラスのコンストラクター メソッド
を実行します。 ; 最後に、サブクラスの通常のメンバー変数とコード ブロックを初期化し、サブクラスのコンストラクター メソッドを実行します。class SuperClass { private static String STR = "Super Class Static Variable"; static { System.out.println("Super Class Static Block:" + STR); } public SuperClass() { System.out.println("Super Class Constructor Method"); } { System.out.println("Super Class Block"); } }public class ObjectInit extends SuperClass { private static String STR = "Class Static Variable"; static { System.out.println("Class Static Block:" + STR); } public ObjectInit() { System.out.println("Constructor Method"); } { System.out.println("Class Block"); } public static void main(String[] args) { @SuppressWarnings("unused") ObjectInit a = new ObjectInit(); } }/* Output: Super Class Static Block:Super Class Static Variable Class Static Block:Class Static Variable Super Class Block Super Class Constructor Method Class Block Constructor Method *///:~
在运行该程序时,所发生的第一件事就是试图访问 ObjectInit.main() 方法(一个static方法),于是加载器开始启动并加载 ObjectInit类 。在对其加载时,编译器注意到它有一个基类(这由关键字extends得知),于是先进行加载其基类。如果该基类还有其自身的基类,那么先加载这个父父基类,如此类推(本例中是先加载 Object类 ,再加载 SuperClass类 ,最后加载 ObjectInit类 )。接下来,根基类中的 static域 和 static代码块 会被执行,然后是下一个导出类,以此类推这种方式很重要,因为导出类的static初始化可能会依赖于基类成员能否被正确初始化。到此为止,所有的类都已加载完毕,对象就可以创建了。首先,初始化根基类所有的普通成员变量和代码块,然后执行根基类构造器以便创建一个基对象,然后是下一个导出类,依次类推,直到初始化完成。
1、重载与覆盖
(1) 定义与区别
重载:如果在一个类中定义了多个同名的方法,但它们有不同的参数(包含三方面:参数个数、参数类型和参数顺序),则称为方法的重载。其中,不能通过访问权限、返回类型和抛出异常进行重载。
覆盖:子类中定义的某个方法与其父类中某个方法具有相同的方法签名(包含相同的名称和参数列表),则称为方法的覆盖。子类对象使用这个方法时,将调用该方法在子类中的定义,对它而言,父类中该方法的定义被屏蔽了。
总的来说,重载和覆盖是Java多态性的不同表现。前者是一个类中多态性的一种表现,后者是父类与子类之间多态性的一种表现。
(2) 实现机制
重载是一种参数多态机制,即通过方法参数的差异实现多态机制。并且,其属于一种 静态绑定机制,在编译时已经知道具体执行哪个方法。
覆盖是一种动态绑定的多态机制。即,在父类与子类中具有相同签名的方法具有不同的具体实现,至于最终执行哪个方法 根据运行时的实际情况而定。
(3) 总结
我们应该注意以下几点:
final 方法不能被覆盖;
子类不能覆盖父类的private方法,否则,只是在子类中定义了一个与父类重名的全新的方法,而不会有任何覆盖效果。
其他需要注意的地方如下图所示:
2、覆盖与隐藏
(1) 定义
覆盖:指 运行时系统调用当前对象引用 运行时类型 中定义的方法 ,属于 运行期绑定。
隐藏:指运行时系统调用当前对象引用 编译时类型 中定义的方法,即 被声明或者转换为什么类型就调用对应类型中的方法或变量,属于编译期绑定。
(2) 范围
オーバーライド: インスタンスメソッドのみ
Hide: 静的メソッドとメンバー変数のみ。
(3) 概要
同様に、サブクラスのインスタンス メソッドは親クラスの静的メソッドをオーバーライドできません。オーバーライドしないと、
エラーが発生します。静的メンバーまたはインスタンス メンバーの、サブクラス内の同じ名前のメンバー変数によって非表示にすることができます。
次のプログラム例は、オーバーロード、オーバーライド、および隠蔽の 3 つの概念を説明しています。
以上がJava 継承、ポリモーフィズム、クラス再利用の詳細な紹介とコード例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。