JavaRush /Java Blog /Random-JA /ポリモーフィズムとその仲間たち
Viacheslav
レベル 3

ポリモーフィズムとその仲間たち

Random-JA グループに公開済み
ポリモーフィズムは、オブジェクト指向プログラミングの基本原理の 1 つです。これにより、Java の強力な型指定の力を利用して、使用可能で保守しやすいコードを作成できます。彼については多くのことが言われていますが、皆さんがこのレビューから何か新しいことを感じ取っていただければ幸いです。
ポリモーフィズムとその仲間たち - 1

導入

Java プログラミング言語が Oracle に属していることは誰もが知っていると思います。したがって、パスはサイトwww.oracle.comから始まります。トップページに「メニュー」があります。その中の「Documentation」セクションに「Java」サブセクションがあります。言語の基本機能に関連するものはすべて「Java SE ドキュメント」に属しているため、このセクションを選択します。最新バージョンのドキュメント セクションが開きますが、現時点では「別のリリースをお探しですか?」オプションとして「JDK8」を選択しましょう。このページにはさまざまなオプションが表示されます。しかし、私たちは言語を学ぶ「Java チュートリアル ラーニング パス」に興味があります。このページには、「 Java 言語の学習」という別のセクションがあります。これは最も神聖な、Oracle による Java の基本に関するチュートリアルです。Java はオブジェクト指向プログラミング言語 (OOP) であるため、Oracle の Web サイトでも Java を学習するには、「オブジェクト指向プログラミングの概念」の基本概念を説明することから始まります。名前自体から、Java がオブジェクトの操作に焦点を当てていることは明らかです。「オブジェクトとは何ですか?」サブセクションから、Java のオブジェクトが状態と動作で構成されていることは明らかです。私たちが銀行口座を持っていると想像してください。アカウント内の金額が状態であり、この状態を操作する方法が動作です。オブジェクトは何らかの方法で記述する必要があり (オブジェクトがどのような状態や動作をするかを伝える)、この記述がクラスです。あるクラスのオブジェクトを作成するとき、そのクラスを指定します。これを「オブジェクトタイプ」と呼びます。したがって、Java 言語仕様の「第 4 章 型、値、および変数」セ​​クションに記載されているように、Java は厳密に型指定された言語であると言われています。Java 言語は OOP の概念に従い、extends キーワードを使用した継承をサポートします。なぜ拡張するのでしょうか?継承により、子クラスは親クラスの動作と状態を継承し、それらを補完できるためです。基本クラスの機能を拡張します。インターフェイスは、 implements キーワードを使用してクラスの説明で指定することもできます。クラスがインターフェイスを実装するということは、そのクラスが何らかの規約、つまりクラスが特定の動作をするというプログラマによる環境の残りの部分に対する宣言に準拠していることを意味します。たとえば、プレーヤーにはさまざまなボタンがあります。これらのボタンはプレーヤーの動作を制御するためのインターフェイスであり、その動作によりプレーヤーの内部状態 (たとえば、音量) が変化します。この場合、状態や動作を記述したものがクラスを与えることになります。クラスがインターフェイスを実装している場合、このクラスによって作成されたオブジェクトは、クラスだけでなくインターフェイスによっても型によって記述できます。例を見てみましょう:
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
タイプは非常に重要な説明です。それは、オブジェクトをどのように操作するかを示します。オブジェクトにどのような動作を期待するか。行動は方法です。したがって、メソッドを理解しましょう。Oracle の Web サイトでは、Oracle チュートリアルにメソッドに関する独自のセクション「メソッドの定義」があります。この記事から最初に理解すべきこと:メソッド シグネチャは、メソッドの名前とパラメータのタイプです
ポリモーフィズムとその仲間たち - 2
たとえば、メソッド public void method(Object o) を宣言する場合、シグネチャはメソッドの名前とパラメーター Object の型になります。戻り値の型は署名に含まれません。大事です!次に、ソースコードをコンパイルしましょう。ご存知のとおり、このためには、コードをクラス名と拡張子 java を持つファイルに保存する必要があります。Java コードは、「javac」コンパイラを使用して、Java 仮想マシン (JVM) で実行できる中間形式にコンパイルされます。この中間形式はバイトコードと呼ばれ、.class 拡張子が付いたファイルに含まれています。コマンドを実行してコンパイルしましょう。Javajavac MusicPlayer.java コードがコンパイルされたら、それを実行できます。「java」ユーティリティを使用して開始すると、Java 仮想マシン プロセスが起動され、クラス ファイルに渡されたバイトコードが実行されます。コマンドを実行してアプリケーションを起動しましょうjava MusicPlayer。println メソッドの入力パラメータで指定されたテキストが画面に表示されます。興味深いことに、拡張子 .class が付いたファイルにバイトコードが含まれていると、「javap」ユーティリティを使用してそれを表示できます。コマンド <ocde>javap -c MusicPlayer を実行してみましょう。
ポリモーフィズムとその仲間たち - 3
invokevirtualバイトコードから、クラスが指定された型のオブジェクトを介したメソッドの呼び出しが を使用して実行され、コンパイラがどのメソッド シグネチャを使用するかを計算していることがわかります。なぜinvokevirtual?仮想メソッドの呼び出し(invokeは呼び出しとして変換されます)があるためです。仮想メソッドとは何ですか? これは、プログラム実行中に本体をオーバーライドできるメソッドです。特定のキー (メソッド シグネチャ) とメソッドの本体 (コード) の間の対応リストがあると想像してください。そして、キーとメソッド本体の間のこの対応関係は、プログラムの実行中に変更される可能性があります。したがって、このメソッドは仮想的です。デフォルトでは、Java では、静的、最終、プライベートではないメソッドは仮想です。このおかげで、Java はポリモーフィズムのオブジェクト指向プログラミング原理をサポートします。すでにおわかりかと思いますが、これが今日のレビューの内容です。

ポリモーフィズム

Oracle の Web サイトの公式チュートリアルには、「ポリモーフィズム」という別のセクションがあります。Java Online Compiler を使用して、Java でポリモーフィズムがどのように機能するかを見てみましょう。たとえば、Java で数値を表す抽象クラスNumber があります。何が許可されますか? 彼は、すべての後継者が持つことになる基本的なテクニックをいくつか持っています。Number から継承した人は文字通り、「私は数字です。あなたも数字として私と一緒に仕事をしてください。」と言います。たとえば、後続の場合、 intValue() メソッドを使用してその整数値を取得できます。Number の Java API を見ると、このメソッドが抽象であることがわかります。つまり、Number の後続のそれぞれがこのメソッド自体を実装する必要があります。しかし、これは私たちに何をもたらすのでしょうか?例を見てみましょう:
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
この例からわかるように、ポリモーフィズムのおかげで、Number の子孫となる任意の型の引数を入力として受け入れるメソッドを作成できます (Number は抽象クラスであるため、取得できません)。プレーヤーの例の場合と同様に、この場合は Number などの何かを操作したいと言っています。Number である人は誰でも、その整数値を指定できる必要があることはわかっています。私たちにとってはそれだけで十分です。特定のオブジェクトの実装の詳細には立ち入りたくないので、Number のすべての子孫に共通のメソッドを通じてこのオブジェクトを操作したいと考えています。利用できるメソッドのリストは、(バイトコードで前に見たように) コンパイル時の型によって決まります。この場合、タイプは Number になります。この例からわかるように、さまざまな型のさまざまな数値を渡しています。つまり、sum メソッドは入力として Integer、Long、Double を受け取ります。しかし、これらすべてに共通しているのは、それらが抽象 Number の子孫であるため、intValue メソッドでその動作をオーバーライドすることです。それぞれの特定の型は、その型を Integer にキャストする方法を知っています。このようなポリモーフィズムは、いわゆるオーバーライド (英語では Overriding) を通じて実装されます。
ポリモーフィズムとその仲間たち - 4
オーバーライドまたは動的ポリモーフィズム。したがって、次の内容を含む HelloWorld.java ファイルを保存することから始めましょう。
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
やってみましょjavac HelloWorld.javajavap -c HelloWorld:
ポリモーフィズムとその仲間たち - 5
ご覧のとおり、メソッド呼び出しを含む行のバイトコードでは、呼び出し用のメソッドへの同じ参照が示されていますinvokevirtual (#6)。やりましょうjava HelloWorld。ご覧のとおり、変数の親と子は Parent 型で宣言されていますが、実装自体は変数に割り当てられたオブジェクト (つまり、オブジェクトの型) に応じて呼び出されます。プログラムの実行中 (実行時とも言います)、JVM はオブジェクトに応じて、同じシグネチャを使用してメソッドを呼び出すときに、異なるメソッドを実行します。つまり、対応する署名のキーを使用して、最初に 1 つのメソッド本体を受信し、次に別のメソッド本体を受信しました。変数内のオブジェクトに応じて異なります。このプログラム実行時にどのメソッドを呼び出すかを決定することをレイトバインディングまたはダイナミックバインディングとも呼びます。つまり、シグネチャとメソッド本体の間の照合は、メソッドが呼び出されるオブジェクトに応じて動的に実行されます。当然のことながら、クラスの静的メンバー (クラス メンバー) や、アクセス タイプが private または Final のクラス メンバーをオーバーライドすることはできません。@Override アノテーションも開発者に役立ちます。これは、この時点で祖先メソッドの動作をオーバーライドしようとしているということをコンパイラーが理解するのに役立ちます。メソッドのシグネチャに間違いがあった場合、コンパイラーはすぐにそれを教えてくれます。例えば:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
エラーが発生してコンパイルされません: エラー: メソッドはスーパータイプのメソッドをオーバーライドまたは実装しません
ポリモーフィズムとその仲間たち - 6
再定義は「共分散」の概念にも関連しています。例を見てみましょう:
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
一見難解ですが、その意味は、オーバーライドするときに、祖先で指定された型だけでなく、より具体的な型も返すことができるという事実に帰着します。たとえば、祖先は Number を返したので、Number の子孫である Integer を返すことができます。メソッドのスローで宣言された例外にも同じことが当てはまります。継承者はメソッドをオーバーライドして、スローされる例外を調整できます。しかし、彼らは拡大することができません。つまり、親が IOException をスローした場合、より正確な EOFException をスローできますが、Exception をスローすることはできません。同様に、範囲を狭めたり、追加の制限を課したりすることはできません。たとえば、静的を追加することはできません。
ポリモーフィズムとその仲間たち - 7

隠蔽

「隠蔽」というものもあります。例:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
これはよく考えてみれば当然のことです。クラスの静的メンバーはそのクラスに属します。つまり、変数の型に。したがって、子の型が Parent の場合、メソッドは子ではなく親に対して呼び出されるのは論理的です。先ほどと同様にバイトコードを見ると、静的メソッドが invokestatic を使用して呼び出されていることがわかります。これにより、invokevirtual や invokeinterface のようにメソッド テーブルではなく、型を確認する必要があることが JVM に説明されます。
ポリモーフィズムとその仲間たち - 8

メソッドのオーバーロード

Java Oracle チュートリアルには他に何が表示されますか? 以前に学習したセクション「メソッドの定義」では、オーバーロードについて説明しています。それは何ですか?ロシア語では、これは「メソッドのオーバーロード」であり、そのようなメソッドは「オーバーロード」と呼ばれます。そこで、メソッドのオーバーロードです。一見すると、すべてがシンプルです。オンライン Java コンパイラー (たとえば、チュートリアルポイント オンライン Java コンパイラー)を開いてみましょう。
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
したがって、ここではすべてが単純に見えます。Oracle チュートリアルで述べたように、オーバーロードされたメソッド (この場合はsay メソッド) は、メソッドに渡される引数の数と型が異なります。同じ名前と同じ数の同じ型の引数を宣言することはできません。コンパイラはそれらを相互に区別できません。非常に重要な点にすぐに注目してください。
ポリモーフィズムとその仲間たち - 9
つまり、オーバーロードする場合、コンパイラは正確性をチェックします。大事です。しかし、コンパイラは実際に、特定のメソッドを呼び出す必要があることをどのように判断するのでしょうか? これは、Java 言語仕様「15.12.2.5. 最も具体的なメソッドの選択」に記載されている「最も具体的なメソッド」ルールを使用します。それがどのように機能するかを示すために、Oracle Certified Professional Java Programmer の例を見てみましょう。
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
ここから例を見てみましょう: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... ご覧のとおり、次のように渡しています。メソッドには null を指定します。コンパイラは、最も具体的な型を決定しようとします。オブジェクトが適切ではないため、すべては彼から受け継がれたものです。どうぞ。例外には 2 つのクラスがあります。java.io.IOExceptionを見て、「直接の既知のサブクラス」に FileNotFoundException があることを確認してください。つまり、FileNotFoundException が最も具体的なタイプであることがわかります。したがって、結果は文字列「FileNotFoundException」が出力されます。しかし、IOException を EOFException に置き換えると、型ツリーの階層の同じレベルに 2 つのメソッドがあることがわかります。つまり、どちらのメソッドも IOException が親であることがわかります。コンパイラは呼び出すメソッドを選択できず、コンパイル エラーをスローします: reference to method is ambiguous。もう 1 つの例:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
1 が出力されます。ここでは質問はありません。int 型は vararg https://docs.oracle.com/javase/8/docs/technotes/guides/ language/varargs.html であり、実際には「糖衣構文」にすぎず、実際には int です。 .. 配列は int[] 配列として読み取ることができます。ここでメソッドを追加すると、次のようになります。
public static void method(long a, long b) {
	System.out.println("2");
}
すると、1 ではなく 2 が表示されます。2 つの数値を渡しており、2 つの引数の方が 1 つの配列よりもよく一致します。メソッドを追加すると、次のようになります。
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
この場合も 2 が表示されます。この場合、プリミティブは整数でボックス化するよりも正確に一致するためです。ただし、実行するとmethod(new Integer(1), new Integer(2));3 が出力されます 。Java のコンストラクターはメソッドに似ており、署名の取得にも使用できるため、オーバーロードされたメソッドと同じ「オーバーロード解決」ルールが適用されます。Java 言語仕様では、「8.8.8. コンストラクターのオーバーロード」でそのように指示されています。メソッドのオーバーロード = 早期バインディング (別名静的バインディング) 静的バインディングまたは動的バインディングとも呼ばれる早期バインディングと後期バインディングについてよく耳にします。それらの違いは非常に簡単です。初期はコンパイル、後期はプログラムが実行される瞬間です。したがって、早期バインディング (静的バインディング) は、コンパイル時にどのメソッドが誰に対して呼び出されるかを決定します。さて、レイトバインディング(動的バインディング)とは、プログラム実行時にどのメソッドを直接呼び出すかを決定することです。前に見たように (IOException を EOFException に変更したとき)、メソッドをオーバーロードしてコンパイラーがどこでどの呼び出しを行うかを理解できない場合、コンパイル時エラー「メソッドへの参照があいまいです」が発生します。英語から翻訳された曖昧という言葉は、曖昧または不確かな、不正確を意味します。オーバーロードは早期バインディングであることがわかります。チェックはコンパイル時に実行されます。結論を確認するために、Java 言語仕様の「8.4.9. オーバーロード」の章を開いてみましょう。
ポリモーフィズムとその仲間たち - 10
コンパイル中に、引数の型と数に関する情報 (コンパイル時に入手可能) がメソッドのシグネチャを決定するために使用されることがわかりました。メソッドがオブジェクトのメソッドの 1 つ (つまり、インスタンス メソッド) である場合、実際のメソッド呼び出しは、動的メソッド検索 (つまり、動的バインディング) を使用して実行時に決定されます。わかりやすくするために、前に説明したものと同様の例を見てみましょう。
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
このコードを HelloWorld.java ファイルに保存し、javac HelloWorld.java 次のコマンドを実行してコンパイラがバイトコードに何を書き込んだかを見てみましょうjavap -verbose HelloWorld
ポリモーフィズムとその仲間たち - 11
前述したように、コンパイラは、将来何らかの仮想メソッドが呼び出されると判断しました。つまり、メソッド本体は実行時に定義されます。ただし、コンパイル時に、コンパイラは 3 つのメソッドすべてのうち、最も適切なものを選択するため、次の番号を示します。"invokevirtual #13"
ポリモーフィズムとその仲間たち - 12
これはどのようなメソッド参照ですか? これはメソッドへのリンクです。大まかに言えば、これは実行時に Java 仮想マシンが実行するメソッドを実際に決定するための手がかりとなります。詳細については、スーパー記事「JVM は内部的にメソッドのオーバーロードとオーバーライドを処理する方法」を参照してください。

要約する

そこで、Java はオブジェクト指向言語としてポリモーフィズムをサポートしていることがわかりました。ポリモーフィズムは静的 (静的バインディング) または動的 (動的バインディング) の場合があります。静的ポリモーフィズム (早期バインディングとも呼ばれます) では、コンパイラーがどのメソッドをどこで呼び出す必要があるかを決定します。これにより、オーバーロードなどのメカニズムの使用が可能になります。動的多態性 (遅延バインディングとも呼ばれます) では、事前に計算されたメソッドのシグネチャに基づいて、メソッドは、使用されるオブジェクト (つまり、どのオブジェクトのメソッドが呼び出されるか) に基づいて実行時に計算されます。これらのメカニズムがどのように機能するかは、バイトコードを使用して確認できます。オーバーロードはメソッドのシグネチャを確認し、オーバーロードを解決するときに、最も具体的な (最も正確な) オプションが選択されます。オーバーライドでは型を調べて使用可能なメソッドを判断し、メソッド自体はオブジェクトに基づいて呼び出されます。このトピックに関する資料と同様に: #ヴィアチェスラフ
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION