Java ダイナミックバインディング練習問題

動的なバインディングについて話すと、初心者の頃は関連する結論を丸暗記することが多いです。

例えば、多態性を使用してオブジェクトを定義する文 A a = new B()(ここで B は A のサブクラスです)について、a.field1 には A クラスの属性が使用され、a.method1() には B インスタンスのメソッドが使用されると言います。これは多態性と動的バインディングの最も典型的な応用です。

それでも、次の問題を見てみましょう。

Animal.java

public class Animal {
      public void eat(double quantity) {
             System.out.println("動物は" + quantity + "g食べました。");
      }
}

Human.java

public class Human extends Animal {
      public void eat(int quantity) {
            System.out.println("人間は" + quantity + "g食べました。");
      }
}

Test.java

public class Test {
     public static void main(String[] args) {
           Animal animal = new Human();
           int quantity = 500;
           animal.eat(quantity);  // ?
     }
}

この問題は、Qiita 上の Java 動的バインディングに関する少ない記事の一つから来ています。 https://qiita.com/kome_ha/items/585c4ba7515bb9b6f019

この著者に感謝します。Java Silver 試験の前に、自分がまだ理解が浅いことに気づかされました。

さて、話を戻しましょう。再度この問題を見てみましょう。 一見すると特に問題ないように思えますが、もしこれが親クラスのメソッドを呼び出しているとしたら、少し厄介だと感じるかもしれません。

親クラスのメソッドには double 型があり、子クラスのメソッドには int 型があります。渡された引数は int 型で、動的バインディングの仕組みにも合っています。なぜ子クラスのメソッドが呼ばれないのでしょうか?

私は ChatGPT 先生の説明が一番分かりやすいと思います。いつものように、Q&A の結果をコピーして、分析を行います。

上述の Java コードの結果は、多態性とメソッドのオーバーロードに関連しています。コードを順を追って解析してみましょう。

  1. クラスの定義:

    • Animal クラスは eat(double quantity) メソッドを定義しています。
    • Human クラスは Animal を継承し、eat(int quantity) というオーバーロードメソッドを定義しています。
  2. メインメソッド (main メソッド) のロジック:

    • Animal animal = new Human(); という行は、Human クラスのインスタンスを生成し、その参照を Animal 型の変数 animal に割り当てています。これは animal 変数が Human オブジェクトを参照しているが、その型は Animal であることを意味します。
    • int quantity = 500; は整数変数 quantity を定義し、500 を代入しています。
    • animal.eat(quantity); は eat メソッドを呼び出しています。

ここで、animal.eat(quantity); を呼び出す際に何が起こるのかを理解する必要があります。

メソッドの解決

animal の静的型が Animal であるため、コンパイラコンパイル時に Animal クラス内に eat(quantity) と一致するシグネチャのメソッドがあるかどうかを確認します。 - コンパイラが見るのは eat(int quantity) です。なぜなら quantity は int 型だからです。 - Animal クラスには eat(int quantity) はなく、eat(double quantity) しかありません。 - したがって、コンパイラは quantity を int 型から double 型に自動変換し、Animal クラスの eat(double quantity) メソッドに一致させます。

メソッドの呼び出し

実行時には、animal は実際には Human オブジェクトであるため、仮想マシンは Human クラス内に eat(double quantity) メソッドがオーバーライドされているかどうかを確認します。Human クラスは eat(double quantity) メソッドをオーバーライドしていないため、仮想マシンは Animal クラスの eat(double quantity) メソッドを呼び出します。

出力結果

呼び出されるのは Animal クラスの eat(double quantity) メソッドであるため、以下の出力が表示されます: text 動物は500.0g食べました。

まとめ

animal 変数は実際には Human オブジェクトを参照しているにもかかわらず、Human クラスが Animal クラスの eat(double quantity) メソッドをオーバーライドしておらず、メソッドの解決がコンパイル時に静的型に基づいて行われるため、最終的に呼び出されるのは Animal クラスの eat(double quantity) メソッドです。

Leeのコメント

簡単に言えば、この問題の難しさは、継承機構と動的バインディング機構だけでなく、静的バインディング機構の深い理解も要求される点にあります。静的バインディング機構は、このオブジェクトの通常の属性および静的メンバー(静的属性および静的メソッド)の呼び出しを決定するだけでなく、動的バインディング機構が働く際に呼び出される通常のメソッドの関数シグネチャも決定します。

この例では、コンパイル時の型は Animal で、実行時の型は Human です。

コンパイル時には、コンパイラは eat(quantity) に一致するパターンを見つけようとします。見つからない場合はエラーになります。Animal には eat(double quantity) というメソッドしかないため、int はこの形式引数と完全には一致しませんが、int はアップキャストできるため、一致します。したがって、最終的に呼び出される通常のメソッドは静的に eat(double quantity) にバインドされます。これは静的バインディング機構の詳細です。

実行時には、JVM はまず動的バインディング機構を発揮し、インスタンスオブジェクト内で通常のメソッド eat(double quantity) を探します。しかし残念ながら、サブクラスのメソッドには存在しません。JVM は次に継承機構を発揮し、上位の親クラスに探しに行きます。そして、親クラスで見つけたので、親クラスのメソッドが実行されます。

より簡潔的にまとめると、多態性においては、まずコンパイル時に静的バインディング機構が働き、次に実行時に通常のメソッドの実行に際して動的バインディング機構と継承機構が働きます。

今日はここまでです。Java は本当に難しいですね😩