サブタイプ多型の背後にある魔法を明らかにする

ポリモーフィズムという言葉は、ギリシャ語で「多くの形」を意味します。ほとんどのJava開発者は、この用語を、プログラムの適切なポイントで正しいメソッド動作を魔法のように実行するオブジェクトの機能と関連付けています。ただし、その実装指向のビューは、基本的な概念の理解ではなく、魔法のイメージにつながります。

Javaのポリモーフィズムは、常にサブタイプのポリモーフィズムです。その多様なポリモーフィックな振る舞いを生成するメカニズムを綿密に調べるには、通常の実装上の懸念を破棄し、タイプの観点から考える必要があります。この記事では、オブジェクトの種類志向の視点を調査し、どのようにその視点が分離どんなオブジェクトから表現できる行動どのようにオブジェクトが実際にその行動を表現しています。ポリモーフィズムの概念を実装階層から解放することで、Javaインターフェイスが実装コードをまったく共有しないオブジェクトのグループ間でポリモーフィズムの動作を促進する方法もわかります。

クアトロ多型

ポリモーフィズムは、幅広いオブジェクト指向の用語です。通常、一般的な概念をサブタイプの多様性と同一視しますが、実際には4種類のポリモーフィズムがあります。サブタイプのポリモーフィズムを詳細に検討する前に、次のセクションでは、オブジェクト指向言語でのポリモーフィズムの一般的な概要を示します。

「タイプ、データ抽象化、およびポリモーフィズムの理解について」の著者であるLucaCardelliとPeterWegner(記事へのリンクについては「参考文献」を参照)は、ポリモーフィズムを2つの主要なカテゴリ(アドホックとユニバーサル)と4つの種類(強制、オーバーロード、パラメトリック、および包含。分類構造は次のとおりです。

|-強制|-アドホック-| |-ポリモーフィズムのオーバーロード-| |-パラメトリック|-ユニバーサル-| |-包含

その一般的なスキームでは、ポリモーフィズムは、複数の形式を持つエンティティの能力を表します。ユニバーサルポリモーフィズムとは、タイプ構造の均一性を指し、ポリモーフィズムは、共通の特徴を持つ無限の数のタイプに対して作用します。構造化されていないアドホック多相性は、有限数のおそらく無関係なタイプに対して作用します。4つの品種は次のように説明できます。

  • 強制:単一の抽象化は、暗黙的な型変換を通じて複数の型に対応します
  • オーバーロード:単一の識別子は複数の抽象化を示します
  • パラメトリック:抽象化はさまざまなタイプにわたって均一に機能します
  • 包含:抽象化は包含関係を通じて機能します

特にサブタイプの多型に目を向ける前に、各品種について簡単に説明します。

強制

強制は、メソッドまたは演算子によって予期される型への暗黙的なパラメーター型変換を表し、それによって型エラーを回避します。次の式の場合、コンパイラは+、オペランドのタイプに適切な二項演算子が存在するかどうかを判断する必要があります。

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

最初の式は2つのdoubleオペランドを追加します。Java言語は、そのような演算子を具体的に定義しています。

ただし、2番目の式はadoubleint;を追加します。Javaは、これらのオペランドタイプを受け入れる演算子を定義していません。幸い、コンパイラは2番目のオペランドを暗黙的に変換し、2doubleつのdoubleオペランドに対して定義された演算子を使用します。これは開発者にとって非常に便利です。暗黙的な変換がないと、コンパイル時エラーが発生するか、プログラマーが明示的ににをキャストするint必要がありdoubleます。

3番目の式は、doubleとを追加しますString。繰り返しますが、Java言語はそのような演算子を定義していません。したがって、コンパイラはdoubleオペランドを強制的に強制しString、plus演算子は文字列の連結を実行します。

強制は、メソッドの呼び出しでも発生します。クラスDerivedがクラスを拡張し、クラスBaseC署名付きのメソッドがあるとしますm(Base)。以下のコードにおけるメソッド呼び出しのために、コンパイラは、暗黙的に変換するderived型を持つ参照変数を、Derivedに、Baseメソッドシグネチャによって規定タイプ。この暗黙的な変換により、m(Base)メソッドの実装コードは、次のように定義された型演算のみを使用できますBase

C c =新しいC(); 派生派生=新しいDerived(); cm(派生);

この場合も、メソッド呼び出し中の暗黙的な強制により、面倒な型キャストや不要なコンパイル時エラーが回避されます。もちろん、コンパイラーは、すべての型変換が定義された型階層に準拠していることを確認します。

オーバーロード

オーバーロードにより、同じ演算子またはメソッド名を使用して、複数の異なるプログラムの意味を示すことができます。+追加のための1つ:前のセクションで使用される演算子は、2つの形式示しdoubleオペランドを連結するための1つStringのオブジェクトを。 2つの整数、2つのlongなどを加算するための他の形式が存在します。演算子をオーバーロードと呼び、プログラムのコンテキストに基づいて適切な機能を選択するためにコンパイラーに依存します。前述のように、必要に応じて、コンパイラは、演算子の正確な署名に一致するようにオペランドタイプを暗黙的に変換します。 Javaは特定のオーバーロードされた演算子を指定していますが、ユーザー定義のオペレーターのオーバーロードはサポートしていません。

Javaは、メソッド名のユーザー定義のオーバーロードを許可します。メソッドのシグネチャが異なる場合、クラスは同じ名前の複数のメソッドを所有できます。つまり、パラメーターの数が異なるか、少なくとも1つのパラメーター位置のタイプが異なる必要があります。一意のシグニチャにより、コンパイラは同じ名前のメソッドを区別できます。コンパイラは、一意のシグネチャを使用してメソッド名をマングルし、効果的に一意の名前を作成します。これに照らして、見かけの多形性の振る舞いは、詳しく調べると蒸発します。

強制とオーバーロードはどちらも、限られた意味でのみポリモーフィックな動作を提供するため、アドホックとして分類されます。それらはポリモーフィズムの広い定義に該当しますが、これらの種類は主に開発者の便宜です。強制により、面倒な明示的な型キャストや不要なコンパイラ型エラーが回避されます。一方、オーバーロードは構文糖衣構文を提供し、開発者が異なるメソッドに同じ名前を使用できるようにします。

パラメトリック

パラメトリック多態性により、多くのタイプにわたって単一の抽象化を使用できます。たとえばList、同種のオブジェクトのリストを表す抽象化は、汎用モジュールとして提供できます。リストに含まれるオブジェクトのタイプを指定することにより、抽象化を再利用します。パラメータ化された型は任意のユーザー定義のデータ型である可能性があるため、一般的な抽象化には潜在的に無限の用途があり、これは間違いなく最も強力な型のポリモーフィズムになります。

一見すると、上記のList抽象化はクラスのユーティリティのように見えるかもしれませんjava.util.List。ただし、Javaは、タイプセーフな方法で真のパラメトリックポリモーフィズムをサポートしていません。そのためjava.util.List、およびjava.utilの他のコレクションクラスは、基本的なJavaクラスで記述されていjava.lang.Objectます。(詳細については、私の記事「A Primordial Interface?」を参照してください。)Javaのシングルルート実装継承は、部分的な解決策を提供しますが、パラメトリック多態性の真の力は提供しません。Eric Allenの優れた記事「パラメトリックポリモーフィズムの力を見よ」では、Javaでのジェネリック型の必要性と、SunのJava Specification Request#000014「AddGenericTypes to theJavaProgrammingLanguage」に対処するための提案について説明しています。(リンクについては、「参考文献」を参照してください。)

インクルージョン

包含ポリモーフィズムは、タイプまたは値のセット間の包含関係を通じてポリモーフィックな動作を実現します。Javaを含む多くのオブジェクト指向言語では、包含関係はサブタイプ関係です。したがって、Javaでは、包含ポリモーフィズムはサブタイプポリモーフィズムです。

前述のように、Java開発者が一般的にポリモーフィズムを指す場合、それらは常にサブタイプのポリモーフィズムを意味します。サブタイプのポリモーフィズムの力をしっかりと理解するには、タイプ指向の観点からポリモーフィズムの動作を生み出すメカニズムを確認する必要があります。この記事の残りの部分では、その観点を詳しく調べます。簡潔さと明確さのために、私はサブタイプの多型を意味するために多型という用語を使用します。

タイプ指向のビュー

図1のUMLクラス図は、ポリモーフィズムのメカニズムを説明するために使用される単純な型とクラス階層を示しています。このモデルは、5つのタイプ、4つのクラス、および1つのインターフェースを示しています。モデルはクラス図と呼ばれていますが、タイプ図だと思います。「ThanksTypeand Gentle Class」で詳しく説明されているように、すべてのJavaクラスとインターフェースはユーザー定義のデータ型を宣言します。したがって、実装に依存しないビュー(つまり、タイプ指向のビュー)から、図の5つの長方形のそれぞれがタイプを表します。実装の観点から、これらのタイプのうち4つはクラス構造を使用して定義され、1つはインターフェースを使用して定義されます。

次のコードは、各ユーザー定義データ型を定義および実装します。私は意図的に実装をできるだけ単純に保ちます:

/ * Base.java * / public class Base {public String m1(){return "Base.m1()"; } public String m2(String s){return "Base.m2(" + s + ")"; }} / * IType.java * / interface IType {String m2(String s);文字列m3(); } / * Derived.java * / public class Derived extends Baseimplements IType {public String m1(){return "Derived.m1()"; } public String m3(){return "Derived.m3()"; }} / * Derived2.java * / public class Derived2 extends Derived {public String m2(String s){return "Derived2.m2(" + s + ")"; } public String m4(){return "Derived2.m4()"; }} / * Separate.java * / publicclassSeparateはITypeを実装します{publicString m1(){return "Separate.m1()"; } public String m2(String s){return "Separate.m2(" + s + ")";} public String m3(){return "Separate.m3()"; }}

これらの型宣言とクラス定義を使用して、図2はJavaステートメントの概念図を示しています。

Derived2派生2 =新しいDerived2(); 

上記のステートメントは、明示的に型指定された参照変数、を宣言し、derived2その参照を新しく作成されたDerived2クラスオブジェクトにアタッチします。図2の上部パネルは、Derived2参照を一連の舷窓として示していますDerived2。この舷窓を通して、下にあるオブジェクトを表示できます。Derived2タイプ操作ごとに1つの穴があります。実際のDerived2オブジェクトDerived2は、上記のコードで定義された実装階層で規定されているように、各操作を適切な実装コードにマップします。たとえば、Derived2オブジェクトm1()はクラスで定義された実装コードにマップされますDerived。さらに、その実装コードm1()はクラスのメソッドをオーバーライドしますBaseDerived2参照変数はオーバーライドさにアクセスすることはできませんm1()クラスでの実装Base。これは、クラス内の実際の実装コードがを介しDerivedBaseクラス実装を使用できないことを意味するものではありませんsuper.m1()。しかし、参照変数derived2に関する限り、そのコードにはアクセスできません。他のDerived2操作のマッピングも同様に、各タイプの操作に対して実行される実装コードを示しています。

今あなたが持っているDerived2オブジェクトを、あなたは準拠をタイプすることを任意の変数とそれを参照することができますDerived2。図1のUMLダイアグラムの型階層はそれを明らかにしDerivedBase、とITypeのすべてのスーパータイプですDerived2。したがって、たとえば、Base参照をオブジェクトにアタッチできます。図3は、次のJavaステートメントの概念図を示しています。

ベースベース=派生2; 

根本的には変更絶対にありませんDerived2方法ものの、オブジェクトや操作のマッピングのいずれかが、m3()そしてm4()もはやからアクセスできますBase参照。変数または変数を呼び出すm1()m2(String)使用すると、同じ実装コードが実行されます。derived2base

文字列tmp; // Derived2リファレンス(図2)tmp = Delivered2.m1(); // tmpは "Derived.m1()" tmp = Delivered2.m2( "Hello"); // tmpは "Derived2.m2(Hello)" //ベースリファレンス(図3)tmp = base.m1(); // tmpは "Derived.m1()" tmp = base.m2( "Hello"); // tmpは「Derived2.m2(Hello)」です

Derived2オブジェクトは各メソッドを呼び出すものを知らないため、両方の参照を通じて同一の動作を実現することは理にかなっています。オブジェクトは、呼び出されたときに、実装階層によって定義された行進順序に従うことだけを知っています。これらの注文は、この方法のためにその規定m1()Derived2オブジェクトは、クラス内のコードを実行Derivedし、方法にm2(String)、それはクラスのコードを実行しますDerived2。基になるオブジェクトによって実行されるアクションは、参照変数のタイプに依存しません。

ただし、参照変数derived2とを使用する場合、すべてが等しくなるわけではありませんbase。図3に示すように、Base型参照Baseは、基になるオブジェクトの型操作のみを表示できます。したがって、Derived2メソッドm3()とのマッピングはありm4()ますbaseが、変数はそれらのメソッドにアクセスできません。

文字列tmp; // Derived2リファレンス(図2)tmp =派生2.m3(); // tmpは "Derived.m3()" tmp = Delivered2.m4(); // tmpは "Derived2.m4()" //ベースリファレンス(図3)tmp = base.m3(); //コンパイル時エラーtmp = base.m4(); //コンパイル時エラー

ランタイム

Derived2

オブジェクトは、次のいずれかを完全に受け入れることができます。

m3()

または

m4()

メソッド呼び出し。これらの試行された呼び出しを許可しないタイプ制限

Base