1. 概要

メソッドのオーバーロードとオーバーライドは、Javaプログラミング言語の重要な概念であるため、詳細に検討する必要があります。

この記事では、これらの概念の基本を学び、どのような状況で役立つかを確認します。

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

メソッドのオーバーロードは、まとまりのあるクラスAPIを定義できる強力なメカニズムです。メソッドのオーバーロードがこのような価値のある機能である理由をよりよく理解するために、簡単な例を見てみましょう。

2つの数値、3つの数値などを乗算するためのさまざまなメソッドを実装する素朴なユーティリティクラスを作成したとします。

multiply2() multiply3() multiply4()、など、誤解を招く名前やあいまいな名前をメソッドに付けた場合、それはひどいことになります。設計されたクラスAPI。 ここで、メソッドのオーバーロードが役立ちます。

簡単に言えば、2つの異なる方法でメソッドのオーバーロードを実装できます。

  • 同じ名前で引数の数が異なる2つ以上のメソッドを実装する
  • 同じ名前で異なるタイプの引数を取る2つ以上のメソッドを実装する

2.1. 異なる数の引数

Multiplier クラスは、簡単に言うと、異なる数の引数を取る2つの実装を定義するだけで、 multiply()メソッドをオーバーロードする方法を示しています。

public class Multiplier {
    
    public int multiply(int a, int b) {
        return a * b;
    }
    
    public int multiply(int a, int b, int c) {
        return a * b * c;
    }
}

2.2. さまざまなタイプの引数

同様に、 multiply()メソッドをオーバーロードするには、さまざまなタイプの引数を受け入れるようにします。

public class Multiplier {
    
    public int multiply(int a, int b) {
        return a * b;
    }
    
    public double multiply(double a, double b) {
        return a * b;
    }
}

さらに、Multiplierクラスを両方のタイプのメソッドオーバーロードで定義することは正当です。

public class Multiplier {
    
    public int multiply(int a, int b) {
        return a * b;
    }
    
    public int multiply(int a, int b, int c) {
        return a * b * c;
    }
    
    public double multiply(double a, double b) {
        return a * b;
    }
}

ただし、戻り型のみが異なる2つのメソッド実装を持つことはできないことに注意してください。

理由を理解するために–次の例を考えてみましょう。

public int multiply(int a, int b) { 
    return a * b; 
}
 
public double multiply(int a, int b) { 
    return a * b; 
}

この場合、メソッド呼び出しのあいまいさのために、コードは単純にコンパイルされません。コンパイラーは、 multiply()のどの実装を呼び出すかを知りません。

2.3. タイププロモーション

メソッドのオーバーロードによって提供される優れた機能の1つは、いわゆるタイププロモーション、別名 プリミティブ変換の拡大

簡単に言うと、オーバーロードされたメソッドと特定のメソッド実装に渡される引数のタイプの間に一致がない場合、特定のタイプが暗黙的に別のタイプにプロモートされます。

タイププロモーションがどのように機能するかをより明確に理解するには、 multiply()メソッドの次の実装を検討してください。

public double multiply(int a, long b) {
    return a * b;
}

public int multiply(int a, int b, int c) {
    return a * b * c;
}

ここで、2つの int 引数を使用してメソッドを呼び出すと、2番目の引数が long にプロモートされます。この場合、2つのintを使用したメソッドの一致する実装はありません。 引数。

タイププロモーションを示すための簡単な単体テストを見てみましょう。

@Test
public void whenCalledMultiplyAndNoMatching_thenTypePromotion() {
    assertThat(multiplier.multiply(10, 10)).isEqualTo(100.0);
}

逆に、実装が一致するメソッドを呼び出すと、型の昇格は行われません。

@Test
public void whenCalledMultiplyAndMatching_thenNoTypePromotion() {
    assertThat(multiplier.multiply(10, 10, 10)).isEqualTo(1000);
}

メソッドのオーバーロードに適用されるタイププロモーションルールの概要は次のとおりです。

  • byte は、 short、int、long、float、またはdoubleにプロモートできます。
  • short は、 int、long、float、またはdoubleにプロモートできます。
  • char は、 int、long、float、またはdoubleにプロモートできます。
  • int は、 long、float、またはdoubleにプロモートできます
  • longfloatまたはdoubleに昇格できます
  • floatdoubleに昇格させることができます

2.4. 静的バインディング

特定のメソッド呼び出しをメソッドの本体に関連付ける機能は、バインディングと呼ばれます。

メソッドのオーバーロードの場合、バインディングはコンパイル時に静的に実行されるため、静的バインディングと呼ばれます。

コンパイラは、メソッドのシグネチャをチェックするだけで、コンパイル時にバインディングを効果的に設定できます。

3. メソッドのオーバーライド

メソッドのオーバーライドにより、基本クラスで定義されたメソッドのサブクラスにきめ細かい実装を提供できます。

メソッドのオーバーライドは強力な機能ですが、継承を使用することの論理的帰結であることを考えると、 OOP をいつどこで利用するかを分析する必要があります。ユースケースごとに慎重に

ここで、単純な継承ベース( “is-a”)の関係を作成して、メソッドのオーバーライドを使用する方法を見てみましょう。

基本クラスは次のとおりです。

public class Vehicle {
    
    public String accelerate(long mph) {
        return "The vehicle accelerates at : " + mph + " MPH.";
    }
    
    public String stop() {
        return "The vehicle has stopped.";
    }
    
    public String run() {
        return "The vehicle is running.";
    }
}

そして、これが考案されたサブクラスです:

public class Car extends Vehicle {

    @Override
    public String accelerate(long mph) {
        return "The car accelerates at : " + mph + " MPH.";
    }
}

上記の階層では、サブタイプ Car。のより洗練された実装を提供するために、 Accelerate()メソッドを単純にオーバーライドしました。

ここで、アプリケーションがVehicleクラスのインスタンスを使用する場合、 Accelerate()メソッドの両方の実装が持っているように、Carのインスタンスでも動作できることは明らかです同じ署名と同じリターンタイプ。

VehicleクラスとCarクラスをチェックするためのいくつかの単体テストを書いてみましょう。

@Test
public void whenCalledAccelerate_thenOneAssertion() {
    assertThat(vehicle.accelerate(100))
      .isEqualTo("The vehicle accelerates at : 100 MPH.");
}
    
@Test
public void whenCalledRun_thenOneAssertion() {
    assertThat(vehicle.run())
      .isEqualTo("The vehicle is running.");
}
    
@Test
public void whenCalledStop_thenOneAssertion() {
    assertThat(vehicle.stop())
      .isEqualTo("The vehicle has stopped.");
}

@Test
public void whenCalledAccelerate_thenOneAssertion() {
    assertThat(car.accelerate(80))
      .isEqualTo("The car accelerates at : 80 MPH.");
}
    
@Test
public void whenCalledRun_thenOneAssertion() {
    assertThat(car.run())
      .isEqualTo("The vehicle is running.");
}
    
@Test
public void whenCalledStop_thenOneAssertion() {
    assertThat(car.stop())
      .isEqualTo("The vehicle has stopped.");
}

ここで、オーバーライドされない run()メソッドと stop()メソッドが、両方のCarに対して等しい値を返す方法を示すいくつかの単体テストを見てみましょう。および車両

@Test
public void givenVehicleCarInstances_whenCalledRun_thenEqual() {
    assertThat(vehicle.run()).isEqualTo(car.run());
}
 
@Test
public void givenVehicleCarInstances_whenCalledStop_thenEqual() {
   assertThat(vehicle.stop()).isEqualTo(car.stop());
}

この例では、両方のクラスのソースコードにアクセスできるため、ベース Vehicle インスタンスでaccelerate()メソッドを呼び出し、accelerateを呼び出すことがはっきりとわかります。 () Car インスタンスのは、同じ引数に対して異なる値を返します。

したがって、次のテストは、オーバーライドされたメソッドがCarのインスタンスに対して呼び出されることを示しています。

@Test
public void whenCalledAccelerateWithSameArgument_thenNotEqual() {
    assertThat(vehicle.accelerate(100))
      .isNotEqualTo(car.accelerate(100));
}

3.1. タイプの代替可能性

OOPのコア原則は、 Liskov Substitution Principle(LSP)と密接に関連するタイプ置換可能性の原則です。

簡単に言えば、LSPは、アプリケーションが特定の基本タイプで動作する場合、そのサブタイプのいずれでも動作する必要があると述べています。 そうすることで、型の置換可能性が適切に保持されます。

メソッドのオーバーライドに関する最大の問題は、派生クラスの一部の特定のメソッド実装がLSPに完全に準拠していない可能性があるため、型の代替可能性を維持できない可能性があることです。

もちろん、オーバーライドされたメソッドを作成して、異なるタイプの引数を受け入れ、異なるタイプを返すことも有効ですが、これらのルールを完全に順守します。

  • 基本クラスのメソッドが特定の型の引数を取る場合、オーバーライドされるメソッドは同じ型またはスーパー型(別名 contravariant メソッド引数)
  • 基本クラスのメソッドがvoidを返す場合、オーバーライドされたメソッドはvoidを返す必要があります
  • 基本クラスのメソッドがプリミティブを返す場合、オーバーライドされたメソッドは同じプリミティブを返す必要があります
  • 基本クラスのメソッドが特定の型を返す場合、オーバーライドされたメソッドは同じ型またはサブタイプ(別名 共変リターンタイプ)
  • 基本クラスのメソッドが例外をスローする場合、オーバーライドされたメソッドは、同じ例外または基本クラスの例外のサブタイプをスローする必要があります

3.2. 動的バインディング

メソッドのオーバーライドは、基本タイプとサブタイプの階層が存在する継承でのみ実装できることを考慮すると、基本クラスとサブクラスの両方が定義するため、コンパイラはコンパイル時にどのメソッドを呼び出すかを決定できません。同じ方法。

結果として、コンパイラはオブジェクトのタイプをチェックして、どのメソッドを呼び出す必要があるかを知る必要があります。

このチェックは実行時に行われるため、メソッドのオーバーライドは動的バインディングの典型的な例です。

4. 結論

このチュートリアルでは、メソッドのオーバーロードとメソッドのオーバーライドを実装する方法を学び、それらが役立ついくつかの典型的な状況を調査しました。

いつものように、この記事に示されているすべてのコードサンプルは、GitHubから入手できます。