Überladen und Überschreiben von Methoden in Java

Methode Überladen und Überschreiben in Java

1. Überblick

Überladen und Überschreiben von Methoden sind Schlüsselkonzepte der Java-Programmiersprache und verdienen daher eine eingehende Betrachtung.

In diesem Artikel lernen wir die Grundlagen dieser Konzepte kennen und sehen, in welchen Situationen sie nützlich sein können.

2. Methodenüberladung

Method overloading is a powerful mechanism that allows us to define cohesive class APIs. Um besser zu verstehen, warum das Überladen von Methoden eine so wertvolle Funktion ist, sehen wir uns ein einfaches Beispiel an.

Angenommen, wir haben eine naive Utility-Klasse geschrieben, die verschiedene Methoden zum Multiplizieren von zwei Zahlen, drei Zahlen usw. implementiert.

Wenn wir den Methoden irreführende oder mehrdeutige Namen wiemultiply2(),multiply3(),multiply4(), gegeben haben, wäre dies eine schlecht gestaltete Klassen-API. Hier kommt das Überladen von Methoden ins Spiel.

Einfach ausgedrückt, können wir das Überladen von Methoden auf zwei verschiedene Arten implementieren:

  • Implementierung von zwei oder mehrmethods that have the same name but take different numbers of arguments

  • Implementierung von zwei oder mehrmethods that have the same name but take arguments of different types

2.1. Unterschiedliche Anzahl von Argumenten

Die KlasseMultiplierzeigt kurz und bündig, wie die Methodemultiply()überladen wird, indem einfach zwei Implementierungen definiert werden, die eine unterschiedliche Anzahl von Argumenten annehmen:

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. Argumente unterschiedlicher Art

In ähnlicher Weise können wir diemultiply()-Methode überladen, indem sie Argumente verschiedener Typen akzeptiert:

public class Multiplier {

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

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

Darüber hinaus ist es legitim, dieMultiplier-Klasse mit beiden Arten der Methodenüberladung zu definieren:

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;
    }
}

Es ist jedoch erwähnenswert, dassit’s not possible to have two method implementations that differ only in their return types.

Um zu verstehen, warum - betrachten wir das folgende Beispiel:

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

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

In diesem Fallthe code simply wouldn’t compile because of the method call ambiguity - Der Compiler weiß nicht, welche Implementierung vonmultiply() aufgerufen werden soll.

2.3. Typ Promotion

Ein nettes Merkmal, das durch das Überladen von Methoden bereitgestellt wird, sind die sogenanntentype promotion, a.k.a. widening primitive conversion.

In einfachen Worten, ein bestimmter Typ wird implizit zu einem anderen heraufgestuft, wenn keine Übereinstimmung zwischen den Typen der an die überladene Methode übergebenen Argumente und einer bestimmten Methodenimplementierung besteht.

Um besser zu verstehen, wie die Typförderung funktioniert, sollten Sie die folgenden Implementierungen dermultiply()-Methode berücksichtigen:

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

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

Wenn Sie nun die Methode mit zweiint Argumenten aufrufen, wird das zweite Argument zulong heraufgestuft, da in diesem Fall keine übereinstimmende Implementierung der Methode mit zweiint Argumenten erfolgt.

Sehen wir uns einen kurzen Komponententest an, um die Typwerbung zu demonstrieren:

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

Wenn wir dagegen die Methode mit einer passenden Implementierung aufrufen, findet keine Typ-Promotion statt:

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

Hier ist eine Zusammenfassung der Typ-Promotion-Regeln, die für das Überladen von Methoden gelten:

  • byte kann zushort, int, long, float, oderdouble befördert werden

  • short kann zuint, long, float, oderdouble befördert werden

  • char kann zuint, long, float, oderdouble befördert werden

  • int kann zulong, float, oderdouble befördert werden

  • long kann zufloat oderdouble befördert werden

  • float kann zudouble heraufgestuft werden

2.4. Statische Bindung

Die Möglichkeit, dem Methodenkörper einen bestimmten Methodenaufruf zuzuordnen, wird als Bindung bezeichnet.

Im Falle einer Methodenüberladung wird die Bindung zur Kompilierungszeit statisch ausgeführt, daher wird sie als statische Bindung bezeichnet.

Der Compiler kann die Bindung zur Kompilierungszeit effektiv festlegen, indem er einfach die Signaturen der Methoden überprüft.

3. Überschreiben der Methode

Durch das Überschreiben von Methoden können wir feinkörnige Implementierungen in Unterklassen für Methoden bereitstellen, die in einer Basisklasse definiert sind.

Während das Überschreiben von Methoden eine leistungsstarke Funktion ist, ist dies eine logische Folge der Verwendung voninheritance, einer der größten Säulen vonOOP -when and where to utilize it should be analyzed carefully, on a per-use-case basis.

Lassen Sie uns nun sehen, wie Sie das Überschreiben von Methoden verwenden, indem Sie eine einfache, auf Vererbung basierende Beziehung ("is-a") erstellen.

Hier ist die Basisklasse:

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.";
    }
}

Und hier ist eine erfundene Unterklasse:

public class Car extends Vehicle {

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

In der obigen Hierarchie haben wir einfach dieaccelerate()-Methode überschrieben, um eine verfeinerte Implementierung für den SubtypCar. bereitzustellen

Hier ist deutlich zu sehen, dassif an application uses instances of the Vehicle class, then it can work with instances of Car as well, da beide Implementierungen der Methodeaccelerate() dieselbe Signatur und denselben Rückgabetyp haben.

Schreiben wir einige Komponententests, um die KlassenVehicle undCarzu überprüfen:

@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.");
}

Sehen wir uns nun einige Komponententests an, die zeigen, wie die nicht überschriebenen Methodenrun() undstop() gleiche Werte fürCar undVehicle zurückgeben:

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

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

In unserem Fall haben wir Zugriff auf den Quellcode für beide Klassen, sodass wir deutlich sehen können, dass die Methodeaccelerate()auf einer Basisinstanz vonVehicleundaccelerate()auf einerCaraufgerufen wird ) s Instanz gibt unterschiedliche Werte für dasselbe Argument zurück.

Daher zeigt der folgende Test, dass die überschriebene Methode für eine Instanz vonCar aufgerufen wird:

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

3.1. Typ Substituierbarkeit

Ein Kernprinzip bei OOP ist die Typ-Substituierbarkeit, die eng mit denLiskov Substitution Principle (LSP) verbunden ist.

Einfach ausgedrückt gibt der LSP an, dassif an application works with a given base type, then it should also work with any of its subtypes. Auf diese Weise wird die Typersetzbarkeit ordnungsgemäß beibehalten.

Das größte Problem beim Überschreiben von Methoden besteht darin, dass einige bestimmte Methodenimplementierungen in den abgeleiteten Klassen möglicherweise nicht vollständig dem LSP entsprechen und daher die Typersubstituierbarkeit nicht beibehalten.

Natürlich ist es gültig, eine überschriebene Methode zu erstellen, um Argumente unterschiedlichen Typs zu akzeptieren und auch einen anderen Typ zurückzugeben, aber unter vollständiger Einhaltung dieser Regeln:

  • Wenn eine Methode in der Basisklasse Argumente eines bestimmten Typs akzeptiert, sollte die überschriebene Methode denselben Typ oder einen Supertyp (a.k.a. contravariant Methodenargumente)

  • Wenn eine Methode in der Basisklassevoid zurückgibt, sollte die überschriebene Methodevoid zurückgeben

  • Wenn eine Methode in der Basisklasse ein Grundelement zurückgibt, sollte die überschriebene Methode dasselbe Grundelement zurückgeben

  • Wenn eine Methode in der Basisklasse einen bestimmten Typ zurückgibt, sollte die überschriebene Methode denselben Typ oder einen Subtyp zurückgeben (a.k.a. covariant Rückgabetyp)

  • Wenn eine Methode in der Basisklasse eine Ausnahme auslöst, muss die überschriebene Methode dieselbe Ausnahme oder einen Subtyp der Basisklassenausnahme auslösen

3.2. Dynamische Bindung

In Anbetracht der Tatsache, dass das Überschreiben von Methoden nur mit Vererbung implementiert werden kann, wenn eine Hierarchie von Basistyp und Subtyp (en) vorhanden ist, kann der Compiler zur Kompilierungszeit nicht bestimmen, welche Methode aufgerufen werden soll, da sowohl die Basisklasse als auch die Unterklassen die definieren gleiche Methoden.

Folglich muss der Compiler den Objekttyp überprüfen, um zu wissen, welche Methode aufgerufen werden soll.

Da diese Überprüfung zur Laufzeit erfolgt, ist das Überschreiben von Methoden ein typisches Beispiel für die dynamische Bindung.

4. Fazit

In diesem Tutorial haben wir gelernt, wie Methodenüberladung und Methodenüberschreibung implementiert werden, und wir haben einige typische Situationen untersucht, in denen sie nützlich sind.

Wie üblich sind alle in diesem Artikel gezeigten Codebeispieleover on GitHub verfügbar.