Fragen zur OOP sind ein wesentlicher Bestandteil des technischen Interviews für eine Stelle als Java-Entwickler in einem IT-Unternehmen. In diesem Artikel werden wir über ein Prinzip der OOP sprechen: die Polymorphie. Wir konzentrieren uns auf die Aspekte, nach denen bei Interviews oft gefragt wird, und geben zur Verdeutlichung auch ein paar Beispiele.

Was ist Polymorphismus in Java?

Polymorphismus ist die Fähigkeit eines Programms, Objekte mit der gleichen Schnittstelle auf die gleiche Weise zu behandeln, ohne Informationen über den spezifischen Typ des Objekts. Wenn du eine Frage darüber beantwortest, was Polymorphismus ist, wirst du höchstwahrscheinlich aufgefordert zu erklären, was du gemeint hast. Ohne einen Haufen zusätzlicher Fragen auszulösen, kannst du deinem Gesprächspartner noch einmal alles erklären. Polymorphismus in Java - 1Du kannst damit beginnen, dass der OOP-Ansatz den Aufbau eines Java-Programms auf der Interaktion zwischen Objekten aufbaut, die auf Klassen beruhen. Klassen sind zuvor geschriebene Entwürfe (Vorlagen), die verwendet werden, um Objekte im Programm zu erstellen. Außerdem hat eine Klasse immer einen bestimmten Typ, der – bei gutem Programmierstil – einen Namen hat, der auf seinen Zweck hinweist. Da Java stark typisiert ist, muss der Programmcode immer einen Objekttyp angeben, wenn Variablen deklariert werden. Hinzu kommt, dass die strikte Typisierung die Sicherheit und Zuverlässigkeit des Codes verbessert und es möglich macht, Fehler aufgrund von Inkompatibilitäten zu vermeiden (z. B. beim Versuch, eine Zeichenkette durch eine Zahl zu dividieren). Natürlich muss der Compiler den deklarierten Typ „kennen“ – es kann eine Klasse aus dem JDK sein oder eine, die wir selbst erstellt haben. Weise den Interviewer darauf hin, dass unser Code nicht nur die Objekte des in der Deklaration angegebenen Typs verwenden kann, sondern auch seine Nachkommen. Das ist ein wichtiger Punkt: Wir können mit vielen verschiedenen Typen als einem einzigen Typ arbeiten (vorausgesetzt, dass diese Typen von einem Basistyp abgeleitet sind). Das bedeutet auch, dass wir, wenn wir eine Variable deklarieren, deren Typ eine Oberklasse ist, dieser Variable eine Instanz eines ihrer Nachkommen zuweisen können. Der Interviewer wird sicher gerne ein Beispiel von dir hören. Wähle eine Klasse aus, die von mehreren Klassen gemeinsam genutzt werden kann (eine Basisklasse für mehrere Klassen), und lass ein paar von ihnen diese erben. Basisklasse:
public class Dancer {
    private String name;
    private int age;

    public Dancer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void dance() {
        System.out.println(toString() + " I dance like everyone else.");
    }

    @Override
    public String toString() {
        Return "I'm " + name + ". I'm " + age + " years old.";
    }
}
In den Unterklassen überschreibst du die Methode der Basisklasse:
public class ElectricBoogieDancer extends Dancer {
    public ElectricBoogieDancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString () + " I dance the electric boogie!");
    }
}

public class Breakdancer extends Dancer {

    public Breakdancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString() + " I breakdance!");
    }
}
Ein Beispiel für Polymorphismus und wie diese Objekte in einem Programm verwendet werden können:
public class Main {

    public static void main(String[] args) {
        Dancer dancer = new Dancer("Fred", 18);

        Dancer breakdancer = new Breakdancer("Jay", 19); // Widening conversion to the base type
        Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20); // Widening conversion to the base type

        List<dancer> disco = Arrays.asList(dancer, breakdancer, electricBoogieDancer);
        for (Dancer d : disco) {
            d.dance(); // Call the polymorphic method
        }
    }
}
Zeige in der main-Methode, dass die Zeilen
Dancer breakdancer = new Breakdancer("Jay", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20);
eine Variable einer Oberklasse deklarieren und ihr ein Objekt zuweisen, das eine Instanz einer ihrer Nachkommen ist. Du wirst dich wahrscheinlich fragen, warum der Compiler nicht über die Inkonsistenz der deklarierten Typen auf der linken und rechten Seite des Zuweisungsoperators schimpft – schließlich ist Java stark typisiert. Erkläre, dass hier eine erweiternde Typumwandlung im Spiel ist – eine Referenz auf ein Objekt wird wie eine Referenz auf seine Basisklasse behandelt. Wenn ein solches Konstrukt im Code auftaucht, führt der Compiler die Umwandlung automatisch und implizit durch.

Der Beispielcode zeigt, dass der auf der linken Seite des Zuweisungsoperators deklarierte Typ (Dancer) mehrere Formen (Typen) hat, die auf der rechten Seite deklariert sind (Breakdancer, ElectricBoogieDancer). Jede Form kann ihr eigenes, einzigartiges Verhalten in Bezug auf die allgemeine Funktionalität haben, die in der Oberklasse definiert ist (die dance-Methode). Das heißt, eine Methode, die in einer Oberklasse deklariert wurde, kann in ihren Nachkommen anders implementiert werden. In diesem Fall haben wir es mit einer Methodenüberschreibung zu tun, die genau das ist, was mehrere Formen (Verhaltensweisen) erzeugt. Das kann man sehen, wenn man den Code in der main-Methode ausführt: Programmausgabe:

Ich bin Fred. Ich bin 18 Jahre alt. Ich tanze wie alle anderen. Ich bin Jay. Ich bin 19 Jahre alt. Ich tanze Breakdance! Ich bin Marcia. Ich bin 20 Jahre alt. Ich tanze den Electric Boogie!
Wenn wir die Methode in den Unterklassen nicht überschreiben, werden wir kein anderes Verhalten erhalten. Wenn wir zum Beispiel die dance-Methode in unseren Klassen Breakdancer und ElectricBoogieDancer auskommentieren, sieht die Ausgabe des Programms so aus:
Ich bin Fred. Ich bin 18 Jahre alt. Ich tanze wie alle anderen. Ich bin Jay. Ich bin 19 Jahre alt. Ich tanze wie alle anderen. Ich bin Marcia. Ich bin 20 Jahre alt. Ich tanze wie alle anderen.
Und das bedeutet, dass es einfach keinen Sinn ergibt, die Klassen Breakdancer und ElectricBoogieDancer zu erstellen.

Wo genau zeigt sich das Prinzip des Polymorphismus? Wo wird ein Objekt im Programm verwendet, ohne dass sein spezifischer Typ bekannt ist? In unserem Beispiel passiert das, wenn die dance()-Methode für das Objekt Dancer d aufgerufen wird. In Java bedeutet Polymorphismus, dass das Programm nicht wissen muss, ob das Objekt ein Breakdancer oder ein ElectricBoogieDancer ist. Wichtig ist, dass sie es Nachkomme der Klasse Dancer ist. Und wenn du Nachkommen erwähnst, solltest du beachten, dass Vererbung in Java nicht nur aus extends besteht, sondern auch implements umfasst. Jetzt ist es an der Zeit zu erwähnen, dass Java keine Mehrfachvererbung unterstützt – jeder Typ kann einen Elternteil (Oberklasse) und eine unbegrenzte Anzahl von Nachkommen (Unterklassen) haben. Dementsprechend werden Interfaces verwendet, um den Klassen mehrere Funktionssätze hinzuzufügen. Im Vergleich zu Unterklassen (Vererbung) sind Interfaces weniger mit der Elternklasse gekoppelt. Sie werden sehr häufig verwendet. In Java ist ein Interface ein Referenztyp, sodass das Programm eine Variable des Interface-Typs deklarieren kann. Sehen wir uns dazu ein Beispiel an. Erstelle ein Interface:

public interface CanSwim {
    void swim();
}
Zur Verdeutlichung nehmen wir verschiedene, nicht verwandte Klassen und lassen sie das Interface implementieren:
public class Human implements CanSwim {
    private String name;
    private int age;

    public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void swim() {
        System.out.println(toString()+" I swim with an inflated tube.");
    }

    @Override
    public String toString() {
        return "I'm " + name + ". I'm " + age + " years old.";
    }

}

public class Fish implements CanSwim {
    private String name;

    public Fish(String name) {
        this.name = name;
    }

    @Override
    public void swim() {
        System.out.println("I'm a fish. My name is " + name + ". I swim by moving my fins.");

    }

public class UBoat implements CanSwim {

    private int speed;

    public UBoat(int speed) {
        this.speed = speed;
    }

    @Override
    public void swim() {
        System.out.println("I'm a submarine that swims through the water by rotating screw propellers. My speed is " + speed + " knots.");
    }
}
main-Methode:
public class Main {

    public static void main(String[] args) {
        CanSwim human = new Human("John", 6);
        CanSwim fish = new Fish("Whale");
        CanSwim boat = new UBoat(25);

        List<swim> swimmers = Arrays.asList(human, fish, boat);
        for (Swim s : swimmers) {
            s.swim();
        }
    }
}
Die Ergebnisse des Aufrufs einer polymorphen Methode, die in einem Interface definiert ist, zeigen uns die Unterschiede im Verhalten der Typen, die dieses Interface implementieren. In unserem Fall sind das die verschiedenen Zeichenfolgen, die von der swim-Methode angezeigt werden. Nach dem Studium unseres Beispiels wird der Interviewer vielleicht fragen, warum das Ausführen dieses Codes in der main-Methode
for (Swim s : swimmers) {
            s.swim();
}
bewirkt, dass die in unseren Unterklassen definierten Überschreibungsmethoden aufgerufen werden? Wie wird die gewünschte Implementierung der Methode ausgewählt, während das Programm läuft? Um diese Fragen zu beantworten, musst du die späte (dynamische) Bindung erklären. Bindung bedeutet, eine Zuordnung zwischen einem Methodenaufruf und seiner spezifischen Klassenimplementierung herzustellen. Im Wesentlichen bestimmt der Code, welche der drei in den Klassen definierten Methoden ausgeführt wird. Java verwendet standardmäßig die späte Bindung, d. h. die Bindung erfolgt zur Laufzeit und nicht zur Kompilierzeit, wie es bei der frühen Bindung der Fall ist. Das bedeutet, dass der Compiler beim Kompilieren dieses Codes
for (Swim s : swimmers) {
            s.swim();
}
nicht weiß, welche Klasse (Human, Fish oder Uboat) den Code enthält, der ausgeführt wird, wenn die swim-Methode aufgerufen wird. Dies wird erst bei der Ausführung des Programms festgelegt, dank des dynamischen Bindungsmechanismus (Überprüfung des Objekttyps zur Laufzeit und Auswahl der richtigen Implementierung für diesen Typ). Wenn du gefragt wirst, wie das umgesetzt wird, kannst du antworten, dass die JVM beim Laden und Initialisieren von Objekten Tabellen im Speicher aufbaut und Variablen mit ihren Werten und Objekte mit ihren Methoden verknüpft. Wenn eine Klasse vererbt wird oder ein Interface implementiert, wird als erstes geprüft, ob es überschriebene Methoden gibt. Wenn es welche gibt, sind sie an diesen Typ gebunden. Wenn dies nicht der Fall ist, wird die Suche nach einer passenden Methode auf die Klasse verlagert, die eine Stufe höher liegt (die Elternklasse) und so weiter bis zur Wurzel in einer mehrstufigen Hierarchie. Wenn es um Polymorphie in der OOP und ihre Umsetzung im Code geht, ist es sinnvoll, abstrakte Klassen und Interfaces zu verwenden, um abstrakte Definitionen von Basisklassen bereitzustellen. Diese Praxis ergibt sich aus dem Prinzip der Abstraktion – gemeinsames Verhalten und gemeinsame Eigenschaften werden identifiziert und in einer abstrakten Klasse zusammengefasst oder nur gemeinsames Verhalten wird identifiziert und in einem Interface zusammengefasst. Der Entwurf und die Erstellung einer Objekthierarchie auf der Grundlage von Schnittstellen und Klassenvererbung sind erforderlich, um Polymorphismus zu implementieren. In Bezug auf Polymorphismus und Innovationen in Java stellen wir fest, dass es ab Java 8 möglich ist, bei der Erstellung von abstrakten Klassen und Interfaces das Schlüsselwort default zu verwenden, um eine Standardimplementierung für abstrakte Methoden in Basisklassen zu schreiben. So wie hier:
public interface CanSwim {
    default void swim() {
        System.out.println("I just swim");
    }
}
Manchmal fragen Interviewer danach, wie Methoden in Basisklassen deklariert werden müssen, damit das Prinzip der Polymorphie nicht verletzt wird. Die Antwort ist einfach: Diese Methoden dürfen nicht static, private oder final sein. private macht eine Methode nur innerhalb einer Klasse verfügbar, sodass du sie nicht in einer Unterklasse überschreiben kannst. static verknüpft eine Methode mit der Klasse und nicht mit einem Objekt, sodass die Methode der Oberklasse immer aufgerufen wird. Und final macht eine Methode unveränderlich und für Unterklassen unsichtbar.

Was bringt uns der Polymorphismus?

Höchstwahrscheinlich wirst du auch gefragt werden, wie Polymorphismus uns nützt. Du kannst diese Frage kurz beantworten, ohne dich in den ganzen Details zu verzetteln:
  1. Er macht es möglich, Klassenimplementierungen zu ersetzen. Das Testen ist darauf aufgebaut.
  2. Er erleichtert die Erweiterbarkeit und macht es viel einfacher, eine Grundlage zu schaffen, auf der in Zukunft aufgebaut werden kann. Das Hinzufügen neuer Typen, die auf bestehenden Typen basieren, ist die häufigste Art, die Funktionalität von OOP-Programmen zu erweitern.
  3. Damit kannst du Objekte, die einen gemeinsamen Typ oder ein gemeinsames Verhalten haben, in einer Collection oder einem Array zusammenfassen und sie einheitlich behandeln (wie in unseren Beispielen, in denen wir alle zu dance() oder swim() gezwungen haben :)
  4. Flexibilität bei der Erstellung neuer Typen: Du kannst dich für die Implementierung einer Methode durch die Elternklasse entscheiden oder sie in einer Unterklasse außer Kraft setzen.
Polymorphismus in Java - 2

Schlussworte

Polymorphismus ist ein sehr wichtiges und umfangreiches Thema. Er ist das Thema von fast der Hälfte dieses Artikels über OOP in Java und bildet einen großen Teil der Grundlage der Sprache. Du wirst nicht umhin kommen, dieses Prinzip in einem Interview zu definieren. Wenn du ihn nicht kennst oder nicht verstehst, wird das Gespräch wahrscheinlich abgebrochen. Sei also kein Faulpelz – überprüfe dein Wissen vor dem Interview und frische es bei Bedarf auf.