Vererbung, Interfaces und Polymorphie in Java

Lerne, wie Java-Klassen aufeinander aufbauen. Erweitere eine Klasse mit extends und super, überschreibe geerbte Methoden und ergänze deine Klassen um nützliche Methoden toString, equals und hashCode aus Object. Definiere gemeinsames Verhalten mit abstrakten Klassen und Interfaces, sage voraus, welche Methode durch Polymorphie und dynamische Bindung ausgeführt wird, und modelliere feste Auswahlmöglichkeiten und einfache Daten mit Aufzählungstypen und Records. Durchgerechnete Codebeispiele zeigen jeden Schritt und die häufigsten Fehler von Einsteigern.

AnfängerEinheit 8 von 11 in Kurs: Java-Programmierung – Klassen, Vererbung, Collections und Streams

Was du lernst

  1. Baue mit extends eine neue Klasse auf einer bestehenden auf, erkenne, was die neue Klasse erbt, und stelle mit super sicher, dass der geerbte Teil jedes Objekts korrekt initialisiert wird.
    • Erstelle eine Unterklasse mit extends
      Deklariere eine Unterklasse, ermittle, welche Mitglieder sie erbt und verwenden kann, und entscheide mit dem Ist-ein-Test, ob Vererbung passt.
    • Initialisiere Objekte mit Oberklassenkonstruktoren
      Übergib Werte mit super(...) an den Oberklassenkonstruktor, sage die Ausführungsreihenfolge der Konstruktoren voraus und vermeide die klassischen Konstruktorfehler.
  2. Ändere geerbtes Verhalten durch das Überschreiben von Methoden, baue mit super auf der Version der Oberklasse auf und ergänze deine Klassen um aussagekräftige Methoden toString, equals und hashCode aus Object.
    • Überschreibe geerbte Methoden
      Ersetze oder erweitere eine geerbte Methode durch eine korrekte Überschreibung, rufe die Version der Oberklasse mit super auf und unterscheide Überschreiben vom Überladen.
    • Verwende und überschreibe die Methoden von Object
      Erkläre, warum jedes Objekt toString, equals, hashCode und getClass hat, lies ihr Standardverhalten ab und schreibe eine nützliche toString-Methode.
    • Überschreibe equals und hashCode korrekt
      Definiere mit einer korrekten Methode equals(Object), wann zwei Objekte gleich sind, implementiere hashCode passend dazu und behandle Gleichheit über eine Klassenhierarchie hinweg.
  3. Verwende abstrakte Klassen, um Code zwischen eng verwandten Klassen zu teilen, und Interfaces, um Verhalten für nicht verwandte Klassen zuzusichern. Wähle für einen Entwurf das passende Mittel.
    • Schreibe abstrakte Klassen und abstrakte Methoden
      Deklariere abstrakte Klassen und Methoden, vervollständige sie in konkreten Unterklassen und verwende eine abstrakte Klasse, um Felder und Code zu teilen.
    • Definiere und implementiere Interfaces
      Schreibe ein Interface als Vertrag, implementiere es in einer oder mehreren Klassen und kombiniere Interfaces mit Vererbung.
    • Ergänze Interfaces um Default-Methoden, statische und private Methoden
      Gib Interfaces mit Default-Methoden gemeinsames Verhalten, ergänze Hilfsmethoden als statische und private Methoden und löse Konflikte zwischen Default-Methoden.
    • Wähle zwischen einer abstrakten Klasse und einem Interface
      Vergleiche abstrakte Klassen und Interfaces und wähle für einen bestimmten Entwurf die passende Lösung oder eine Kombination aus beiden.
  4. Greife über allgemeine Typen auf Objekte zu, sage voraus, welche Methode zur Laufzeit ausgeführt wird, wandle Typen sicher um und schreibe Code, der auch beim Hinzufügen neuer Unterklassen funktioniert.
    • Greife über Oberklassen- und Interface-Typen auf Objekte zu
      Unterscheide den deklarierten Typ einer Variablen vom dynamischen Typ des Objekts und verwende Oberklassen- und Interface-Typen für Variablen, Parameter und Listen.
    • Sage voraus, welche Methode durch dynamische Bindung ausgeführt wird
      Verfolge Methodenaufrufe durch eine Klassenhierarchie und unterscheide, was der Compiler und was erst zur Laufzeit entschieden wird.
    • Wandle Referenzen mit Casts um und prüfe ihre Typen mit instanceof
      Führe Downcasts mit instanceof und Mustererkennung sicher aus, erkenne eine ClassCastException und verstehe, wann ein Typtest auf eine fehlende überschriebene Methode hinweist.
    • Entwirf flexiblen Code mit Polymorphie
      Schreibe Methoden und Collections für allgemeine Typen, sodass neue Klassen ohne Änderungen hineinpassen, und mache Klassen mit Comparable sortierbar.
  5. Stelle eine feste Auswahl mit einem Aufzählungstyp und ein einfaches Datenelement mit einem Record dar und verwende die Mitglieder, die der Compiler für dich erzeugt.
    • Stelle feste Wertevorräte mit Aufzählungstypen dar
      Deklariere Aufzählungstypen, verwende ihre eingebauten Methoden und switch und gib Aufzählungskonstanten Felder, Konstruktoren und Verhalten.
    • Modelliere Daten mit Records
      Deklariere Records für einfache Daten, verwende den generierten Konstruktor, die Zugriffsmethoden, equals, hashCode und toString und validiere Daten in einem kompakten Konstruktor.

Vererbung in Java einfach erklärt

5 Kapitel

1

Erweitere Klassen durch Vererbung

Eine Unterklasse beginnt mit den Feldern und dem Verhalten einer bestehenden Klasse und ergänzt dann, was sie unterscheidet. Diese Beziehung beeinflusst sowohl, wie anderer Code das Objekt verwenden kann, als auch, wie Java es erzeugt. Verfolge eine Person und einen Studenten durch ihre Deklarationen und Konstruktoren: Private Daten bleiben unter der Kontrolle der Oberklasse, und Konstruktoraufrufe liefern die Werte, die der geerbte Zustand benötigt.

1.1 Erstelle eine Unterklasse und schütze ihre Daten

Vererbung erzeugt eine Unterklasse aus einer bestehenden Oberklasse. In class Student extends Person ist jeder Student eine Person. Ein Studentenobjekt enthält seinen Personenzustand und seinen zusätzlichen Studentenzustand zusammen in einem Objekt.

Ergänze die studentenspezifischen Teile

Hier sind eine kleine Oberklasse und eine Unterklasse:

class Person {
    private String name;
    public Person(String name) { this.name = name; }
    public String getName() { return name; }
}

class Student extends Person {
    private int credits;
    public Student(String name) { super(name); }
    public void study() { credits += 5; }
    public int getCredits() { return credits; }
}

Student erbt getName(). Die Klasse ergänzt ein Feld für Leistungspunkte und zwei Methoden. Nach Student s = new Student("Ada Lovelace"); beginnt das Feld für Leistungspunkte bei 0. Zwei Aufrufe von s.study() erhöhen den Wert zuerst auf 5 und dann auf 10. s.getName() gibt weiterhin „Ada Lovelace“ zurück: Die geerbte Methode liest das in Person deklarierte private Feld.

Die Anweisung super(name) übergibt den Namen an den Konstruktor von Person. Konstruktoren werden nicht geerbt, daher deklariert Student einen eigenen Konstruktor. Eine Unterklasse darf Methoden ergänzen und geerbtes Verhalten überschreiben, kann aber eine geerbte öffentliche Methode nicht aus ihrer öffentlichen Schnittstelle entfernen.

Bestimme, auf welche Mitglieder zugegriffen werden kann
ZugriffsmodifikatorWo auf das Mitglied zugegriffen werden kann
publicJede Klasse mit Zugriff auf den deklarierenden Typ
protectedDasselbe Paket sowie Zugriff durch Unterklassen über Paketgrenzen hinweg
Kein ModifikatorDasselbe Paket
privateDie deklarierende Klasse

Private Mitglieder werden nicht geerbt. Das private Feld name existiert weiterhin in jedem Studentenobjekt, aber Student kann es nicht direkt lesen. Indem Person das Feld privat hält und einen Getter bereitstellt, kontrolliert die Klasse ihre eigenen Daten. Über Paketgrenzen hinweg gilt für den protected-Instanzzugriff eine zusätzliche Einschränkung: Der Code der Unterklasse greift über sich selbst oder eine Referenz ihres eigenen Unterklassentyps auf das Mitglied zu, nicht über eine beliebige Oberklassenreferenz.

Prüfe die Beziehung vor dem Erweitern

Java erlaubt eine direkte Oberklasse pro Klasse. Eine Kette wie GraduateStudent extends Student und Student extends Person ist gültig; extends Student, Teacher ist es nicht. Eine Klasse ohne explizite Oberklasse erweitert Object. Object selbst hat keine Oberklasse.

Verwende den Ist-ein-Test: Kann jedes Unterklassenobjekt überall dort verwendet werden, wo die Oberklasse erwartet wird? Ein Lastwagen ist ein Fahrzeug. Ein Kunde hat eine Adresse, aber ein Kunde ist keine Adresse. Verwende für die zweite Beziehung Komposition:

class Customer {
    private Address address;
}

Hier ist Address eine andere Klasse. Das Feld ermöglicht einem Kunden, eine Adresse zu verwenden, ohne zu behaupten, selbst eine zu sein. Wer eine Klasse nur erweitert, um eine praktische Methode zu übernehmen, erbt auch ihren gesamten öffentlichen Vertrag. Schließlich kann eine final-Klasse nicht erweitert werden. Weil String final ist, lässt sich class ShortString extends String nicht kompilieren.

Wichtige Erkenntnisse

  • Ein Unterklassenobjekt enthält den Zustand der Oberklasse und jeden Zustand, den die Unterklasse ergänzt.
  • Auf private Felder der Oberklasse wird über deren Methoden zugegriffen.
  • Eine Klasse erweitert eine direkte Oberklasse; Konstruktoren werden nicht geerbt.
  • Verwende Vererbung für eine Ist-ein-Beziehung und Komposition für eine Hat-ein-Beziehung.
  • Eine final-Klasse kann nicht erweitert werden.

1.2 Übergib Werte durch eine Konstruktorkette

Ein Konstruktor einer Unterklasse muss die Initialisierung des Oberklassenteils desselben Objekts veranlassen. Er kann einen Oberklassenkonstruktor mit super(...) aufrufen oder mit this(...) an einen anderen Konstruktor seiner eigenen Klasse delegieren.

Wähle den Oberklassenkonstruktor
class Person {
    private final String name;
    Person(String name) { this.name = name; }
}
class Student extends Person {
    private final String studentId;
    Student(String name, String studentId) {
        super(name);
        this.studentId = studentId;
    }
    Student(String name) {
        this(name, "no id yet");
    }
}

Der Konstruktor mit zwei Argumenten übergibt den Namen an Person und speichert die Kennung selbst. Der Konstruktor mit einem Argument delegiert an ihn. Ein Konstruktor enthält höchstens einen expliziten Konstruktoraufruf: this(...) oder super(...). Verfolgst du die this(...)-Kette, erreichst du schließlich einen Konstruktor, der super(...) aufruft.

Steht keiner der beiden Aufrufe im Code, fügt Java super() ein. Das schlägt hier fehl, weil Person nur einen Konstruktor hat, der einen Namen benötigt. Sobald irgendein Konstruktor deklariert wird, stellt Java keinen parameterlosen Standardkonstruktor bereit. Eine Klasse ohne deklarierten Konstruktor erhält einen Standardkonstruktor, der super() aufruft.

new Person(name) innerhalb von Student bewirkt etwas anderes: Es erzeugt ein separates Objekt. Es initialisiert weder den Personenzustand des aktuellen Studenten noch ersetzt es den erforderlichen Aufruf des Oberklassenkonstruktors.

Verfolge die Initialisierung in ihrer Reihenfolge
class Base {
    Base() { System.out.println("Base ready"); }
}
class Middle extends Base {
    Middle() { System.out.println("Middle ready"); }
}
class Top extends Middle {
    Top() { super(); System.out.println("Top ready"); }
}

new Top() durchläuft die Konstruktorkette bis zu Object. Bei der Rückkehr aus den Aufrufen lautet die Ausgabe „Base ready“, „Middle ready“ und dann „Top ready“. Die Feldinitialisierer jeder Klasse werden ausgeführt, nachdem ihr Oberklassenkonstruktor zurückgekehrt ist und bevor der restliche Konstruktorrumpf dieser Klasse ausgeführt wird.

Eine überschreibbare Methode, die ein Oberklassenkonstruktor aufruft, kann bereits eine Überschreibung der Unterklasse ausführen. Liest diese Überschreibung ein Referenzfeld der Unterklasse, dessen Zuweisung noch nicht ausgeführt wurde, sieht sie null, selbst wenn das Feld einen Initialisierer in der Deklaration hat. Beschränke die Arbeit von Konstruktoren auf das Initialisieren des Zustands; vermeide Aufrufe von Methoden, die Unterklassen überschreiben können.

Erkenne die Änderung in Java 25

In Java 25 kann ein Konstruktor Parameter vor seinem expliziten Konstruktoraufruf validieren:

Student(String name, String studentId, int credits) {
    if (credits < 0) {
        throw new IllegalArgumentException("negative credits");
    }
    super(name);
    this.studentId = studentId;
}

Java 25 erlaubt außerdem einfache vorgezogene Zuweisungen an Felder, die in der aktuellen Klasse ohne Initialisierer in der Deklaration definiert sind. Weiterhin verboten ist es, vor dem Aufruf Felder des aktuellen Objekts zu lesen, seine Instanzmethoden aufzurufen oder this weiterzugeben. Bei Standard-Sprachversionen vor Java 25 muss super(...) oder this(...) an erster Stelle stehen. Java 22–24 bot frühere Formen als Vorschaufunktionen an.

Wichtige Erkenntnisse

  • super(...) initialisiert den Oberklassenteil des aktuellen Objekts.
  • Ein implizites super() benötigt einen zugänglichen parameterlosen Oberklassenkonstruktor.
  • this(...) delegiert innerhalb derselben Klasse; seine Kette muss einen Oberklassenkonstruktor erreichen.
  • Feldinitialisierer werden ausgeführt, nachdem der Oberklassenkonstruktor zurückgekehrt ist.
  • Java 25 erlaubt eingeschränkte Arbeiten vor einem expliziten Konstruktoraufruf.
  • Eine während der Initialisierung aufgerufene Überschreibung kann auf Felder der Unterklasse zugreifen, bevor ihnen Werte zugewiesen wurden.
2

Überschreibe Methoden und verwende die Klasse Object

Geerbte Methoden geben verwandten Objekten einen gemeinsamen Ausgangspunkt. Durch Überschreiben kann jede Unterklasse auf denselben Aufruf auf eigene Weise reagieren, während die von Object geerbten Methoden bestimmen, wie ein Objekt ausgegeben und mit anderen verglichen wird. Kleine Unterschiede in der Signatur sind dabei wichtig: Eine Methode kann für Lesende richtig aussehen und dennoch die Methode nicht ersetzen, die Bibliothekscode tatsächlich aufruft. Auch die Gleichheit ist an Zusagen gebunden, auf die sich hashbasierte Collections verlassen.

2.1 Ersetze geerbtes Verhalten durch Überschreiben

Eine Überschreibung gibt einer Unterklasse eine eigene Implementierung einer geerbten Instanzmethode. Ihr Name und ihre Parametertypen stimmen mit der ursprünglichen Methode überein, sodass Aufrufer weiterhin dieselbe Operation verwenden können.

Erweitere eine funktionierende Methode

Angenommen, das Jahresgehalt eines Beschäftigten beträgt zwölf Monatsgehälter. Eine Führungskraft erhält außerdem einen jährlichen Bonus:

class Employee {
    private final double monthlySalary;
    Employee(double monthlySalary) {
        this.monthlySalary = monthlySalary;
    }
    public double yearlyPay() { return 12 * monthlySalary; }
}
class Manager extends Employee {
    private final double bonus;
    Manager(double monthlySalary, double bonus) {
        super(monthlySalary);
        this.bonus = bonus;
    }
    @Override
    public double yearlyPay() {
        return super.yearlyPay() + bonus;
    }
}

Für new Manager(3000.0, 5000.0) gibt der Aufruf der Oberklassenmethode 36.000,0 zurück. Die Addition von 5.000,0 ergibt 41.000,0. super.yearlyPay() wählt ausdrücklich die Implementierung der Oberklasse. Die Oberklassenmethode bleibt innerhalb der Unterklasse verfügbar, obwohl gewöhnliche Aufrufe auf dem Manager die Überschreibung ausführen.

return yearlyPay() + bonus; ruft die eigene Methode des Managers erneut auf. Kein Aufruf wird abgeschlossen, sodass die Ausführung schließlich mit StackOverflowError fehlschlägt. Das fehlende super. ändert die Bedeutung des Aufrufs.

Lass den Compiler die Signatur prüfen

Setze @Override über jede überschreibende Methode. Ein Schreibfehler wie yearlypay() oder ein zusätzlicher Parameter führt dann zu einem Kompilierfehler. Ohne die Annotation akzeptiert Java möglicherweise eine neue Methode, die Aufrufer nie verwenden.

Eine gültige Überschreibung folgt diesen Regeln:

  • Ihre Parametertypen stimmen exakt überein.
  • Ihr Rückgabetyp ist gleich oder bei der Rückgabe eines Objekttyps ein Untertyp. Diese erlaubte Einengung heißt kovarianter Rückgabetyp.
  • Ihr Zugriff ist mindestens so offen wie der Zugriff auf die geerbte Methode.
  • Sie fügt keine geprüften Ausnahmen hinzu, die die geerbte Deklaration verbietet.

Zum Beispiel kann public String toString() nicht durch String toString() mit Paketzugriff ersetzt werden. Bestehende Aufrufer müssen den Zugriff behalten.

Unterscheide Überschreiben von Überladen und Verdecken
DeklarationWirkung
Dieselbe Instanzmethodensignatur in einer UnterklasseÜberschreibt geerbtes Verhalten
Derselbe Name, andere ParameterlisteFügt eine Methodenüberladung hinzu
Dieselbe statische Methodensignatur in einer UnterklasseVerdeckt eine statische Methode

role() und role(String language) sind überladene Methoden; die zweite ersetzt die erste nicht. Die Auswahl der überladenen Methode erfolgt zur Kompilierzeit anhand der deklarierten Argumenttypen. Auch statische Methoden werden anhand des zur Kompilierzeit verwendeten Typs ausgewählt und daher nicht dynamisch überschrieben.

Eine private Methode wird nicht geerbt; eine gleichnamige Methode der Unterklasse ist unabhängig davon. Eine final-Methode kann nicht überschrieben werden, und Konstruktoren können nicht überschrieben werden. Über die Kompilierbarkeit hinaus muss die Methode weiterhin ihren ursprünglichen Zweck erfüllen. Eine Überschreibung für eine Führungskraft, die das Monatsgehalt zurückgibt, würde Code zur Berechnung von Jahresgehaltssummen unbrauchbar machen, selbst wenn ihre Signatur korrekt wäre.

Wichtige Erkenntnisse

  • Eine Überschreibung stimmt in Name und Parametertypen mit einer geerbten Instanzmethode überein.
  • @Override deckt versehentliche Überladungen und Schreibfehler auf.
  • super.method() verwendet die Implementierung der Oberklasse wieder.
  • Eine Überschreibung darf den Zugriff nicht einschränken oder die erlaubten geprüften Ausnahmen erweitern.
  • Überladen ändert die Parameter; statische Methoden werden verdeckt, und final-Methoden können nicht überschrieben werden.

2.2 Gib Objekten eine nützliche Textdarstellung und bestimme ihre Klasse

Jede Java-Klasse erweitert Object direkt oder indirekt. Auch Arrays sind Objekte. Diese gemeinsame Wurzel stellt Methoden wie toString(), equals(Object), hashCode() und getClass() bereit, selbst wenn eine Klasse keine davon deklariert.

Ersetze die ausgegebene Standardbeschreibung

Das geerbte Object.toString() erzeugt einen Klassennamen, @ und den Hashcode des Objekts in hexadezimaler Schreibweise. Ein Text wie „school.Person@1b6d3586“ bezeichnet die Klasse, erläutert aber nicht die Daten der Person. Die Zahl kann sich zwischen Programmläufen ändern und ist keine von Java zugesicherte Speicheradresse.

Eine nützliche Überschreibung zeigt die Felder, die Lesende benötigen. Hier hat ein Student getName() geerbt und speichert ein Feld credits:

@Override
public String toString() {
    return getName() + " (" + credits + " credits)";
}

Für einen Studenten namens Ada Lovelace mit 10 Leistungspunkten gibt System.out.println(s) „Ada Lovelace (10 credits)“ aus. Der Ausgabeaufruf ermittelt den Text des Objekts automatisch. Die String-Konkatenation tut das ebenfalls: "Student: " + s enthält dieselbe Beschreibung.

Bei einer Nullreferenz erzeugen Ausgabe und Konkatenation „null“. Ein expliziter Aufruf s.toString(), wenn s null ist, wirft eine NullPointerException, weil kein Objekt vorhanden ist, das den Aufruf entgegennehmen kann.

Wenn Person.toString() bereits den Namen der Person zurückgibt, kann eine Unterklasse darauf aufbauen:

@Override
public String toString() {
    return super.toString() + ", student id " + studentId;
}

Für die Kennung „S-17“ ergibt das „Ada Lovelace, student id S-17“. Wie nützlich super.toString() ist, hängt davon ab, was die Oberklasse tatsächlich zurückgibt. Verwendet sie noch die Version von Object, steuert sie den Standardtext aus Klasse und Hashcode bei.

Halte Identität und Klasseninformation auseinander

Object.equals(Object) vergleicht die Identität: Die Methode gibt wahr zurück, wenn beide Referenzen auf dasselbe Objekt verweisen. Zwei separate new Person("Ada")-Objekte sind nach dieser geerbten Implementierung trotz übereinstimmender Namen nicht gleich. Klassen wie String, Integer und ArrayList stellen Überschreibungen für den Wertvergleich bereit.

getClass() meldet die tatsächliche Laufzeitklasse:

Person p = new Student("Ada Lovelace");
System.out.println(p.getClass().getSimpleName());

Dies gibt „Student“ aus, wenn Student den gezeigten Konstruktor hat. Der deklarierte Typ der Variablen ändert die Klasse des Objekts nicht. getClass() ist final und kann nicht überschrieben werden.

hashCode() unterstützt hashbasierte Collections. Zu den weiteren Methoden von Object gehören das geschützte clone() und die Methoden zur Koordination von Threads wait(), notify() und notifyAll().

Prüfe die genaue Schreibweise

Java unterscheidet toString() von tostring(). Eine Methode toString(String prefix) ist eine Überladung und wird daher bei der Ausgabe nicht aufgerufen. Beide Fehler werden durch @Override erkannt. Eine Textdarstellung definiert auch keine Gleichheit: Zwei Objekte können denselben Text ausgeben und beim Vergleich dennoch ungleich sein.

Wichtige Erkenntnisse

  • Object stellt allen Java-Objekten einschließlich Arrays gemeinsame Methoden bereit.
  • Ausgabe und String-Konkatenation verwenden toString() und stellen eine Nullreferenz als „null“ dar.
  • Object.equals vergleicht die Identität, bis eine Klasse die Methode überschreibt.
  • getClass() gibt die tatsächliche Klasse zurück und ist final.

2.3 Halte Gleichheitsvergleich und Hashcode konsistent

Zwei Objekte können denselben Wert darstellen, ohne dasselbe Objekt zu sein. Ein Bücherkatalog kann zwei Buchobjekte als gleich behandeln, wenn ihr Katalogcode und ihre Auflage übereinstimmen. Definiere diese Regel durch Überschreiben von equals(Object) und halte die Hashcode-Berechnung dazu konsistent.

Vergleiche die Felder, die den Wert bestimmen
import java.util.Objects;

final class Book {
    private final String code;
    private final int edition;
    Book(String code, int edition) {
        this.code = code;
        this.edition = edition;
    }
    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof Book b)) return false;
        return edition == b.edition && Objects.equals(code, b.code);
    }
    @Override
    public int hashCode() {
        return Objects.hash(code, edition);
    }
}

Die Identitätsprüfung akzeptiert dasselbe Objekt schnell. Die Typprüfung lehnt ein nicht verwandtes Objekt oder null ab und führt dann b als Book ein. Für die primitiven Auflagenwerte wird == verwendet; Objects.equals vergleicht Codes auch dann sicher, wenn ein Code null ist. Zwei Bücher mit dem Code „B-1042“ und der Auflage 2 gelten als gleich. Auflage 3 ergibt einen anderen Wert.

Der Parameter muss Object sein. equals(Book other) fügt eine Überladung hinzu und belässt das geerbte equals(Object). Bibliothekscode, der diese Signatur aufruft, würde weiterhin die Identität vergleichen. @Override lässt die falsche Deklaration bei der Kompilierung fehlschlagen.

Halte die Gleichheitszusagen ein

Für Referenzen, die nicht null sind, muss Gleichheit folgende Eigenschaften haben:

  • Reflexiv: Ein Objekt ist gleich sich selbst.
  • Symmetrisch: Das Vertauschen der beiden Objekte ergibt dieselbe Antwort.
  • Transitiv: Ist das erste gleich dem zweiten und das zweite gleich dem dritten, ist das erste gleich dem dritten.
  • Konsistent: Wiederholte Vergleiche stimmen überein, solange die verglichenen Daten unverändert bleiben.

Ein Objekt muss beim Vergleich mit null außerdem ungleich sein. Gleiche Objekte müssen gleiche Hashcodes zurückgeben. Ungleiche Objekte dürfen denselben Hashcode haben: Eine Hashkollision belegt keine Gleichheit. Leite den Hashcode aus denselben Feldern wie die Gleichheit ab, wie es hier Objects.hash(code, edition) tut.

Eine Hash-Collection verwendet den Hashcode, um Kandidaten zu finden, bevor sie deren Gleichheit prüft. Wird nur equals überschrieben, kann eine Menge separate gleiche Bücher behalten oder eine Map einen gleichen Schlüssel nicht finden. Wird ein für den Hashcode verwendetes Feld geändert, während das Objekt als Schlüssel gespeichert ist, kann das Objekt unter seinem alten Hashcode abgelegt bleiben.

Schütze die Symmetrie über Unterklassen hinweg

Angenommen, eine erweiterbare Klasse Book akzeptiert jedes instanceof Book, während Ebook die Dateigröße in den Gleichheitsvergleich aufnimmt und nur E-Books akzeptiert. Ein Buch kann ein E-Book mit demselben Code und derselben Auflage akzeptieren, aber das E-Book lehnt das Buch ab. Der Vergleich ist nicht mehr symmetrisch.

Ein Ansatz verlangt übereinstimmende Laufzeitklassen und prüft vor getClass() auf null. Ein anderer macht die Gleichheitsmethode der Oberklasse final, sodass Unterklassen die Gleichheitsregel nicht erweitern können. Eine finale Werteklasse wie oben vermeidet Unterklassen und damit auch deren Gleichheitsvergleich. Records erzeugen Gleichheitsvergleich und Hashcode-Berechnung aus ihren Komponenten; Aufzählungskonstanten besitzen bereits einen identitätsbasierten Gleichheitsvergleich, der zu ihren festen Werten passt.

Wichtige Erkenntnisse

  • Überschreibe equals(Object), nicht eine Überladung mit der Klasse als Parameter.
  • Gleichheit muss reflexiv, symmetrisch, transitiv und konsistent sein; der Vergleich mit null ist falsch.
  • Gleiche Objekte benötigen gleiche Hashcodes; ein gemeinsamer Hashcode belegt keine Gleichheit.
  • Halte gleichheitsrelevante Felder unverändert, während ein Objekt Schlüssel einer Hash-Collection oder Element einer Menge ist.
  • Oberklasse und Unterklasse dürfen keine Gleichheitsregeln verwenden, die beim Vertauschen der Operanden zu unterschiedlichen Ergebnissen führen.
  • Records und Aufzählungstypen bieten ein geeignetes eingebautes Verhalten für den Gleichheitsvergleich.
3

Definiere Verträge mit abstrakten Klassen und Interfaces

Manche verwandte Klassen teilen Felder und funktionierende Methoden; andere Klassen haben nur eine Fähigkeit gemeinsam. Abstrakte Klassen und Interfaces drücken diese unterschiedlichen Beziehungen aus. Ein Zeichenprogramm kann gemeinsame Formdaten in einer abstrakten Klasse verwalten, während ein Ladeprogramm über ein Interface mit nicht verwandten Geräten arbeitet. Beide Ansätze ermöglichen es dem Compiler, das erforderliche Verhalten vor der Programmausführung zu prüfen, und Java bietet genaue Regeln, wenn geerbte Implementierungen miteinander konkurrieren.

3.1 Teile Zustand und verlange Verhalten von Unterklassen

Eine abstrakte Klasse ist eine Basisklasse, die nicht direkt instanziiert werden kann. Sie kann gemeinsame Felder und funktionierende Methoden enthalten und zugleich ausgewählte Operationen den Unterklassen zur Vervollständigung überlassen.

Lass eine erforderliche Operation abstrakt
abstract class Shape {
    private final String name;
    Shape(String name) { this.name = name; }
    public abstract double area();
    public String describe() {
        return name + " with area " + area();
    }
}
class Rectangle extends Shape {
    private final double width;
    private final double height;
    Rectangle(double width, double height) {
        super("rectangle");
        this.width = width;
        this.height = height;
    }
    @Override
    public double area() { return width * height; }
}

area() hat eine Signatur und ein Semikolon, aber keinen Rumpf. Die Methode legt fest, was jede konkrete Form bereitstellen muss. Weil Shape eine abstrakte Methode enthält, muss auch die Klasse abstrakt sein. Rectangle stellt die fehlende Implementierung bereit und kann instanziiert werden.

Für new Rectangle(3.0, 4.0) speichert der Oberklassenkonstruktor „rectangle“. Der Konstruktor der Unterklasse speichert die Abmessungen. Die Fläche beträgt 12,0 Quadratmeter, wenn die Abmessungen in Metern angegeben sind. Der Aufruf von describe() gibt „rectangle with area 12.0“ zurück. Der Aufruf von area() in der geerbten Methode führt die Implementierung des Rechtecks aus.

Verwende Konstruktoren und konkrete Methoden wie gewohnt

Eine abstrakte Klasse kann Felder, Konstruktoren und Methoden mit Rumpf sowie den üblichen Zugriffsstufen haben. Ihr Konstruktor wird als Teil der Erzeugung eines Unterklassenobjekts ausgeführt. new Shape() ist selbst dann verboten, wenn ein passender Konstruktor existiert; new Rectangle(3.0, 4.0) ist gültig.

Eine zweite Unterklasse kann dieselbe geerbte Beschreibung verwenden und die Fläche anders berechnen. Eine Implementierung für einen Kreis gibt Math.PI * radius * radius zurück. Bei einem Radius von 1,5 m lautet die Rechnung π mal 2,25, also etwa 7,07 Quadratmeter. Die gemeinsam genutzte Methode describe() braucht keinen kreisspezifischen Zweig.

Eine Variable kann weiterhin den abstrakten Typ haben: Shape s = new Rectangle(3.0, 4.0);. Java verbietet die Erzeugung der abstrakten Klasse, nicht den Verweis auf ein konkretes Objekt über diesen Typ.

Mache fehlende Implementierungen zum Kompilierfehler

Jede konkrete Unterklasse muss Implementierungen aller abstrakten Methoden bereitstellen oder erben. Eine Unterklasse, die einige davon unvollendet lässt, muss ebenfalls als abstrakt deklariert werden. Würde ein Platzhalter area() den Wert 0 zurückgeben, ließe sich eine unvollständige konkrete Unterklasse kompilieren und würde unbemerkt falsche Ergebnisse liefern. Deklarierst du area() als abstrakt, wird die fehlende Implementierung für den Compiler sichtbar.

Abstrakte Methoden können nicht private, static oder final sein: Jede dieser Festlegungen würde die Überschreibung in der Unterklasse verhindern, die die Operation bereitstellt. Eine abstrakte Klasse kann auch keine abstrakten Methoden enthalten; der Modifikator verhindert dann nur die direkte Erzeugung. Verwende diese Art von Basisklasse, wenn ihre Unterklassen den Ist-ein-Test bestehen und von gemeinsamem Zustand oder Code profitieren.

Wichtige Erkenntnisse

  • Eine abstrakte Klasse kann nicht direkt erzeugt werden, ihr Konstruktor wird aber für Unterklassenobjekte ausgeführt.
  • Eine abstrakte Methode hat keinen Rumpf und erfordert eine konkrete Implementierung.
  • Eine konkrete Unterklasse muss jede geerbte abstrakte Methode vervollständigen.
  • Ein abstrakter Typ kann für Variablen, Parameter und Listen verwendet werden.
  • Abstrakte Klassen können gemeinsame Felder, funktionierende Methoden und erforderliche Operationen verbinden.

3.2 Gib nicht verwandten Klassen einen gemeinsamen Vertrag

Ein Interface benennt eine Fähigkeit, die mehrere Klassen bereitstellen können. Es legt die Methoden fest, die über diese Fähigkeit verfügbar sind, ohne zu verlangen, dass die Klassen dieselben Instanzfelder oder dieselbe Oberklasse haben.

Deklariere und implementiere die Methoden
interface Chargeable {
    int batteryPercent();
    void charge(int minutes);
}
class Phone implements Chargeable {
    private int battery;
    Phone(int battery) { this.battery = battery; }
    @Override
    public int batteryPercent() { return battery; }
    @Override
    public void charge(int minutes) {
        battery = Math.min(100, battery + 2 * minutes);
    }
}

Die abstrakten Methoden des Interfaces sind implizit public. Die implementierenden Methoden müssen ausdrücklich public sein; das Weglassen dieses Modifikators würde den Zugriff einschränken und zu einem Kompilierfehler führen. Eine konkrete Klasse muss für jede abstrakte Methode eine geeignete öffentliche Implementierung bereitstellen oder erben. Eine abstrakte implementierende Klasse darf einige davon ihren Unterklassen überlassen.

Bei einem Telefon mit 35 % Ladung fügt charge(20) 40 Prozentpunkte hinzu und ergibt 75 %. Weitere 20 Minuten würden vor der Begrenzung 115 ergeben, daher speichert Math.min den Wert 100. Ein ElectricScooter kann dasselbe Interface implementieren und dabei nur einen Punkt pro Minute hinzufügen: Ausgehend von 50 % erreicht er nach 20 Minuten 70. Das Interface legt die verfügbare Operation fest, während die Klasse deren Verhalten bereitstellt.

Verwende das Interface als Typ
Chargeable device = new Phone(35);
device.charge(20);
System.out.println(device.batteryPercent());

Dies gibt 75 aus. Aufrufender Code kann die Methoden des Interfaces und die öffentlichen Methoden verwenden, die auf jedem Objekt verfügbar sind. Operationen, die nur das Telefon bietet, sind über device nicht verfügbar, sofern das deklarierte Interface sie nicht bereitstellt.

Interfaces können auch Typen von Parametern, Rückgabewerten und Listenelementen sein. Eine Methode, die Chargeable akzeptiert, kann mit einem Telefon oder einem Roller arbeiten, ohne deren Feldstruktur zu kennen. Die Java-Bibliothek verwendet denselben Ansatz: List hat Implementierungen wie ArrayList und LinkedList; Runnable deklariert run(); Comparable deklariert den natürlichen Vergleich.

Kombiniere mehrere Fähigkeiten

Eine Klasse erweitert eine Klasse und kann mehrere Interfaces implementieren. Die Reihenfolge lautet class Phone extends Device implements Chargeable, Connectable. Ein Interface kann mehrere Interfaces erweitern: interface SmartDevice extends Chargeable, Connectable. Implementierende Klassen müssen dann alle geerbten abstrakten Operationen bereitstellen.

Ein Interface hat keine Konstruktoren und keine Instanzfelder. new Chargeable() lässt sich nicht kompilieren. Ein deklariertes Feld wie int FULL = 100; ist implizit eine public static final-Konstante, auf die mit Chargeable.FULL zugegriffen wird.

Eine abstrakte Interface-Methode liefert keinen funktionsfähigen Code. Ihre Implementierung kann aus der Klasse selbst oder einer geerbten öffentlichen Oberklassenmethode stammen. Default-Methoden sind diejenigen Interface-Methoden, die geerbtes Instanzverhalten bereitstellen können.

Wichtige Erkenntnisse

  • Ein Interface ist ein Typ, der eine gemeinsame Fähigkeit beschreibt.
  • Eine konkrete implementierende Klasse muss geeignete öffentliche Implementierungen bereitstellen oder erben.
  • Eine Klasse kann mehrere Interfaces implementieren und zugleich eine Klasse erweitern.
  • Interfaces haben keine Konstruktoren oder Instanzfelder; ihre Felder sind Konstanten.
  • Über den Interface-Typ kann aufrufender Code den Vertrag bei nicht verwandten Klassen nutzen.

3.3 Nutze gemeinsames Interface-Verhalten und löse Konflikte

Interfaces können neben abstrakten Methodendeklarationen auch funktionsfähigen Code bereitstellen. Der Modifikator einer Methode entscheidet, ob implementierende Objekte sie erben, ob aufrufender Code den Interface-Namen verwendet oder ob nur Code innerhalb des Interfaces sie verwenden kann.

Baue Default-Verhalten aus dem Vertrag auf
interface Chargeable {
    int batteryPercent();
    void charge(int minutes);
    default boolean needsCharge() {
        return batteryPercent() < 20;
    }
    static int clamp(int percent) {
        return Math.max(0, Math.min(100, percent));
    }
}

Eine Default-Methode ist eine öffentliche Instanzmethode mit einem Rumpf. Ein Telefon, das 15 % meldet, erbt needsCharge() und gibt wahr zurück. Bei 20 % gibt es falsch zurück. Die Default-Methode ruft die abstrakte Operation batteryPercent() auf; sie liest kein privates Feld eines Telefons. Eine Klasse darf die Methode mit einer eigenen öffentlichen Implementierung überschreiben.

Eine statische Interface-Methode gehört zum Interface: Chargeable.clamp(130) gibt 100 und Chargeable.clamp(-5) gibt 0 zurück. Implementierende Klassen erben sie nicht, daher ist Phone.clamp(130) kein gültiger Aufruf dieser Methode. Default- und statische Interface-Methoden wurden in Java 8 eingeführt.

Seit Java 9 kann ein Interface auch private Methoden deklarieren, um Hilfscode zwischen seinen eigenen Methoden zu teilen. Eine private Instanzhilfsmethode unterstützt Default-Methoden; eine private statische Hilfsmethode kann auch statische Methoden unterstützen. Implementierende Klassen können diese Hilfsmethoden nicht direkt aufrufen.

Löse Konflikte zwischen Default-Methoden

Zwei nicht verwandte Interfaces können dieselbe Signatur bereitstellen:

interface Lamp {
    default String describe() { return "lamp"; }
}
interface Speaker {
    default String describe() { return "speaker"; }
}
class SmartLamp implements Lamp, Speaker {
    @Override
    public String describe() {
        return Lamp.super.describe() + " and "
             + Speaker.super.describe();
    }
}

Ohne die Überschreibung hat SmartLamp einen Konflikt und lässt sich nicht kompilieren. Die qualifizierten Aufrufe InterfaceName.super.method() wählen die einzelnen Default-Methoden aus den direkten Oberinterfaces aus. Diese Implementierung gibt „lamp and speaker“ zurück.

Konkurrierende ImplementierungenRegel
Geerbte öffentliche konkrete Oberklassenmethode und Interface-Default-MethodeDie Klassenmethode gewinnt
Default-Methode aus einem Interface und aus dem Interface, das es erweitertDas spezifischere Interface gewinnt
Default-Methoden aus nicht verwandten InterfacesDie Klasse muss den Konflikt lösen

Auch eine abstrakte Deklaration in der Oberklasse kann eine konkrete Unterklasse verpflichten, die Methode zu implementieren; eine Interface-Default-Methode hebt diese Pflicht nicht auf.

Berücksichtige bestehende implementierende Klassen

Das Hinzufügen einer abstrakten Methode verlangt von jeder konkreten implementierenden Klasse, eine passende Implementierung bereitzustellen oder zu erben. Eine Default-Methode vermeidet diese Änderung oft, indem sie funktionsfähiges Verhalten bereitstellt, kann aber einen Konflikt mit einer anderen geerbten Methode verursachen. Default-Methoden helfen bei der Weiterentwicklung eines Interfaces; die Vererbungsregeln bestimmen weiterhin, ob sich die entstehenden Klassen kompilieren lassen.

Wichtige Erkenntnisse

  • Eine Default-Methode stellt überschreibbares öffentliches Instanzverhalten bereit.
  • Rufe eine statische Interface-Methode über den Namen des Interfaces auf.
  • Private Interface-Methoden sind Hilfsmethoden, die nur innerhalb des Interfaces sichtbar sind.
  • Eine konkrete Klassenmethode hat Vorrang vor einer Interface-Default-Methode.
  • Konkurrierende Default-Methoden nicht verwandter Interfaces erfordern eine Überschreibung; andernfalls gewinnt die Default-Methode des spezifischeren Interfaces.
  • Das Hinzufügen einer Default-Methode kann Konflikte zwischen geerbten Methoden verursachen.

3.4 Wähle eine gemeinsame Basis, eine Fähigkeit oder beides

Sowohl eine abstrakte Klasse als auch ein Interface können erforderliche Methoden definieren. Die Wahl hängt von der Beziehung zwischen den Objekten ab und davon, ob ihre Implementierung gemeinsamen Instanzzustand benötigt.

Vergleiche, was beide bereitstellen
FrageAbstrakte KlasseInterface
Kann sie beziehungsweise es Felder pro Objekt speichern?JaNein
Kann sie beziehungsweise es Konstruktoren haben?JaNein
Kann sie beziehungsweise es funktionsfähige Instanzmethoden bereitstellen?JaJa, mit Default-Methoden
Wie viele kann eine Klasse direkt verwenden?Eine OberklasseMehrere implementierte Interfaces
Welche Zugriffsstufen sind verfügbar?Alle üblichen StufenÖffentliche Methoden und private Hilfsmethoden

Interface-Felder sind public static final-Konstanten, keine Daten einzelner Objekte. Private Interface-Hilfsmethoden unterstützen die eigenen funktionsfähigen Methoden des Interfaces; sie erzeugen keinen privaten Zustand für jedes implementierende Objekt.

Lege eng verwandten Zustand in einer abstrakten Klasse ab

Ein Zeichenprogramm hat Kreise, Rechtecke und Dreiecke. Jede Form speichert einen Namen und eine Position, und jede berechnet ihre Fläche anders. Eine abstrakte Klasse Shape kann die gemeinsamen Felder und den Konstruktor verwalten, funktionsfähige Methoden zum Bewegen oder Beschreiben bereitstellen und area() als abstrakt deklarieren.

Alle konkreten Formen sind Arten von Shape, sodass anderer Code eine Form anfordern und jede davon erhalten kann. Ebenso könnte eine abstrakte Klasse Enemy Lebenspunkte und eine gemeinsame Methode takeDamage(int points) verwalten und die Bewegung den jeweiligen Gegnerklassen überlassen. Der gemeinsame Zustand gibt diesen Basisklassen sinnvolle Aufgaben.

Lege eine klassenübergreifende Fähigkeit in einem Interface fest

Dasselbe Zeichenprogramm kann Formen, Textbeschriftungen und Bilder exportieren. Durch den Export wird eine Textbeschriftung nicht zu einer Form, und diese Objekte benötigen möglicherweise bereits unterschiedliche Oberklassen. Ein Interface Exportable kann String exportText() deklarieren. Jede Klasse implementiert die Operation mit ihren eigenen Daten.

Eine Klasse kann beide Rollen kombinieren: class Circle extends Shape implements Exportable. Die Oberklasse stellt den Zustand der Form und gemeinsamen Code bereit; das Interface macht den Kreis für Exportcode verwendbar. Eine Klasse, die mehrere Interfaces implementiert, sichert mehrere Fähigkeiten zu, ohne Instanzfelder von ihnen zu erben.

Kombiniere einen Vertrag mit einer Teilimplementierung

Eine abstrakte Klasse kann ein Interface implementieren und nur einige Methoden vervollständigen. Konkrete Unterklassen erledigen die übrigen Operationen. Java verwendet diese Anordnung bei List und AbstractList; ArrayList erweitert AbstractList. Aufrufender Code kann sich auf das Interface stützen, während Implementierungen eine Basisklasse wiederverwenden, wo sie passt.

Zum Beispiel ermöglicht List<String> names = new ArrayList<>(); aufrufendem Code, Namen über List hinzuzufügen, zu lesen und zu entfernen. Wird der Initialisierer durch new LinkedList<>() ersetzt, bleiben diese Aufrufe erhalten. Ein als ArrayList<String> deklarierter Parameter würde eine LinkedList<String> ablehnen, selbst wenn er nur Listenoperationen benötigt.

Eine abstrakte Klasse, die nur abstrakte Methoden enthält, belegt weiterhin den einzigen Oberklassenplatz der Unterklasse. Sie ist nicht mit einem Interface austauschbar. Wähle das Interface, wenn nur der gemeinsame Vertrag benötigt wird, und ergänze eine Basisklasse, wenn verwandte Implementierungen von gemeinsamem Zustand oder Code profitieren.

Wichtige Erkenntnisse

  • Verwende eine abstrakte Klasse für verwandte Arten von Objekten, die Zustand oder Code teilen.
  • Verwende ein Interface für eine Fähigkeit, die nicht verwandte Klassen bereitstellen können.
  • Eine Klasse kann eine abstrakte Basisklasse erweitern und zugleich Interfaces implementieren.
  • Eine abstrakte Teilimplementierung kann ein Interface unterstützen, ohne aufrufenden Code auf diese Basisklasse zu beschränken.
4

Schreibe flexible Programme mit Polymorphie

Der Typ einer Variablen und die Klasse eines Objekts beantworten unterschiedliche Fragen. Der deklarierte Typ der Variablen bestimmt, welche Aufrufe kompilieren; die tatsächliche Klasse des Objekts bestimmt, welche Überschreibung ausgeführt wird. Trennst du diese Entscheidungen, kannst du unbekannten Code nachvollziehen, explizite Typumwandlungen sicher verwenden und Schleifen schreiben, die mit neuen Implementierungen funktionieren. Derselbe Ansatz unterstützt Bibliotheksinterfaces wie Comparable, das Objekten eine natürliche Sortierreihenfolge gibt.

4.1 Unterscheide den Referenztyp von der Objektklasse

Polymorphie ermöglicht es einer allgemeinen Referenz, auf Objekte verschiedener Untertypen zu verweisen. Das Objekt behält seinen vollständigen Zustand und sein Verhalten; die Referenz gibt aufrufendem Code eine bestimmte Sicht auf die verfügbaren Operationen.

Benenne die beiden Typen
Person p = new Student("Ada Lovelace", "S-17");

Der deklarierte Typ, auch statischer Typ genannt, ist Person: Er steht in der Variablendeklaration. Der dynamische Typ oder Laufzeittyp ist Student: Das ist die Klasse, die mit new instanziiert wird. Die tatsächliche Klasse des Objekts ändert sich nach seiner Erzeugung nicht.

Die Zuweisung einer Studentenreferenz an eine Personenreferenz ist ein Upcast. Er erfolgt automatisch und ist sicher, weil jeder Student eine Person ist. Auf denselben Studenten kann auch eine Variable vom Typ Object verweisen. Ein implementierendes Objekt kann per Upcast einem Interface zugewiesen werden, das es implementiert, wie in Chargeable c = new Phone(35);.

Durch die Zuweisung werden keine Studentendaten gelöscht. Es gibt weiterhin ein Studentenobjekt mit seiner Studentenkennung, geerbten Personendaten und überschriebenen Methoden. Eine allgemeinere Referenz ändert, welche Zugriffe der Compiler über diese Referenz erlaubt.

Prüfe Aufrufe anhand des deklarierten Typs

Wenn Person die Methode getName() deklariert, lässt sich p.getName() kompilieren. Wenn nur Student die Methode getStudentId() deklariert, lässt sich p.getStudentId() nicht kompilieren. Der Compiler prüft die Schnittstelle von Person, selbst wenn der Initialisierer eindeutig einen Studenten erzeugt.

Die umgekehrte Zuweisung Student s = p; wird ohne explizite Typumwandlung nicht akzeptiert. Eine Personenreferenz könnte auf eine Person verweisen, die kein Student ist. Diese Einschränkung schützt Code davor, ein Objekt als einen Untertyp zu behandeln, den es nicht hat.

Deklarierter TypÜber die Referenz verfügbare Aufrufe
ObjectÖffentliche Operationen von Object
PersonZugängliche Operationen von Person, einschließlich geerbter Operationen
StudentZugängliche Operationen von Studenten und geerbte Operationen
Lass Parameter und Listen mehrere Untertypen akzeptieren

Eine Methode static void printCard(Person person) kann ein Objekt vom Typ Person, Student oder Teacher erhalten, sofern die beiden letzten Person erweitern. Ihr Code kann sich auf den Vertrag von Person stützen. Auch ein Rückgabetyp kann allgemein sein, zum Beispiel bei einer Methode, die ein Shape zurückgibt.

Eine Liste mit einem allgemeinen Elementtyp kann verschiedene Formklassen speichern:

List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle(1.5));
shapes.add(new Rectangle(3.0, 4.0));
for (Shape shape : shapes) {
    System.out.println(shape.area());
}

Hier stammen List und ArrayList aus java.util, und jede konkrete Form stellt area() bereit. Die Schleife verwendet nur diese gemeinsame Operation.

Eine List<Circle> ist keine List<Shape>. Wäre sie es, könnte Code, der sie als Formliste erhält, ein Rechteck zu einer Liste hinzufügen, die nur Kreise enthalten soll. Wenn eine Methode List<Shape> verlangt, erstelle die Liste von Anfang an mit diesem Elementtyp.

Wichtige Erkenntnisse

  • Der deklarierte Typ bestimmt, auf welche Operationen eine Referenz zugreifen kann.
  • Der dynamische Typ ist die Klasse des Objekts und ändert sich nach dessen Erzeugung nicht.
  • Ein Upcast zu einer Oberklasse oder einem implementierten Interface erfolgt automatisch und ist sicher.
  • Ein allgemeiner Parameter- oder Listenelementtyp kann mehrere konkrete Untertypen aufnehmen.
  • List<Circle> kann nicht als List<Shape> verwendet werden.

4.2 Verfolge die Auswahl überladener Methoden und die dynamische Bindung

Ein Methodenaufruf wird in zwei Phasen aufgelöst. Zur Kompilierzeit prüft Java die deklarierten Typen und wählt eine Methodensignatur. Zur Laufzeit wählt die dynamische Bindung die überschreibende Instanzmethodenimplementierung für die tatsächliche Klasse des Objekts.

Verfolge einen Aufruf innerhalb einer geerbten Methode
class A {
    String who() { return "A"; }
    String hello() { return "Hello from " + who(); }
}
class B extends A {
    @Override String who() { return "B"; }
}
class C extends B { }

Für A x = new C(); ist x.hello() erlaubt, weil A die Methode hello() deklariert. Weder Klasse C noch B überschreibt hello(), daher führt Java die Implementierung in A aus.

Innerhalb dieser Methode bedeutet who() dasselbe wie this.who(). Das Empfängerobjekt ist weiterhin das C-Objekt. Java sucht von C aus und findet dann die nächstgelegene who()-Implementierung in B. Das Ergebnis ist „Hello from B“. Der Eintritt in eine geerbte Methode verwandelt das Empfängerobjekt nicht in ein Objekt der Oberklasse.

Wähle eine Überladung vor einer Überschreibung
static String kind(Object value) { return "object"; }
static String kind(String value) { return "string"; }

Innerhalb einer Klasse, die diese Methoden enthält, wählt Object value = "hi"; kind(value) die Methode kind(Object) und gibt „object“ zurück. Der deklarierte Typ der Variablen ist Object; Java prüft die Überladungen zur Laufzeit nicht anhand des tatsächlichen Strings erneut.

Bei einem Instanzaufruf kann die Laufzeitbindung weiterhin eine Überschreibung der bereits gewählten Signatur auswählen. Sie kann nicht zu einer anderen Überladung wechseln, die für das tatsächliche Argument lediglich spezifischer aussieht. Bestimme zuerst die Signatur und suche dann nach ihrer Überschreibung.

Erkenne Aufrufe und Mitglieder ohne diese Bindung
Mitglied oder AufrufWas die Auswahl bestimmt
Überschriebene InstanzmethodeTatsächliche Klasse des Empfängerobjekts nach Auswahl der Signatur
Statische MethodeFür den Aufruf verwendeter Typ zur Kompilierzeit
Verdecktes FeldDeklarierter Typ der Referenz
Private MethodeCode ihrer deklarierenden Klasse
super.method()Implementierung der Oberklasse

Wenn P das Feld String label = "P"; deklariert und Q extends P ein eigenes String label = "Q"; deklariert, liest P p = new Q(); p.label den Wert „P“. Felder werden verdeckt, nicht überschrieben. Beide Felder existieren im Objekt. Vermeide es, geerbte Feldnamen erneut zu verwenden, weil gleich aussehende Zugriffe je nach Referenztyp unterschiedliche Zustände lesen können.

super.method() wählt die Implementierung der Oberklasse, einschließlich einer Implementierung, die diese Oberklasse erbt. Wenn die gewählte Methode selbst einen gewöhnlichen Aufruf auf this ausführt, kann dieser innere Aufruf weiterhin an eine Überschreibung der Unterklasse gebunden werden.

Wichtige Erkenntnisse

  • Zur Kompilierzeit wird die aufrufbare Signatur einschließlich der Auswahl überladener Methoden festgelegt.
  • Die dynamische Bindung wählt die nächstgelegene Überschreibung dieser Signatur, ausgehend von der tatsächlichen Klasse aufwärts.
  • Ein gewöhnlicher Aufruf innerhalb einer geerbten Methode verwendet weiterhin das tatsächliche Empfängerobjekt.
  • Felder, statische Methoden, private Methoden und explizite super-Aufrufe verwenden nicht die gewöhnliche dynamische Bindung überschreibender Methoden.

4.3 Prüfe einen Untertyp, bevor du seine speziellen Methoden verwendest

Eine allgemeine Referenz benötigt manchmal Zugriff auf eine Operation, die nur zu einem Untertyp gehört. Ein Downcast weist den Compiler an, die Referenz als diesen Untertyp zu behandeln. Zur Laufzeit prüft Java, ob das Objekt bereits diesen Typ hat.

Verstehe, was eine explizite Typumwandlung ändert

Angenommen, Person p verweist auf einen Studenten. Student s = (Student) p; erlaubt s.getStudentId(). Beide Variablen verweisen weiterhin auf dasselbe Objekt. Die Typumwandlung kopiert es nicht, ändert seine tatsächliche Klasse nicht und fügt einer einfachen Person keine Studentenfelder hinzu.

Wenn p tatsächlich auf new Person("Grace Hopper") verweist, wirft diese Typumwandlung eine ClassCastException. Der Compiler akzeptiert die mögliche Beziehung von Person zu Student, aber das Laufzeitobjekt muss sie auch erfüllen. Eine Typumwandlung zwischen Klassen, die nicht verwandt sein können, etwa die Umwandlung einer Person-Referenz in die finale Klasse Integer, schlägt zur Kompilierzeit fehl.

Das unterscheidet sich von einer Umwandlung primitiver Werte: (int) 3.9 erzeugt den Ganzzahlwert 3. Eine explizite Umwandlung einer Referenz prüft und ändert nur den Typ, über den eine bestehende Referenz verwendet wird.

Verbinde die Prüfung mit der neuen Variablen

Seit Java 16 macht die Mustererkennung den sicheren Fall kurz:

static void showStudentId(Person person) {
    if (person instanceof Student student) {
        System.out.println(student.getStudentId());
    }
}

Die Prüfung ist für einen Studenten oder ein Objekt einer Unterklasse von Student wahr. Die Variable student ist innerhalb des Blocks verfügbar, wo bekannt ist, dass die Prüfung erfolgreich war. Eine einfache Person überspringt den Block. Auch eine Nullreferenz macht die Prüfung falsch, sodass der Block nie versucht, eine Methode auf null aufzurufen.

Die ältere Form prüft person instanceof Student und deklariert dann Student student = (Student) person; innerhalb des erfolgreichen Zweigs. Beide Formen sichern die Typumwandlung ab. Die Prüfung eines anderen Objekts an anderer Stelle sichert eine Typumwandlung von person nicht ab; Prüfung und Typumwandlung müssen dieselbe Referenz betreffen.

Ein Upcast benötigt keine explizite Typumwandlung. Person person = student; lässt sich bereits kompilieren und kann nicht wegen eines unpassenden Typs fehlschlagen. Eine Umwandlung von null in einen kompatiblen Referenztyp ergibt null; wird das Ergebnis für einen Instanzmethodenaufruf verwendet, schlägt dies weiterhin mit einer NullPointerException fehl.

Entscheide, ob ein Typtest nötig ist

Typtests eignen sich für Code, der ein Object erhält, etwa equals(Object other), oder eine Operation benötigt, die nur ein Untertyp bietet, etwa eine Studentenkennung. Sie können auch bei der Arbeit mit Klassen helfen, die nicht geändert werden können.

Eine wiederholte Kette von Typtests für eine gemeinsame Operation ist ein anderes Problem. Prüft Code jede Formklasse, um ihre Fläche zu berechnen, benötigt jede hinzugefügte Form einen weiteren Zweig. Wird area() in Shape deklariert und von den Unterklassen implementiert, kann aufrufender Code stattdessen shape.area() verwenden.

Eine explizite Typumwandlung kann eine fehlende gemeinsame Operation nicht für jeden Untertyp verfügbar machen. Lege gemeinsames Verhalten im gemeinsamen Vertrag fest und verwende einen Untertyptest für Aufgaben, die tatsächlich diesen bestimmten Untertyp benötigen.

Wichtige Erkenntnisse

  • Ein Downcast prüft ein vorhandenes Objekt; er verändert das Objekt nicht.
  • Eine mögliche explizite Typumwandlung kann kompilieren und zur Laufzeit dennoch eine ClassCastException werfen.
  • instanceof ist für null falsch und für Instanzen des Typs oder seiner Untertypen wahr.
  • Ein instanceof-Muster führt dort eine sicher typisierte Variable ein, wo die Prüfung als wahr bekannt ist.
  • Gemeinsame Operationen gehören in den gemeinsamen Typ; Operationen einzelner Untertypen benötigen möglicherweise einen Typtest.

4.4 Schreibe allgemeine Schleifen und definiere eine natürliche Ordnung

Eine Methode bleibt für neue Klassen nützlich, wenn ihr Parameter genau die Operationen bereitstellt, die sie benötigt. Aufrufender Code liefert Objekte; die dynamische Bindung liefert deren individuelles Verhalten.

Addiere Flächen ohne klassenspezifische Zweige
static double totalArea(List<Shape> shapes) {
    double total = 0;
    for (Shape shape : shapes) {
        total += shape.area();
    }
    return total;
}

Bei einem Kreis mit Radius 1,5 m, einem Rechteck mit den Abmessungen 3,0 m und 4,0 m und einem Dreieck mit Grundseite 4,0 m und Höhe 2,5 m betragen die Flächen etwa 7,06858; 12,0 und 5,0 Quadratmeter. Die Addition der ungerundeten Kreisfläche ergibt etwa 24,07 Quadratmeter. Jedes Objekt stellt sein eigenes area() bereit.

Das Hinzufügen einer Klasse Triangle erfordert keine Änderung der Schleife. Die neue Klasse implementiert die bestehende Formoperation. Ein negatives Ergebnis würde den Flächenvertrag verletzen, selbst wenn die Überschreibung kompiliert; allgemeiner Code verlässt sich sowohl auf die Bedeutung der Operation als auch auf ihre Signatur.

Derselbe Ansatz funktioniert mit einem Interface. Eine Methode chargeAll(List<Chargeable> devices, int minutes) durchläuft Geräte und ruft device.charge(minutes) auf. Nach 20 Minuten erreicht das Telefon, das bei 35 % beginnt, 75 %, und der Roller, der bei 50 % beginnt, erreicht 70 %. Die Schleife wählt selbst nie eine Laderegel aus.

Lass aufrufenden Code eine Listenimplementierung wählen

Ein Parameter List<Shape> akzeptiert sowohl eine ArrayList<Shape> als auch eine LinkedList<Shape>. Ein Parameter ArrayList<Shape> lehnt die verkettete Liste ab, obwohl nur Listenoperationen benötigt werden. Verwende den allgemeinsten Typ, der die benötigten Methoden bereitstellt; alles als Object zu deklarieren wäre zu allgemein, weil diesem Typ area() und Listenoperationen fehlen.

Mache Objekte mit Comparable sortierbar

Comparable<T> definiert die natürliche Ordnung eines Objekts. Das Ergebnis von compareTo ist negativ, wenn dieses Objekt vor dem anderen steht, null bei einem Gleichstand in der Ordnung und positiv, wenn es danach steht. Das Vorzeichen des Ergebnisses ist entscheidend; es muss nicht genau −1 oder 1 sein.

record Runner(String name, int seconds)
        implements Comparable<Runner> {
    @Override
    public int compareTo(Runner other) {
        return Integer.compare(seconds, other.seconds);
    }
}

Eine modifizierbare Liste mit Mia bei 3.605 s, Ken bei 3.490 s und Ana bei 3.720 s ergibt nach Collections.sort(runners) die Reihenfolge Ken, Mia, Ana. Auch list.sort(null) verwendet die natürliche Ordnung. Erstelle eine modifizierbare Liste mit new ArrayList<>(List.of(...)); der Versuch, das nicht modifizierbare Ergebnis von List.of(...) zu sortieren, wirft eine UnsupportedOperationException.

Verwende Integer.compare, statt die Werte zu subtrahieren. Die Subtraktion kann bei großen Werten mit unterschiedlichen Vorzeichen überlaufen und das falsche Vorzeichen zurückgeben. Dieselbe Idee gilt für Double.compare bei Dezimalfeldern. In diesem Läuferbeispiel ergeben gleiche Zeiten einen Gleichstand in der Ordnung, selbst wenn die Namen unterschiedlich sind. Ein Gleichstand beim Vergleich und Objektgleichheit müssen daher nicht dasselbe bedeuten. Bevorzuge eine natürliche Ordnung, deren Ergebnis null mit der Gleichheit übereinstimmt; dokumentiere eine beabsichtigte Abweichung.

Wichtige Erkenntnisse

  • Allgemeiner Code ruft eine gemeinsame Operation auf und lässt jedes Objekt seine Implementierung bereitstellen.
  • Ein Interface-Parameter kann verschiedene Implementierungen desselben Vertrags akzeptieren.
  • compareTo drückt die Ordnung durch ein negatives Ergebnis, null oder ein positives Ergebnis aus.
  • Integer.compare vermeidet das Überlaufrisiko beim Subtrahieren von Ganzzahlfeldern.
  • Sortieren erfordert eine modifizierbare Liste; List.of(...) allein ist nicht modifizierbar.
  • Ein Gleichstand in der Ordnung kann sich von der Wertgleichheit unterscheiden, wenn der Vergleich weniger Felder verwendet.
5

Modelliere feste Werte und Daten mit Aufzählungstypen und Records

Manche Programme benötigen eine feste Auswahl, andere eine kleine Gruppe zusammengehöriger Werte. Aufzählungstypen geben Auswahlmöglichkeiten einen eigenen Typ; Records stellen einen Großteil des wiederkehrenden Codes für Datenobjekte bereit. Beide passen in das Typsystem von Java und können Interfaces implementieren. Ihr eingebautes Verhalten spart Arbeit, doch die Wahl zwischen ihnen und einer gewöhnlichen Klasse hängt weiterhin davon ab, was sich ändern kann und wann zwei Werte gleich sind.

5.1 Stelle feste Auswahlmöglichkeiten mit einem Aufzählungstyp dar

Ein Aufzählungstyp gibt einer festen Menge benannter Auswahlmöglichkeiten einen eigenen Typ. enum Size { SMALL, MEDIUM, LARGE } erlaubt Size size = Size.MEDIUM;. Er verhindert, dass aufrufender Code den falsch geschriebenen String „MEDUIM“ oder die Ganzzahl 7 an eine Methode übergibt, die Size verlangt.

Verwende Namen, Konstanten und switch

Jede Aufzählungskonstante ist ein einzelnes Objekt, daher ist size == Size.LARGE ein geeigneter Vergleich. Eine Variable eines Aufzählungstyps kann auch null sein. Size.values() gibt alle Konstanten in der Reihenfolge ihrer Deklaration zurück; Size.valueOf("LARGE") findet den exakt passenden Namen. Bei der Suche wird die Groß- und Kleinschreibung beachtet: „large“ verursacht eine IllegalArgumentException.

name() gibt den deklarierten Namen zurück. ordinal() gibt die Position ab null zurück, daher ist Size.MEDIUM.ordinal() gleich 1. Das Einfügen einer Konstante ändert spätere Positionen, daher eignet sich ein Ordinalwert schlecht als gespeicherter Code oder Geschäftswert. Standardmäßig gibt auch toString() den Namen zurück.

Ein switch-Ausdruck, seit Java 14 Standard, kann einen Wert zurückgeben:

String label = switch (size) {
    case SMALL -> "S";
    case MEDIUM -> "M";
    case LARGE -> "L";
};

Durch das Nennen jeder Konstante ist der Ausdruck ohne default-Zweig vollständig. Diese Fälle setzen voraus, dass size nicht null ist; das Auflisten der Konstanten behandelt null nicht.

Gib jeder Konstante aussagekräftige Daten
enum CupSize {
    SMALL(250), MEDIUM(350), LARGE(500);
    private final int milliliters;
    CupSize(int milliliters) { this.milliliters = milliliters; }
    public int milliliters() { return milliliters; }
}

Das Semikolon trennt die Konstantenliste von den Mitgliedern. Der Konstruktor wird bei der Initialisierung des Aufzählungstyps einmal für jede Konstante ausgeführt. Er ist implizit privat, und aufrufender Code kann mit new CupSize(400) keine neuen Konstanten erzeugen.

Zwei große und ein kleiner Becher fassen 1.250 mL oder 1,25 L. Diese Rechnung verwendet das Feld milliliters, nicht die Positionen der Konstanten. Das Durchlaufen von CupSize.values() und die Ausgabe jeder Konstante mit ihrem Fassungsvermögen ergeben „SMALL: 250 mL“, „MEDIUM: 350 mL“ und „LARGE: 500 mL“.

Gib Konstanten eigenes Verhalten

Ein Aufzählungstyp kann ein Interface implementieren und einzelnen Konstanten unterschiedliche Methodenrümpfe geben:

enum Operation {
    ADD { int apply(int a, int b) { return a + b; } },
    MULTIPLY { int apply(int a, int b) { return a * b; } };
    abstract int apply(int a, int b);
}

Operation.MULTIPLY.apply(6, 7) gibt 42 zurück. Jede Konstante vervollständigt die abstrakte Operation. Aufzählungstypen erweitern implizit java.lang.Enum; sie können keine andere Klasse erweitern, und gewöhnliche Klassen können sie nicht erweitern.

Verwende einen Aufzählungstyp für Auswahlmöglichkeiten, die beim Schreiben des Programms feststehen, etwa Richtungen oder Bestellzustände. Die veränderliche Produktliste eines Geschäfts gehört in Daten: Das Hinzufügen von Produkten sollte nicht verlangen, eine Deklaration des Aufzählungstyps neu zu schreiben und zu kompilieren.

Wichtige Erkenntnisse

  • Ein Aufzählungstyp gibt einer festen Menge von Konstanten einen eigenen Typ.
  • values() bewahrt die Reihenfolge der Deklaration und valueOf verlangt den exakten Namen.
  • Speichere aussagekräftige Zahlen in Feldern, da sich Ordinalpositionen ändern können.
  • Konstruktoren von Aufzählungstypen initialisieren Konstanten; aufrufender Code kann mit new keine Konstanten hinzufügen.
  • Aufzählungstypen können Interfaces implementieren und Konstanten unterschiedliches Verhalten geben.

5.2 Speichere Werte in einem Record und validiere sie

Ein Record ist eine kompakte Datenklasse, die seit Java 16 zum Standard gehört. Sein Kopf listet die Werte auf, die jede Instanz bestimmen und Komponenten heißen: record Point(int x, int y) { }.

Verwende die generierten Mitglieder

Der Compiler stellt für jede Komponente ein privates finales Feld, einen kanonischen Konstruktor mit diesen Komponenten in ihrer Reihenfolge und nach ihnen benannte öffentliche Zugriffsmethoden bereit. Außerdem stellt er equals, hashCode und toString auf Grundlage der Komponenten bereit.

Point a = new Point(3, 4);
Point b = new Point(3, 4);
System.out.println(a.x());
System.out.println(a.equals(b));
System.out.println(a == b);

Dies gibt 3, true und false in getrennten Zeilen aus. Übereinstimmende Komponentenwerte machen die Records gleich, aber die beiden new-Ausdrücke erzeugen separate Objekte. Gleiche Records haben gleiche Hashcodes. Ihr nützlicher generierter Text enthält die Namen des Records und der Komponenten; aktuelle JDKs zeigen „Point[x=3, y=4]“. Dieses genaue Textformat ist nicht garantiert, daher solltest du es nicht als Datenformat parsen.

Die Zugriffsmethoden heißen x() und y(), nicht getX() und getY(). Ein Setter, der this.x = x zuweist, lässt sich nicht kompilieren, weil das Komponentenfeld final ist. Eine Methode kann stattdessen einen neuen Wert zurückgeben: Point moveRight(int dx) { return new Point(x + dx, y); }.

Validiere in einem kompakten Konstruktor
record Temperature(double celsius) {
    Temperature {
        if (celsius < -273.15) {
            throw new IllegalArgumentException("below absolute zero");
        }
    }
}

Ein kompakter Konstruktor wiederholt den Namen des Records ohne Parameterliste. Er prüft oder normalisiert die eingehenden Parameter; Java weist die Komponentenfelder nach seinem Rumpf zu. Diese Prüfung lehnt −300 °C ab und akzeptiert 21,5 °C. Eine Textkomponente könnte vor der automatischen Zuweisung mit name = name.trim(); normalisiert werden.

Ein öffentlicher Record benötigt einen öffentlichen kompakten Konstruktor. Records dürfen außerdem statische Felder, statische Methoden und Instanzmethoden deklarieren. Ein zusätzlicher Konstruktor delegiert mit this(...) an den kanonischen Konstruktor. Zusätzliche Instanzfelder über die Komponenten hinaus sind verboten.

Schütze veränderliche Komponentenobjekte

Finale Komponentenfelder machen die Unveränderlichkeit flach. Ein Record, der eine veränderliche Liste enthält, verweist weiterhin auf diese Liste:

record Team(String name, List<String> members) {
    Team {
        members = List.copyOf(members);
    }
}

Wenn java.util.List importiert ist, speichert dies eine nicht modifizierbare Kopie. Spätere Ergänzungen der ursprünglichen Liste wirken sich nicht auf die Teammitglieder aus, und team.members().add("Lee") wirft eine UnsupportedOperationException. Die Kopie schützt die Listenstruktur; sie macht veränderliche Elementobjekte nicht tiefgehend unveränderlich.

Records erweitern implizit java.lang.Record und sind final. Sie können keine andere Klasse erweitern oder selbst erweitert werden, aber Interfaces wie Comparable implementieren. Verwende sie für Daten, die durch ihre Werte bestimmt sind; verwende eine gewöhnliche Klasse für veränderlichen Zustand, zusätzliche verborgene Instanzfelder oder eine erweiterbare Klassenhierarchie.

Wichtige Erkenntnisse

  • Ein Record-Kopf definiert Komponenten und den generierten Konstruktor, die Zugriffsmethoden und Wertmethoden.
  • Zugriffsmethoden eines Records verwenden Komponentennamen wie x().
  • Ein kompakter Konstruktor prüft oder normalisiert Parameter vor der automatischen Feldzuweisung.
  • Referenzen in Record-Komponenten sind final, aber referenzierte Objekte können veränderlich bleiben.
  • Eine nicht modifizierbare Listenkopie schützt die Listenstruktur eines Records.
  • Ein Record ist final, kann keine Klasse ausdrücklich erweitern und kann Interfaces implementieren.

Vererbung in Java: Beispiele

5 Interaktive Lektionen

1

Entwickle Schritt für Schritt ein Gerät mit Vererbung

12 Schritte
Erstelle ein beschriftetes Gerät, ergänze einen Sensoruntertyp und verfolge die Initialisierung desselben Objekts.
  1. 1
    EinführungEin Objekt mit einer allgemeinen und einer speziellen Rolle

    Ein Sensor ist eine Art Gerät. Jedes Gerät benötigt eine Beschriftung, während ein Sensor zusätzlich eine Maßeinheit benötigt. Vererbung lässt das Gerät seine Beschriftung verwalten und den Sensor seine Einheit ergänzen.

    Wir bauen diese Beziehung auf, verwenden eine geerbte Methode und verfolgen, wie Konstruktoraufrufe jeden Teil initialisieren. Die zentrale Frage ist, welche Klasse für welches Feld zuständig ist und welcher Konstruktor ihm daher einen Wert zuweisen sollte.

  2. 2
    BeispielBeginne mit dem Zustand, den jedes Gerät benötigt
    class Device {
        private final String label;
        Device(String label) { this.label = label; }
        public String label() { return label; }
    }
    

    Device kontrolliert seine private Beschriftung. Anderer Code liest sie über label(). Eine Unterklasse erbt diese öffentliche Methode, kann dem privaten Feld aber nicht direkt einen Wert zuweisen. So bleibt die Initialisierung der Beschriftung an einer Stelle.

  3. 3
    BeispielErgänze nur den sensorspezifischen Zustand
    class Sensor extends Device {
        private final String unit;
        Sensor(String label, String unit) {
            super(label);
            this.unit = unit;
        }
        public String unit() { return unit; }
    }
    

    Sensor erweitert eine Klasse und ergänzt ein Feld. Der Aufruf super(label) überträgt die Verantwortung für die Beschriftung an Device; der Sensorkonstruktor speichert die Einheit selbst.

  4. 4
    KernaussageLies beide Teile über den Sensor

    Nach Sensor s = new Sensor("Room", "C"); gibt s.label() „Room“ und s.unit() „C“ zurück. Die erste Methode wird geerbt; die zweite gehört zu Sensor.

    Beide Aufrufe betreffen dasselbe Objekt. Das Erweitern von Device hat kein zweites Geräteobjekt neben dem Sensor erzeugt. Die private Beschriftung ist vorhanden, obwohl Sensorcode über eine Methode auf sie zugreift.

  5. 5
    HerausforderungWähle die Beziehung für ein Labor

    Ein Lab enthält mehrere Sensoren. Sollte Lab die Klasse Sensor erweitern oder Referenzen auf Sensoren verwalten? Erkläre die Beziehung.

    ---reveal---

    Verwalte Sensorreferenzen in Feldern oder einer Collection. Ein Labor hat Sensoren, ist aber selbst kein Sensor, daher passt Komposition. Das Erweitern von Sensor würde zusichern, dass ein Labor überall dort verwendet werden kann, wo ein Sensor benötigt wird, einschließlich der Operationen, die die Einheit eines einzelnen Sensors beschreiben.

  6. 6
    ErklärungRufe den Oberklassenkonstruktor auf

    Der Aufruf new Sensor("Room", "C") wählt den Sensorkonstruktor mit zwei Argumenten. Sein super(label) ruft Device(String) für dasselbe Objekt auf.

    Device erreicht zuerst seine eigene Oberklasse Object und weist dann die Beschriftung zu. Wenn Device zurückkehrt, werden die Sensorfeldinitialisierung und der restliche Sensorkonstruktorrumpf ausgeführt. Schließlich steht die neue Referenz dem aufrufenden Code zur Verfügung.

  7. 7
    TippFinde den fehlenden parameterlosen Konstruktor

    Entfernst du super(label) aus dem Sensorkonstruktor, fügt Java super() ein. Für diesen Aufruf gibt es keinen passenden Konstruktor: Device deklariert nur Device(String).

    Der Konstruktorfehler betrifft die Argumente, nicht das private Feld. Wird der Aufruf durch new Device(label) ersetzt, entsteht ein unabhängiges Objekt, und das Problem des impliziten super() bleibt bestehen. Übergib die Beschriftung stattdessen an den Oberklassenkonstruktor.

  8. 8
    BeispielDelegiere für eine optionale Standardeinheit

    Füge diesen Konstruktor innerhalb von Sensor hinzu:

    Sensor(String label) {
        this(label, "unknown");
    }
    

    new Sensor("Hall") delegiert an den vorhandenen Konstruktor mit zwei Argumenten, der super(label) aufruft. Das entstehende Objekt hat die Beschriftung „Hall“ und die Einheit „unknown“. Schreibe nicht zusätzlich super(...) in den delegierenden Konstruktor: Ein Konstruktor hat höchstens einen expliziten Konstruktoraufruf.

  9. 9
    VerbindungValidiere einen Parameter vor der Initialisierung

    Das Standard-Java 25 erlaubt eingeschränkte Arbeiten vor einem expliziten Konstruktoraufruf. Zum Beispiel kann ein Sensorkonstruktor eine eingehende leere Beschriftung ablehnen, bevor er super(label) aufruft.

    Dieser vorgezogene Code darf Parameter verwenden, aber keine Felder des Objekts lesen oder seine Instanzmethoden aufrufen. Eingeschränkte einfache Zuweisungen an Felder der aktuellen Klasse ohne Initialisierer in der Deklaration sind ebenfalls erlaubt. Bei älteren Standard-Sprachversionen muss der Aufruf an erster Stelle stehen.

  10. 10
    HerausforderungSage eine Konstruktormeldung voraus

    In einer separaten Version gibt Device nach seinem eigenen Oberklassenaufruf „device“ aus. Sensor gibt nach super(label) „sensor“ aus. Welche Meldung erscheint beim Erzeugen eines Sensors zuerst und warum?

    ---reveal---

    „device“ erscheint zuerst. Der Sensorkonstruktor ruft den Gerätekonstruktor auf, bevor er mit seiner eigenen Ausgabeanweisung fortfährt. Das Objekt ist während der gesamten Initialisierung ein Sensor; diese Meldungen zeigen die Reihenfolge der Konstruktorarbeit und keine Änderungen seiner Klasse.

  11. 11
    HerausforderungKorrigiere eine Zuweisung an ein geerbtes Feld

    Eine neue Unterklasse versucht, die private Gerätebeschriftung mit this.label = label; zu initialisieren, und lässt super(label) weg. Welche zwei zusammenhängenden Probleme entstehen dadurch?

    ---reveal---

    Die Unterklasse kann dem privaten Feld nicht direkt einen Wert zuweisen. Auch für das implizite super() gibt es keinen passenden Gerätekonstruktor. Verwende super(label), damit Device seinem eigenen privaten Zustand Werte zuweist, und weise danach nur Feldern Werte zu, die in der Unterklasse deklariert sind.

  12. 12
    ZusammenfassungLokalisiere einen Fehler bei der Initialisierung

    Wenn ein Untertyp nicht erzeugt werden kann, suche den ersten Konstruktoraufruf ohne passenden zugänglichen Konstruktor. Prüfe dann, ob jede Klasse die Felder initialisiert, für die sie zuständig ist.

    Prüfe neben diesem Ablauf auch die Beziehung: Ein Untertyp ist als sein Basistyp verwendbar; ein enthaltenes Objekt gehört in ein Feld. Diese Prüfungen betreffen unterschiedliche Arten von Entwurfs- und Kompilierfehlern.

2

Gib Katalogeinträge korrekt aus und vergleiche sie

14 Schritte
Überschreibe Verhalten, entwickle eine Methode zur Wertgleichheit und verbinde sie mit der Hashcode-Berechnung.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungLege die Bedeutung eines Katalogeintrags fest
  2. 2
    BeispielErweitere eine grundlegende Beschreibung
  3. 3
    TippVermeide den Aufruf der Überschreibung aus sich selbst
  4. 4
    HerausforderungPrüfe einen scheinbaren Ersatz
  5. 5
    BeispielGib der Ausgabe eine exakt passende Methode
  6. 6
    KernaussageLies die tatsächliche Klasse, ohne sie zu ändern
  7. 7
    HerausforderungUntersuche einen standardmäßig aussehenden Ausgabetext
  8. 8
    BeispielDeklariere die gleichheitsrelevanten Daten
  9. 9
    BeispielLehne das falsche Objekt vor dem Feldvergleich ab
  10. 10
    BeispielBerechne den Hashcode aus denselben gleichheitsrelevanten Feldern
  11. 11
    HerausforderungErkenne die Überladung, die eine Collection nicht verwendet
  12. 12
    KernaussageSichere Vergleiche in beiden Richtungen
  13. 13
    HerausforderungUnterscheide gleichen Text von gleichen Werten
  14. 14
    ZusammenfassungPrüfe eine Werteklasse an ihrer öffentlichen Schnittstelle
3

Erstelle einen Vertrag und teile seine Implementierung

18 Schritte
Verwende eine abstrakte Quelle, ergänze ein Interface und löse einen Konflikt zwischen geerbten Default-Methoden.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungTrenne gemeinsame Daten von der gemeinsamen Fähigkeit
  2. 2
    BeispielBeginne mit einer abstrakten Nachrichtenquelle
  3. 3
    BeispielVervollständige die fehlende Operation
  4. 4
    HerausforderungEntscheide, ob eine Unterklasse konkret sein kann
  5. 5
    BeispielBenenne das Lesen als Fähigkeit
  6. 6
    VerbindungLass die bestehende Basis das Interface erfüllen
  7. 7
    BeispielSchreibe aufrufenden Code für den Vertrag
  8. 8
    HerausforderungKorrigiere eine Implementierung mit eingeschränktem Zugriff
  9. 9
    BeispielErgänze eine nützliche Default-Operation
  10. 10
    ErklärungBelasse statische und private Hilfsmethoden in ihren Rollen
  11. 11
    BeispielErzeuge einen echten Konflikt zwischen Default-Methoden
  12. 12
    BeispielWähle eine Default-Methode ausdrücklich
  13. 13
    HerausforderungGib der Klassenimplementierung ihren Vorrang
  14. 14
    KernaussageBiete eine Basis an, ohne sie für jede Implementierung vorzuschreiben
  15. 15
    HerausforderungOrdne Beschriftung und Konstruktor dem richtigen Typ zu
  16. 16
    TippPrüfe neue Default-Methoden auf Konflikte mit geerbten Methoden
  17. 17
    HerausforderungFüge einen neuen Leser hinzu, ohne seine bestehende Oberklasse zu ändern
  18. 18
    ZusammenfassungBeurteile einen vorgeschlagenen gemeinsamen Typ
4

Verfolge einen Berichtsaufruf über allgemeine Typen

17 Schritte
Verfolge ein Empfängerobjekt durch die Auswahl einer überladenen Methode, die Laufzeitbindung und einen sicheren Untertyptest und schreibe dann eine allgemeine Schleife.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungVerfolge, was die Variable zusichert und was das Objekt tut
  2. 2
    BeispielGib einem spezialisierten Bericht eine allgemeine Referenz
  3. 3
    ErklärungPrüfe, was die Referenz verfügbar macht
  4. 4
    HerausforderungWähle Typen für eine gemischte Berichtsliste
  5. 5
    BeispielLöse zuerst den äußeren Aufruf auf
  6. 6
    BeispielLöse den inneren Aufruf auf demselben Empfängerobjekt auf
  7. 7
    BeispielWähle eine Überladung anhand des deklarierten Arguments
  8. 8
    HerausforderungTrenne zwei Entscheidungen in einem Aufruf
  9. 9
    TippKennzeichne Felder und statische Aufrufe im Ablauf getrennt
  10. 10
    BeispielRufe die Verkaufsoperation sicher auf
  11. 11
    HerausforderungSage eine Typumwandlung bei geändertem Objekt voraus
  12. 12
    BeispielSammle Überschriften über die gemeinsame Operation
  13. 13
    VerbindungVerwende den Vergleich als weitere gemeinsame Operation
  14. 14
    HerausforderungSortiere Werte durch die Wahl einer Implementierung
  15. 15
    KernaussageVerwende Untertyptests für untertypspezifische Aufgaben
  16. 16
    HerausforderungUnterstütze eine Prüfüberschrift ohne neuen Zweig
  17. 17
    ZusammenfassungVerfolge einen unbekannten Aufruf der Reihe nach
5

Erstelle eine feste Auswahl und einen geschützten Datenwert

15 Schritte
Ergänze Daten in einem Aufzählungstyp, erstelle einen Record und verhindere unbemerkte Änderungen an veränderlichen Listenkomponenten.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungWähle zwischen benannten Optionen und gruppierten Daten
  2. 2
    BeispielGib den Serviceoptionen einen eigenen Typ
  3. 3
    BeispielVerknüpfe die Optionen mit nützlichen Zahlen
  4. 4
    TippHalte die Servicedaten unabhängig von der Deklarationsreihenfolge
  5. 5
    HerausforderungSage das Ergebnis einer Suche und eines Feldzugriffs voraus
  6. 6
    VerbindungVerbinde Verhalten mit der festen Auswahl
  7. 7
    BeispielDeklariere die Komponenten eines Lieferauftrags
  8. 8
    BeispielNormalisiere eingehende Daten vor der Zuweisung
  9. 9
    HerausforderungVergleiche die normalisierten Aufträge
  10. 10
    BeispielFinde das veränderliche Objekt in einer finalen Komponente
  11. 11
    BeispielSpeichere eine nicht modifizierbare Kopie
  12. 12
    TippGib einen neuen Record zurück, wenn Werte geändert werden müssen
  13. 13
    HerausforderungWähle die passende Korrektur für eine veränderliche Route
  14. 14
    HerausforderungModelliere neue Serviceoptionen und Kundenaufträge
  15. 15
    ZusammenfassungPrüfe generiertes Verhalten, bevor du Code hinzufügst

Vererbung in Java: Übungsaufgaben

50 Übungsfragen in 5 Sets

Set 1: Erstelle Klassen und verfolge geerbtes Verhalten10 Übungsfragen

Frage 1 von 10

Eine Library speichert mehrere Shelf-Objekte, und jedes Regal speichert Bücher. Wie sollte Library ihre Beziehung zu Shelf darstellen?
  1. Shelf erweitern, um Regalmethoden zu erben.
  2. Regale in Feldern oder einer Collection speichern.
  3. Shelf zu einer Unterklasse von Library machen.
  4. Library zu einer finalen Unterklasse von Shelf machen.
Antwort zeigenAntwort ausblenden
Antwort: B
Eine Bibliothek hat Regale, daher modellieren Felder oder eine Collection die Beziehung durch Komposition. Das Erweitern von Shelf würde außerdem behaupten, dass jede Bibliothek ein Regal ist und überall dort verwendet werden kann, wo ein Regal erwartet wird. Die Wiederverwendung von Regalmethoden macht diese Ist-ein-Beziehung nicht wahr.

Frage 2 von 10

Account deklariert ein privates Feld balance und eine öffentliche Methode getBalance(). Es gilt SavingsAccount extends Account. Wie kann eine Methode in SavingsAccount den geerbten Kontozustand lesen?
  1. Das Feld als this.balance lesen.
  2. Das Feld als super.balance lesen.
  3. Ein Konto erstellen und dessen Kontostand lesen.
  4. Die geerbte Methode getBalance() aufrufen.
Antwort zeigenAntwort ausblenden
Antwort: D
Der öffentliche Getter wird geerbt und kann das private Feld innerhalb von Account lesen. Der private Zustand gehört weiterhin zum Sparkontoobjekt, aber Code der Unterklasse kann nicht direkt auf dieses Feld zugreifen. Das Hinzufügen von super. umgeht die private Zugriffsregel nicht.

Frage 3 von 10

Was geschieht, wenn dieser Code kompiliert wird?

class Machine {
    Machine(int serial) { }
}
class Printer extends Machine {
    Printer() { }
}
  1. Die Kompilierung schlägt fehl, weil es für super() keinen passenden Konstruktor gibt.
  2. Die Kompilierung gelingt und verwendet standardmäßig Machine(0).
  3. Die Kompilierung gelingt, ohne den Maschinenteil zu initialisieren.
  4. Die Kompilierung schlägt fehl, weil Unterklassen keine parameterlosen Konstruktoren haben können.
Antwort zeigenAntwort ausblenden
Antwort: A
Der leere Druckerkonstruktor ruft implizit super() auf. Machine deklariert nur einen Konstruktor mit einem Ganzzahlparameter, daher existiert kein parameterloser Konstruktor. Java erfindet keinen Argumentwert; Printer() kann parameterlos bleiben, wenn sein Rumpf ausdrücklich eine Seriennummer an super übergibt.

Frage 4 von 10

Was gibt new Leaf(); aus?

class Root {
    Root() { System.out.print("R"); }
}
class Branch extends Root {
    Branch() { System.out.print("B"); }
}
class Leaf extends Branch {
    Leaf() { this(7); System.out.print("L"); }
    Leaf(int n) { System.out.print(n); }
}
  1. L7BR
  2. RBL7
  3. RB7L
  4. RRB7L
Antwort zeigenAntwort ausblenden
Antwort: C
Leaf() delegiert an Leaf(int), dessen impliziter Oberklassenaufruf zu Branch und dann zu Root führt. Deren Rümpfe geben „R“ und „B“ aus, dann gibt Leaf(int) 7 aus und der delegierende Konstruktor „L“. Die Delegation initialisiert den Wurzelteil nicht ein zweites Mal.

Frage 5 von 10

Meter deklariert public int read(). Eine Unterklasse schreibt @Override public int read(int offset). Warum schlägt die Kompilierung fehl?
  1. Der Rückgabetyp muss sich bei jeder Überschreibung in einer Unterklasse ändern.
  2. Die Annotation verhindert, dass öffentliche Methoden geerbt werden.
  3. Die Parameterliste ändert sich, daher ist die Methode eine Überladung.
  4. Die Unterklasse muss vor dem Überschreiben jedes Feld neu deklarieren.
Antwort zeigenAntwort ausblenden
Antwort: C
Die geerbte Methode nimmt keine Argumente entgegen, während die neue Deklaration eine Ganzzahl entgegennimmt. Das ist eine andere Signatur, daher überlädt sie read, statt die Methode zu überschreiben. @Override erkennt die Abweichung; die Rückgabe von int ist mit der ursprünglichen Methode vereinbar.

Frage 6 von 10

Was gibt die letzte Anweisung aus?

class Parcel {
    public String toString() { return "parcel"; }
}
class ChilledParcel extends Parcel {
    @Override public String toString() {
        return super.toString() + " cold";
    }
}
Parcel item = new ChilledParcel();
System.out.println("Send " + item);
  1. „Send parcel“
  2. „Send parcel cold“
  3. „Send ChilledParcel cold“
  4. „Send null cold“
Antwort zeigenAntwort ausblenden
Antwort: B
Die Konkatenation ermittelt den Text des Objekts über toString(), und das tatsächliche Kühlpaketobjekt führt seine Überschreibung aus. Diese Methode erhält „parcel“ von super.toString() und hängt „ cold“ an. Die Deklaration der Referenz als Parcel unterdrückt die Überschreibung nicht.

Frage 7 von 10

Eine finale Klasse Label hat nur public boolean equals(Label other), das ihren Text vergleicht, und erbt equals(Object). Zwei verschiedene Labels haben denselben Text. Beide Referenzen sind in Variablen gespeichert, die als Object deklariert sind. Was gibt a.equals(b) zurück?
  1. false, weil die gewählte Methode weiterhin die Identität vergleicht.
  2. true, weil gleicher Text die Label-Überladung auswählt.
  3. true, weil Object jedes Feld automatisch vergleicht.
  4. Die Kompilierung schlägt fehl, weil Object keine Gleichheitsmethode hat.
Antwort zeigenAntwort ausblenden
Antwort: A
Bei Object-Referenzen wählt der Aufruf equals(Object), das Label nicht überschrieben hat. Die geerbte Implementierung vergleicht die beiden verschiedenen Objekte nach Identität und gibt falsch zurück. Die gleichnamige Methode mit Label als Parameter ist eine Überladung und wird nicht anhand der Laufzeitklasse des Arguments ausgewählt.

Frage 8 von 10

abstract class Sensor deklariert public abstract double reading();. Was muss eine konkrete Klasse LightSensor extends Sensor bereitstellen oder erben?
  1. Einen öffentlichen Konstruktor namens Sensor.
  2. Eine statische Implementierung von double reading().
  3. Eine private Implementierung von double reading().
  4. Eine öffentliche Implementierung von double reading().
Antwort zeigenAntwort ausblenden
Antwort: D
Eine konkrete Unterklasse muss die geerbte abstrakte Operation mit einer kompatiblen Instanzmethode vervollständigen. Ihre Implementierung muss den von Sensor zugesicherten öffentlichen Zugriff beibehalten. Eine statische Methode ist keine Instanzüberschreibung, und eine private Methode würde den erforderlichen Zugriff einschränken.

Frage 9 von 10

Warum lässt sich diese Klasse nicht kompilieren?

interface Resettable { void reset(); }
class Counter implements Resettable {
    void reset() { }
}
  1. reset() muss in Counter statisch sein.
  2. reset() muss in Counter öffentlich sein.
  3. Resettable benötigt einen Konstruktorrumpf.
  4. Counter muss Resettable erweitern.
Antwort zeigenAntwort ausblenden
Antwort: B
Die abstrakte Methode reset() des Interfaces ist implizit öffentlich. Die Implementierung hat Paketzugriff, schwächt damit den zugesicherten Zugriff ab und führt zu einem Kompilierfehler. Eine Klasse implementiert ein Interface; eine Änderung der Beziehung zu extends würde die Methodendeklaration nicht korrigieren.

Frage 10 von 10

Was gibt new TimedJob().cost() zurück?

class Job {
    public int rate() { return 3; }
    public int cost() { return 4 * rate(); }
}
class TimedJob extends Job {
    @Override public int rate() { return 5; }
    @Override public int cost() { return super.cost() + 2; }
}
  1. 22
  2. 14
  3. 20
  4. 28
Antwort zeigenAntwort ausblenden
Antwort: A
Die Überschreibung führt über super.cost() zuerst Job.cost() aus. Dessen gewöhnlicher Aufruf von rate() wird weiterhin auf dem TimedJob-Objekt ausgeführt und gibt daher 5 zurück; die geerbte Berechnung ergibt 20 und die äußere Überschreibung addiert 2. Würde für den inneren Aufruf 3 verwendet, wäre die dynamische Bindung innerhalb der gesamten Oberklassenmethode fälschlich ausgeschaltet.
Set 2: Verwende Verträge und allgemeine Referenztypen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Ein TextDocument und eine nicht verwandte Klasse Image müssen beide exportText() unterstützen. Jede erweitert bereits eine andere Klasse. Welcher gemeinsame Typ drückt diese Fähigkeit am besten aus?
  1. Eine Klasse Exportable, die beide als zweite Oberklasse erweitern.
  2. Eine Oberklasse Image, die auch Textdokumente enthält.
  3. Ein Interface Exportable, das beide implementieren.
  4. Eine Variable Object, die exportText() direkt bereitstellt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Lässt sich dieser Code kompilieren, und warum?

interface Named { String name(); }
class Entity {
    public String name() { return "entity"; }
}
class Asset extends Entity implements Named { }
  1. Ja; die geerbte öffentliche Methode erfüllt den Interface-Vertrag.
  2. Nein; jede implementierende Klasse muss alle Interface-Methoden neu deklarieren.
  3. Nein; die Oberklasse muss dasselbe Interface ausdrücklich implementieren.
  4. Ja; Java erzeugt für fehlende Interface-Methoden einen leeren Rumpf.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Was gibt new Screen().label() zurück?

interface Tagged {
    default String label() { return "tagged"; }
}
class Panel {
    public String label() { return "panel"; }
}
class Screen extends Panel implements Tagged { }
  1. „tagged“
  2. „paneltagged“
  3. Die Deklarationen verursachen einen Kompilierfehler.
  4. „panel“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Fast und Safe sind nicht verwandte Interfaces, die beide default String mode() enthalten. Eine konkrete Klasse Service implementiert beide und erbt keine Klassenimplementierung von mode(). Was muss Service tun?
  1. Das nach implements zuerst genannte Interface verwenden.
  2. mode() überschreiben, um den geerbten Konflikt zu lösen.
  3. Beide Interface-Methoden im Klassenrumpf umbenennen.
  4. Service.super.mode() aus dem Konstruktor aufrufen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Robot implementiert Movable. Welchen deklarierten Typ hat unit in Movable unit = new Robot();?
  1. Robot
  2. Movable
  3. Object
  4. Sowohl Movable als auch Robot
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Vehicle hat move(), und nur Bicycle hat ringBell(). Es gilt Bicycle extends Vehicle. Was geschieht mit v.ringBell() nach Vehicle v = new Bicycle();?
  1. Der Aufruf lässt sich nicht kompilieren, weil Vehicle keine Methode ringBell() hat.
  2. Der Aufruf kompiliert und führt die Fahrradimplementierung von ringBell() aus.
  3. Der Aufruf kompiliert, wirft aber eine Ausnahme, weil das Objekt zu einem Vehicle wurde.
  4. Der Aufruf kompiliert und führt eine leere, von Object geerbte Methode aus.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

interface Percent deklariert static int cap(int n), und Gauge implements Percent deklariert keine Methode cap. Wie sollte Code die Interface-Hilfsmethode mit 120 aufrufen?
  1. Gauge.cap(120)
  2. new Gauge().cap(120)
  3. Percent.super.cap(120)
  4. Percent.cap(120)
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine Methode durchläuft nur Formen und ruft area() auf. Sie muss sowohl ArrayList<Shape> als auch LinkedList<Shape> akzeptieren. Welcher Parametertyp stellt die benötigten Listenoperationen bereit?
  1. ArrayList<Shape>
  2. LinkedList<Shape>
  3. List<Shape>
  4. Object
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Was geschieht beim Kompilieren und Ausführen von new Row().render()?

interface Renderable { String render(); }
abstract class Base implements Renderable {
    public String render() { return "[" + body() + "]"; }
    protected abstract String body();
}
class Row extends Base {
    @Override protected String body() { return "row"; }
}
  1. Es schlägt fehl, weil Row render() neu deklarieren muss.
  2. Es kompiliert und gibt „[row]“ zurück.
  3. Es schlägt fehl, weil body() öffentlich sein muss.
  4. Es kompiliert und gibt „[]“ zurück.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein Berichtsframework hat eine abstrakte Klasse Report mit privatem Titelzustand, einem Konstruktor zum Setzen dieses Zustands und einem abstrakten body(). Berichte und nicht verwandte Diagrammobjekte müssen außerdem druckbar sein. Welche Struktur erhält beide Anforderungen?
  1. Report durch ein Interface nur mit Default-Methoden ersetzen, das jeden Titel speichert.
  2. Diagramme Report erweitern lassen, auch wenn sie keine Berichtsobjekte sind.
  3. Report behalten und den betreffenden Klassen ein Interface Printable hinzufügen.
  4. Berichtsklassen sowohl Report als auch ein abstraktes Printable erweitern lassen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 3: Verfolge Aufrufe, verdeckte Mitglieder und geprüfte Typumwandlungen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Auf Shape s = null; folgt s instanceof Circle. Es gilt Circle extends Shape. Was ist das Ergebnis der Prüfung?
  1. false
  2. true
  3. Eine ClassCastException wird geworfen.
  4. Eine NullPointerException wird geworfen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Was geschieht mit der letzten Anweisung?

class Animal { }
class Fox extends Animal { }
Animal a = new Animal();
Fox f = (Fox) a;
  1. Sie lässt sich nicht kompilieren, weil Downcasts nie erlaubt sind.
  2. Sie erzeugt einen Fox mit dem Zustand des ursprünglichen Animal.
  3. Sie kompiliert und wirft dann eine ClassCastException.
  4. Sie gelingt und weist f dasselbe einfache Animal-Objekt zu.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Was gibt pick(value) zurück?

static String pick(Object x) { return "object"; }
static String pick(CharSequence x) { return "sequence"; }
// Inside a method of the class containing these overloads:
Object value = "notes";
  1. „sequence“
  2. „notes“
  3. Der Aufruf wirft eine ClassCastException.
  4. „object“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Welcher Text entsteht durch p.label + ":" + p.show()?

class Parent {
    String label = "P";
    String show() { return label; }
}
class Child extends Parent {
    String label = "C";
    @Override String show() { return label; }
}
Parent p = new Child();
  1. „P:P“
  2. „P:C“
  3. „C:C“
  4. „C:P“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine Methode akzeptiert List<Vehicle> und darf einen Bus hinzufügen. Es gilt Bicycle extends Vehicle und Bus extends Vehicle. Warum kann ihr keine List<Bicycle> übergeben werden?
  1. Ein Bicycle-Objekt kann keiner Vehicle-Referenz zugewiesen werden.
  2. Listen verhindern Vererbung, wenn ihre Elemente veränderliche Objekte sind.
  3. Die Methode könnte einen Bus in eine Liste einfügen, die Fahrräder enthalten soll.
  4. Die Methode würde jedes Fahrradobjekt in ein einfaches Vehicle umwandeln.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Was gibt b.test() zurück?

class Base {
    String use(Object x) { return "BO"; }
    String use(String x) { return "BS"; }
}
class Sub extends Base {
    @Override String use(Object x) { return "SO"; }
    @Override String use(String x) { return "SS"; }
    String test() {
        Object text = "x";
        return super.use(text) + ":" + use(text);
    }
}
Sub b = new Sub();
  1. „BO:SO“
  2. „BS:SS“
  3. „BO:SS“
  4. „SO:SO“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Was gibt new Sub(); mit Standard-Java 25 aus?

class Base {
    Base() { System.out.print(value()); }
    int value() { return -1; }
}
class Sub extends Base {
    int n = 8;
    Sub() { super(); n = 11; }
    @Override int value() { return n; }
}
  1. -1
  2. 8
  3. 11
  4. 0
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Was gibt dieser Code aus? Tool deklariert keine Methoden.

class Tool { }
class Drill extends Tool {
    int speed() { return 6; }
}
Tool tool = new Drill();
Object copy = tool;
if (copy instanceof Drill d) {
    System.out.print(d.speed());
}
System.out.print(":" + (copy == tool));
  1. „6:false“
  2. „6:true“
  3. „0:true“
  4. Der Aufruf der Geschwindigkeitsmethode lässt sich nicht kompilieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Creator hat public Object create(). TextCreator extends Creator überschreibt die Methode. Welche Kombination aus Rückgabetyp und Zugriff ist gültig?
  1. protected String create()
  2. public String create()
  3. public int create()
  4. private Object create()
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein Plugin-System hat viele Unterklassen von Document. Seine Vorschaumethode verwendet instanceof-Zweige, um die Wortzahl jedes Dokuments zu berechnen. Häufig werden neue Dokumentklassen hinzugefügt. Welche Änderung lässt neue Klassen die Vorschau unterstützen, ohne diese Typtestkette zu ändern?
  1. Jedes Document zu Object umwandeln und dieselben Untertypzweige behalten.
  2. Die Untertypzweige in eine statische Methode der Vorschauklasse verschieben.
  3. Jedem Untertyp ein gleichnamiges Feld geben und es über Document lesen.
  4. wordCount() in Document deklarieren und in jedem konkreten Untertyp implementieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 4: Modelliere Werte, vergleiche und sortiere sie10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Eine Anwendung verwendet enum Status { NEW, SENT, DONE }. Was bewirkt Status.valueOf("sent")?
  1. Es gibt Status.SENT zurück.
  2. Es gibt den String „sent“ zurück.
  3. Es gibt Status.NEW zurück.
  4. Es wirft eine IllegalArgumentException.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welcher generierte Aufruf liest bei record Reading(int count, String unit) { } den Zählwert aus einer Variablen r?
  1. r.getCount()
  2. r.count()
  3. r.count
  4. r.readCount()
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

enum Band { LOW(2), HIGH(9); ... } speichert das Konstruktorargument in einem unveränderlichen Feld code. Eine neue Konstante MID(5) wird zwischen LOW und HIGH eingefügt. Wie ändern sich ordinal() und der gespeicherte Code von HIGH?
  1. Der Ordinalwert wird 2; der Code bleibt 9.
  2. Der Ordinalwert bleibt 1; der Code wird 2.
  3. Der Ordinalwert wird 2; der Code wird 2.
  4. Der Ordinalwert bleibt 1; der Code bleibt 9.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Zwei ungleiche Token-Objekte geben denselben Hashcode zurück. Ihre equals(Object)-Implementierungen vergleichen die Felder, die den jeweiligen Token bestimmen, korrekt. Verletzt der gemeinsame Hashcode den Gleichheits- und Hashcode-Vertrag?
  1. Ja; jedes Objekt muss einen eindeutigen Hashcode haben.
  2. Ja; durch denselben Hashcode werden die Token gleich.
  3. Nein; ungleiche Objekte dürfen denselben Hashcode haben.
  4. Nein; Hash-Collections ignorieren Gleichheit nach der Hashcode-Berechnung.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

record Result(int seconds) implements Comparable<Result> gibt Integer.compare(seconds, other.seconds) zurück. Welches Vorzeichen hat new Result(418).compareTo(new Result(405))?
  1. Negativ.
  2. Positiv.
  3. Null.
  4. Es gibt kein Ergebnis, weil Records keine Interfaces implementieren können.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Was gibt die letzte Anweisung aus?

record Tag(String text) {
    Tag { text = text.trim(); }
}
Tag a = new Tag(" north ");
Tag b = new Tag("north");
System.out.println(a.equals(b));
  1. true
  2. false
  3. „north“
  4. Die Kompilierung schlägt fehl, weil Parametern keine neuen Werte zugewiesen werden können.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Eine Basismethode Ticket.equals akzeptiert jedes Ticket mit demselben Code. PriorityTicket extends Ticket akzeptiert nur PriorityTicket-Objekte und vergleicht zusätzlich die Priorität. Ein einfaches Ticket t und ein Prioritätsticket p haben denselben Code. Was geben t.equals(p) und p.equals(t) zurück, und ist das symmetrisch?
  1. Beide geben falsch zurück; das Paar ist symmetrisch.
  2. Beide geben wahr zurück; das Paar ist symmetrisch.
  3. Sie geben wahr und falsch zurück; das Paar verletzt die Symmetrie.
  4. Sie geben falsch und wahr zurück; das Paar verletzt die Symmetrie.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Was wird ausgegeben? List und ArrayList sind importiert.

record Batch(List<String> names) { }
var original = new ArrayList<String>(List.of("Rin"));
Batch first = new Batch(original);
Batch second = new Batch(new ArrayList<>(original));
original.add("Sol");
System.out.println(first.equals(second) + ":"
    + first.names().size() + ":" + second.names().size());
  1. „true:2:1“
  2. „false:1:1“
  3. „true:1:1“
  4. „false:2:1“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Was geschieht, wenn Collections.sort(list) ausgeführt wird? Die benötigten Typen aus java.util sind importiert.

record Rank(int score) implements Comparable<Rank> {
    public int compareTo(Rank other) {
        return Integer.compare(score, other.score);
    }
}
List<Rank> list = List.of(new Rank(8), new Rank(3));
  1. Die Liste wird nach den Punktzahlen 3 und dann 8 sortiert.
  2. Die Kompilierung schlägt fehl, weil Rank keine Vergleichsmethode hat.
  3. Es wird eine UnsupportedOperationException geworfen.
  4. Die Reihenfolge 8 und dann 3 bleibt, weil die Record-Reihenfolge feststeht.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Was gibt Rule.DOUBLE.apply(Rule.NEXT.apply(4)) zurück?

enum Rule {
    NEXT {
        int apply(int n) {
            return switch (n) { case 4 -> 6; default -> n + 1; };
        }
    },
    DOUBLE { int apply(int n) { return 2 * n; } };
    abstract int apply(int n);
}
  1. 12
  2. 10
  3. 9
  4. 16
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 5: Kombiniere Java-Regeln und untersuche Fehler10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welcher Java-Typ eignet sich für eine feste Auswahl von Exportmöglichkeiten namens PDF, TEXT und HTML?
  1. Ein Record mit drei veränderlichen Auswahlfeldern.
  2. Ein Aufzählungstyp, der diese drei Konstanten deklariert.
  3. Eine Ganzzahl, deren gültige Werte nicht dokumentiert sind.
  4. Eine Oberklasse mit einer Unterklasse für jede Datei.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Was gibt new Terminal().name() zurück?

interface Basic {
    default String name() { return "basic"; }
}
interface Detailed extends Basic {
    default String name() { return "detail"; }
}
class Terminal implements Basic, Detailed { }
  1. „basic“
  2. „basicdetail“
  3. „detail“
  4. Die Klasse hat einen ungelösten Konflikt zwischen Default-Methoden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine List<Notifier> enthält Objekte mehrerer nicht verwandter Klassen, die Notifier.send() implementieren. Eine Schleife ruft für jedes Element n.send() auf. Ein neuer PushNotifier implementiert denselben Vertrag korrekt. Welche Änderung benötigt die Schleife, um dessen Instanzen zu unterstützen?
  1. Keine Änderung; der bestehende Interface-Aufruf wird an die jeweilige Implementierung gebunden.
  2. Vor jedem Aufruf von send() einen Cast zu PushNotifier hinzufügen.
  3. Für jedes Element die Interface-Variable durch eine Object-Variable ersetzen.
  4. Einen Zweig hinzufügen, damit alle älteren Objekte die Implementierung der neuen Klasse aufrufen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Was wird mit Standard-Java 25 ausgegeben?

class Base {
    Base(int n) { System.out.print("B" + n); }
}
class Sub extends Base {
    Sub(int n) {
        if (n < 0) throw new IllegalArgumentException();
        super(n + 1);
        System.out.print("S" + n);
    }
}
new Sub(2);
  1. „S2B3“
  2. „B2S2“
  3. Die Kompilierung schlägt fehl, weil super nicht zuerst steht.
  4. „B3S2“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine veränderliche Klasse Code definiert Gleichheit und Hashcode anhand ihres Textfelds. Ein HashSet<Code> enthält c mit dem Text „red“. Das Programm muss den Text in „blue“ ändern, dasselbe Objekt behalten und sicherstellen, dass es in der Menge zuverlässig gefunden wird. In welcher Reihenfolge sollte es Objekt und Menge aktualisieren?
  1. Den Text ändern, das Objekt entfernen und dann erneut hinzufügen.
  2. Das Objekt erneut hinzufügen, den Text ändern und es dann entfernen.
  3. Das Objekt entfernen, erneut hinzufügen und dann den Text ändern.
  4. Das Objekt entfernen, den Text ändern und es dann erneut hinzufügen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Was ergibt x.call(arg) + ":" + x.call("x")?

class Alpha {
    String call(Object x) { return "AO"; }
    String call(String x) { return "AS"; }
}
class Beta extends Alpha {
    @Override String call(Object x) { return "BO"; }
    @Override String call(String x) { return "BS"; }
}
Alpha x = new Beta();
Object arg = "data";
  1. „AO:AS“
  2. „BO:BS“
  3. „BO:AS“
  4. „BS:BS“
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Was gibt die letzte Anweisung aus? List und ArrayList sind importiert.

record Group(List<String> names) {
    Group { names = List.copyOf(names); }
}
var original = new ArrayList<String>();
original.add("Uma");
Group group = new Group(original);
original.add("Zed");
System.out.println(group.names().size() + ":" + original.size());
  1. „1:2“
  2. „2:2“
  3. „1:1“
  4. Der Record-Konstruktor lässt sich nicht kompilieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Was geschieht, wenn dieser Code kompiliert wird?

interface Ready {
    default String state() { return "ready"; }
}
abstract class Base {
    public abstract String state();
}
class Console extends Base implements Ready { }
  1. Die Kompilierung gelingt; Console erbt die Default-Implementierung von Ready.
  2. Die Kompilierung gelingt; die Base-Methode gibt bis zum Überschreiben null zurück.
  3. Die Kompilierung schlägt fehl; Console muss die geerbte abstrakte Methode implementieren.
  4. Die Kompilierung schlägt fehl; abstrakte Klassen können nicht mit Interfaces kombiniert werden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Zwei Objekte von record Player(String name, int points) haben unterschiedliche Namen, aber dieselbe Punktzahl. Ihr eigenes compareTo vergleicht mit Integer.compare nur die Punktzahl. Was ergeben equals und compareTo für diese beiden Objekte?
  1. equals ist wahr; compareTo ist null.
  2. equals ist falsch; compareTo muss ungleich null sein.
  3. equals ist wahr; compareTo muss ungleich null sein.
  4. equals ist falsch; compareTo ist null.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Bibliothek stellt bereits interface Storable { String saveText(); } bereit. Viele neue Implementierungen teilen ein privates Pfadfeld, eine Validierung im Konstruktor und Hilfscode, während einige bestehende Implementierungen andere Klassen erweitern. Wie kann die Bibliothek Wiederverwendung anbieten und die gemeinsame Schnittstelle erhalten?
  1. Eine abstrakte Storable-Implementierung hinzufügen, die Klassen erweitern dürfen.
  2. Storable durch eine Klasse ersetzen, die jede Implementierung erweitern muss.
  3. Das private Instanzfeld für den Pfad in das Interface Storable verschieben.
  4. Den bestehenden Implementierungen eine zweite Oberklasse für die gemeinsame Speicherung hinzufügen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

72 Schlüsselbegriffe, jeweils mit Definition

A

Abstrakte Klasse
Eine abstrakte Klasse ist eine Klasse, die nicht direkt instanziiert werden kann und gemeinsame Felder und Code mit unvollendeten Operationen verbinden kann.
Eine abstrakte Klasse Shape kann einen Namen speichern und zugleich von konkreten Kreisen und Rechtecken verlangen, eine Fläche bereitzustellen.
Abstrakte Methode
Eine abstrakte Methode deklariert eine erforderliche Operation ohne Rumpf und überlässt es konkreten Unterklassen, ihre Implementierung bereitzustellen oder zu erben.
Die abstrakte Methode area() verhindert, dass eine unvollendete konkrete Form unbemerkt einen Platzhalter mit der Fläche null verwendet.
Aufruf des Oberklassenkonstruktors
Ein Aufruf des Oberklassenkonstruktors verwendet super(...), um Argumente für das aktuelle Objekt an einen Konstruktor der direkten Oberklasse zu übergeben.
Ein Aufruf des Oberklassenkonstruktors übergibt den Namen eines Studenten an Person, wo das private Namensfeld deklariert ist.
Aufzählungskonstante
Eine Aufzählungskonstante ist ein benannter Wert, der in einem Aufzählungstyp deklariert ist und genau eine Instanz hat, die Daten enthalten und konstantenspezifisches Verhalten bereitstellen kann.
Die Aufzählungskonstante CupSize.LARGE kann ein Fassungsvermögen von 500 mL speichern und auf Referenzgleichheit verglichen werden.
Aufzählungstyp
Ein Aufzählungstyp ist ein Java-Typ, der eine feste Menge benannter Konstanten definiert und verhindert, dass gewöhnlicher aufrufender Code beliebige Strings oder Zahlen einsetzt.
Ein Aufzählungstyp für Lieferzustände kann eine Bestellung auf die benannten Werte NEW, SENT und DONE beschränken.

C

ClassCastException
Eine ClassCastException ist ein Laufzeitfehler durch eine Referenzumwandlung, bei der ein Objekt, das nicht null ist, mit dem verlangten Zieltyp unvereinbar ist.
Eine ClassCastException tritt auf, wenn ein einfaches Animal-Objekt in seine spezifischere Unterklasse Fox umgewandelt wird.
Comparable
Comparable ist das Java-Interface, über das ein Typ seine natürliche Ordnung definiert, indem er eine Instanz mit compareTo mit einer anderen vergleicht.
Ein Läufer implementiert Comparable<Runner>, damit eine Sortiermethode Läufer nach ihrer Zielzeit ordnen kann.

D

Default-Methode
Eine Default-Methode ist eine öffentliche Interface-Instanzmethode mit Rumpf, die implementierende Klassen erben und durch eine Überschreibung ersetzen dürfen.
Eine Default-Methode needsCharge() kann batteryPercent() verwenden, ohne die Struktur der privaten Felder eines Geräts zu kennen.
Deklarierter Typ
Der deklarierte Typ ist der einer Variablen im Quellcode zugewiesene statische Typ und bestimmt, welche Operationen der Compiler über sie erlaubt.
In Person p = new Student(...) ist der deklarierte Typ Person, sodass Methoden, die nur Studenten besitzen, nicht direkt verfügbar sind.
Downcasting
Downcasting behandelt eine Referenz nach einer Kompatibilitätsprüfung zur Laufzeit als spezifischeren Typ, ohne ihr Objekt umzuwandeln oder zu ersetzen.
Der Downcast einer Personenreferenz zu einem Studenten funktioniert nur, wenn das referenzierte Objekt bereits den Typ Student hat.
Dynamische Bindung
Dynamische Bindung wählt zur Laufzeit anhand der tatsächlichen Klasse des Empfängerobjekts die Implementierung einer überschriebenen Instanzmethode aus.
Dynamische Bindung ermöglicht einem geerbten hello(), das who() aufruft, die Methode B.who() zu erreichen, wenn das Empfängerobjekt zur Klasse B gehört.
Dynamischer Typ
Der dynamische Typ ist die Klasse des Objekts, auf das eine Referenz verweist. Er wird bei der Erzeugung des Objekts festgelegt und bei der Auswahl von Instanzmethodenüberschreibungen verwendet.
Eine Person-Referenz kann auf einen Studenten verweisen, dessen dynamischer Typ nach dem Upcast Student bleibt.

E

Einfachvererbung
Einfachvererbung bedeutet, dass eine Java-Klasse höchstens eine direkte Oberklasse hat, obwohl sie zu einer längeren Kette von Unterklassen gehören kann.
Einfachvererbung verhindert, dass eine Klasse sowohl Student als auch Teacher als direkte Oberklassen erweitert.
Empfängerobjekt
Ein Empfängerobjekt ist das Objekt, auf dem eine Instanzmethode aufgerufen wird und dessen tatsächliche Klasse die dynamische Bindung der ausgewählten Methodensignatur bestimmt.
In shape.area() ist das Empfängerobjekt das Formobjekt, selbst wenn die Variable einen abstrakten deklarierten Typ hat.

F

Fähigkeit
Eine Fähigkeit ist eine Operation oder eine Gruppe von Operationen, deren Unterstützung ein Objekt zusichert; sie wird für nicht verwandte Klassen häufig durch ein Interface ausgedrückt.
Exportieren ist eine Fähigkeit, die eine Textbeschriftung und ein Kreis beide bereitstellen können, während sie unterschiedliche Oberklassen behalten.
Feldinitialisierer
Ein Feldinitialisierer ist ein Ausdruck in einer Felddeklaration, der den Anfangswert des Felds zuweist, wenn dessen Initialisierungsphase ausgeführt wird.
Ein Instanzfeldinitialisierer in einer Unterklasse wird ausgeführt, nachdem der Oberklassenkonstruktor des Objekts zurückgekehrt ist.
final-Methode
Eine final-Methode ist eine Methode, deren Implementierung nicht von einer Unterklasse überschrieben werden kann, sodass diese Implementierung in abgeleiteten Klassen erhalten bleibt.
Eine final-Methode zur Berechnung eines geprüften Werts verhindert, dass eine Unterklasse diese Berechnung ersetzt.
Flache Unveränderlichkeit
Flache Unveränderlichkeit legt die eigenen Feldreferenzen oder Werte eines Objekts fest, erlaubt aber Änderungen an veränderlichen Objekten, die über diese Referenzen erreicht werden.
Durch flache Unveränderlichkeit behält ein Record dieselbe Listenreferenz, selbst wenn über eine andere Referenz ein Name zu dieser Liste hinzugefügt wird.
Flexibler Konstruktorrumpf
Ein flexibler Konstruktorrumpf erlaubt eingeschränkte Anweisungen vor einem expliziten Konstruktoraufruf; dies ist seit Java 25 eine Standardfunktion.
Ein flexibler Konstruktorrumpf kann einen negativen Parameter ablehnen, bevor er ihn an den Oberklassenkonstruktor übergibt.

G

Ganzzahlüberlauf
Ein Ganzzahlüberlauf tritt auf, wenn eine Ganzzahlberechnung den darstellbaren Bereich überschreitet; dadurch kann eine Subtraktion ein falsches Vorzeichen für die Ordnung liefern.
Ein Ganzzahlüberlauf kann einen Vergleich durch Subtraktion fehlerhaft machen; Integer.compare vermeidet diesen Vergleichsfehler.
Gemeinsamer Zustand
Gemeinsamer Zustand in einer Oberklasse sind die gemeinsamen Daten pro Objekt, die verwandte Unterklassen als Teil des Basisklassenteils jedes Objekts erhalten.
Name und Position einer Form sind gemeinsamer Zustand, wenn eine abstrakte Oberklasse sie für jede Form definiert.
Geschützter Zugriff
Geschützter Zugriff erlaubt den Zugriff innerhalb eines Pakets und eingeschränkten Zugriff durch Unterklassen über Paketgrenzen hinweg, ohne ein Mitglied öffentlich zu machen.
Geschützter Zugriff ermöglicht einer Unterklasse, eine Hilfsmethode der Oberklasse zu verwenden, ohne diese in die öffentliche Schnittstelle aufzunehmen.
Gleichheitsvertrag
Der Gleichheitsvertrag verlangt reflexive, symmetrische, transitive und konsistente Vergleiche; außerdem muss jedes Objekt, das nicht null ist, beim Vergleich mit null ungleich sein.
Der Gleichheitsvertrag schließt aus, dass ein Buch ein E-Book akzeptiert, wenn das E-Book dasselbe Buch umgekehrt ablehnt.
Gleichstand in der Ordnung
Ein Gleichstand in der Ordnung liegt vor, wenn compareTo null zurückgibt und zwei Objekte nach der gewählten Ordnungsregel denselben Vergleichsrang einnehmen.
Ein Gleichstand nach Rennzeit kann bei Läufern mit unterschiedlichen Namen auftreten, selbst wenn ihre Records ungleich sind.

H

Hashcode
Ein Hashcode ist eine Ganzzahl, mit der Kandidaten in einer Hash-Collection gefunden werden; gleich verglichene Objekte müssen denselben Hashcode zurückgeben.
Der Hashcode eines Buches kann aus seinem Katalogcode und seiner Auflage berechnet werden, wenn diese Felder auch die Gleichheit bestimmen.
Hashkollision
Eine Hashkollision tritt auf, wenn ungleiche Objekte denselben Hashcode zurückgeben. Dies ist erlaubt, weil der Gleichheitsvergleich sie unterscheiden kann.
Eine Hashkollision zwischen zwei unterschiedlichen Token verlangt nicht, dass ihre Gleichheitsmethode wahr zurückgibt.

I

Identität
Identität bedeutet, dass Referenzen auf genau dasselbe Objekt verweisen, unabhängig davon, ob separate Objekte zufällig gleiche Werte enthalten.
Zwei separat erzeugte Bücher können gleiche Codes haben, während ihre unterschiedliche Identität a == b falsch ergibt.
Implementierung
Eine Implementierung ist funktionsfähiger Code, der einen Methodenvertrag erfüllt und von einer Klasse bereitgestellt oder über eine geeignete Methodendeklaration geerbt wird.
Eine von Entity geerbte öffentliche Implementierung kann Named.name() für eine Unterklasse Asset erfüllen.
instanceof
Der instanceof-Operator prüft, ob eine Referenz eine Instanz eines angegebenen Typs oder Untertyps bezeichnet, und gibt falsch zurück, wenn die Referenz null ist.
Ein instanceof-Test ermöglicht zu prüfen, ob eine Person den Typ Student hat, bevor eine studentenspezifische Methode verwendet wird.
Interface
Ein Interface ist ein benannter Typvertrag, der Klassen eine gemeinsame Fähigkeit ermöglicht, ohne dass sie eine zustandsbehaftete Oberklasse teilen müssen.
Das Interface Chargeable ermöglicht einem Telefon und einem Roller dieselben Aufrufe, obwohl sie unterschiedlich schnell laden.
Interface-Konstante
Eine Interface-Konstante ist ein implizit als public, static und final deklariertes Feld, das zum Interface und nicht zu einzelnen Objekten gehört.
Die Interface-Konstante Chargeable.FULL kann 100 enthalten, während jedes Gerät sein eigenes veränderliches Batteriefeld speichert.
Interface-Typ
Ein Interface-Typ ist ein Typ für Referenzen, Parameter oder Ergebnisse, der eine Fähigkeit verfügbar macht und dabei unterschiedliche implementierende Klassen erlaubt.
Die Verwendung von List als Interface-Typ ermöglicht einer Methode, sowohl Array-basierte als auch verkettete Listen zu akzeptieren.

K

Kanonischer Konstruktor
Ein kanonischer Konstruktor ist ein Record-Konstruktor, dessen Parameter mit der Komponentenliste übereinstimmen und die Komponentenfelder des Records initialisieren.
Der kanonische Konstruktor von Point(int x, int y) nimmt zuerst die x-Koordinate und danach die y-Koordinate entgegen.
Kompakter Konstruktor
Ein kompakter Konstruktor ist ein Record-Konstruktorrumpf, der ohne Wiederholung seiner Parameter geschrieben wird; danach erfolgt die automatische Zuweisung an die Komponentenfelder.
Ein kompakter Konstruktor kann äußere Leerzeichen eines eingehenden Namens entfernen, bevor dieser normalisierte Wert dem finalen Komponentenfeld zugewiesen wird.
Komposition
Komposition modelliert eine Hat-ein-Beziehung, indem Referenzen auf andere Objekte in Feldern gespeichert werden, statt zu behaupten, selbst diese Objekte zu sein.
Komposition ermöglicht einem Customer, eine Address zu enthalten, ohne jeden Kunden als Adresse verwendbar zu machen.
Konflikt zwischen Default-Methoden
Ein Konflikt zwischen Default-Methoden entsteht, wenn nicht verwandte Interfaces konkurrierende geerbte Default-Methoden bereitstellen, die die implementierende Klasse auflösen muss.
Ein Konflikt zwischen Default-Methoden verlangt, dass SmartLamp die übereinstimmenden Beschreibungen von Lamp und Speaker überschreibt.
Konkrete Klasse
Eine konkrete Klasse ist eine nicht abstrakte Klasse, die Implementierungen aller erforderlichen abstrakten Methoden bereitstellt oder erbt und Instanzen haben kann.
Rectangle ist eine konkrete Klasse, sobald sie die von ihrer abstrakten Formbasis verlangte Flächenoperation vervollständigt.
Konkrete Methode
Eine konkrete Methode hat einen Implementierungsrumpf, sodass aufrufender Code ihre Anweisungen ausführen kann, statt auf die Vervollständigung einer Deklaration durch eine Unterklasse angewiesen zu sein.
Die konkrete Methode Shape.describe() kann den Namen wiederverwenden und die Implementierung von area() der jeweiligen Unterklasse aufrufen.
Konstantenspezifischer Klassenrumpf
Ein konstantenspezifischer Klassenrumpf ist der eigene Klassenrumpf einer Aufzählungskonstante, der Verhalten speziell für diesen benannten Wert des Aufzählungstyps bereitstellt.
Ein konstantenspezifischer Klassenrumpf ermöglicht MULTIPLY, eine Operation anders zu implementieren als die Konstante ADD des Aufzählungstyps.
Konstruktorkette
Eine Konstruktorkette ist eine Folge von Aufrufen von Konstruktoren derselben Klasse und von Oberklassenkonstruktoren, die die Teile eines neuen Objekts initialisiert.
Eine Konstruktorkette für eine Blattklasse erreicht deren Basiskonstruktoren, bevor der restliche Rumpf der Blattklasse ausgeführt wird.
Kovarianter Rückgabetyp
Ein kovarianter Rückgabetyp ist ein Untertyp, der bei einer Überschreibung als Referenzrückgabetyp anstelle des ursprünglichen allgemeineren Rückgabetyps verwendet wird.
Ein kovarianter Rückgabetyp erlaubt einer Überschreibung, String zurückzugeben, wenn die Oberklassenmethode Object zurückgibt.

L

Laufzeitklasse
Die Laufzeitklasse ist die tatsächliche Klasse eines Objekts, die getClass() unabhängig vom deklarierten Typ seiner Referenz meldet.
Die Laufzeitklasse eines Studenten bleibt Student, selbst wenn eine Methode ihn über einen Person-Parameter erhält.

M

Methodensignatur
Eine Methodensignatur bestimmt eine Methode für die Auswahl überladener Methoden und das Überschreiben anhand ihres Namens und ihrer Parametertypen; der Rückgabetyp allein reicht nicht aus.
Die Änderung eines Parameters von Object zu String ändert die Methodensignatur und erzeugt eine Überladung.
Mustervariable
Eine Mustervariable ist eine typisierte lokale Variable, die durch ein erfolgreiches Typmuster eingeführt wird und dort verfügbar ist, wo dieses passende Ergebnis als gegeben bekannt ist.
Die Mustervariable student in person instanceof Student student macht die Kennungsmethode des passenden Objekts sicher zugänglich.

N

Natürliche Ordnung
Eine natürliche Ordnung ist die durch die Vergleichsmethode eines Typs definierte Standardordnung, die Sortieroperationen verwenden, wenn keine alternative Ordnung verlangt wird.
Die natürliche Ordnung eines Läufers kann beim Sortieren einer Liste kleinere Zielzeiten vor größeren einordnen.
Nicht modifizierbare Kopie
Eine nicht modifizierbare Kopie bewahrt den Inhalt einer Collection vor späteren strukturellen Änderungen über die ursprüngliche oder die zurückgegebene Collection-Referenz.
Eine nicht modifizierbare Kopie aus List.copyOf schützt die Mitgliederliste eines Teams vor Ergänzungen der veränderlichen Liste des aufrufenden Codes.

O

Oberinterface
Ein Oberinterface ist ein Interface, das von einem anderen Interface erweitert wird und dessen erforderliche Operationen Teil des erweiternden Vertrags werden.
Chargeable ist ein Oberinterface von SmartDevice, wenn Letzteres es zusammen mit Connectable erweitert.
Oberklasse
Eine Oberklasse ist eine Klasse, deren zugängliche Mitglieder eine Unterklasse erbt und die in ihrer Klassenbeziehung den allgemeineren Typ bildet.
Person ist die Oberklasse von Student, sodass eine Methode, die eine Person akzeptiert, auch einen Studenten erhalten kann.
Object
Object ist die Wurzelklasse der Java-Klassenhierarchie und stellt gemeinsame Methoden bereit, die gewöhnliche Klassen direkt oder indirekt erben.
Eine Object-Variable kann einen Studenten oder ein Array enthalten, und beide unterstützen die Methoden, die jedes Objekt hat.
Ordinalwert
Ein Ordinalwert ist die bei null beginnende Deklarationsposition einer Aufzählungskonstante, die sich ändern kann, wenn vor diesem Wert Konstanten eingefügt werden.
In einem als LOW, HIGH deklarierten Aufzählungstyp ändert sich der Ordinalwert von HIGH von 1 auf 2, wenn MID davor eingefügt wird.
Override-Annotation
Die Override-Annotation, geschrieben als @Override, weist den Compiler an zu prüfen, ob eine Methode eine geerbte Methode überschreibt oder implementiert.
Die Override-Annotation erkennt tostring(), wenn eigentlich toString() ersetzt werden sollte.

P

Polymorphie
Polymorphie ermöglicht einer allgemeinen Referenz, über gemeinsame Operationen mit unterschiedlichen Untertypen zu arbeiten, wobei jedes Objekt sein eigenes Verhalten bereitstellt.
Polymorphie ermöglicht einer Schleife, area() auf Kreisen, Rechtecken und Dreiecken ohne separate Typzweige aufzurufen.
Private Interface-Methode
Eine private Interface-Methode ist eine Hilfsmethode mit Rumpf, die nur für Code innerhalb dieses Interfaces sichtbar ist und dessen funktionsfähigen Methoden das Teilen von Code ermöglicht.
Eine private Interface-Methode kann doppelte Prüfungen aus zwei Default-Methoden entfernen, ohne Teil des öffentlichen Vertrags zu werden.

R

Record
Ein Record ist eine kompakte Java-Klasse für eine feste Gruppe von Komponentenwerten, für die Konstruktion, Zugriff, Gleichheitsvergleich, Hashcode-Berechnung und Textdarstellung generiert werden.
Ein Record Point(int x, int y) gruppiert zwei Koordinaten und stellt komponentenbasierte Gleichheit ohne handgeschriebene Methoden bereit.
Record-Komponente
Eine Record-Komponente ist ein im Record-Kopf deklarierter Wert, der ein privates finales Feld, einen Konstruktorparameter und eine öffentliche Zugriffsmethode bestimmt.
Die Record-Komponente seconds kann bei einem Läufer mit der generierten Methode runner.seconds() gelesen werden.

S

Spezifischeres Interface
Ein spezifischeres Interface erweitert ein anderes Interface und hat Vorrang, wenn beide einer implementierenden Klasse eine passende Default-Methode bereitstellen.
Ein spezifischeres Interface Detailed liefert das gewählte name(), wenn es Basic erweitert und dessen Default-Methode ersetzt.
Standardkonstruktor
Ein Standardkonstruktor ist ein parameterloser Konstruktor, den Java bereitstellt, wenn eine Klasse keinen Konstruktor deklariert; sein Rumpf enthält einen Aufruf von super().
Das Hinzufügen von Person(String name) entfernt den Standardkonstruktor, auf den eine Unterklasse möglicherweise angewiesen war.
Statische Interface-Methode
Eine statische Interface-Methode ist eine Methode, die zum Interface selbst gehört, über dessen Namen aufgerufen und von implementierenden Klassen nicht geerbt wird.
Die statische Interface-Methode Chargeable.clamp(130) kann 100 zurückgeben, ohne ein Geräteobjekt zu erzeugen.
switch-Ausdruck
Ein switch-Ausdruck wählt ein Ergebnis aus passenden Fällen und muss die zulässigen Eingaben abdecken; Aufzählungskonstanten ermöglichen kurze Fälle.
Ein switch-Ausdruck, der alle drei Konstanten eines Größen-Aufzählungstyps aufführt, kann ohne default-Zweig „S“, „M“ oder „L“ erzeugen.
Symmetrie
Symmetrie bei der Gleichheit verlangt, dass das Vertauschen zweier Objekte in einem Gleichheitsvergleich für beide Richtungen dasselbe Ergebnis liefert.
Symmetrie ist verletzt, wenn book.equals(ebook) wahr, aber ebook.equals(book) für dieselben Objekte falsch ist.

T

Teilimplementierung
Eine Teilimplementierung stellt in einer abstrakten Klasse einen Teil des Interface-Verhaltens bereit und überlässt es konkreten Unterklassen, die erforderlichen Operationen zu vervollständigen.
Eine Teilimplementierung von Storable kann die Pfadvalidierung teilen, während konkrete Klassen den gespeicherten Text bereitstellen.
Textdarstellung
Eine Textdarstellung ist der String, den toString() für ein Objekt zurückgibt und der verwendet wird, wenn das Objekt ausgegeben oder mit anderem Text verknüpft wird.
Die Textdarstellung eines Studenten kann statt des Standardtexts aus Klasse und Hashcode einen Namen und Leistungspunkte zeigen.
Transitivität
Transitivität bei der Gleichheit verlangt: Ist ein Objekt gleich einem zweiten und dieses gleich einem dritten, muss auch das erste gleich dem dritten sein.
Transitivität sorgt dafür, dass drei Bücher mit demselben gleichheitsbestimmenden Code und derselben Auflage bei paarweisen Vergleichen übereinstimmen.

U

Unterklasse
Eine Unterklasse ist eine Klasse, die als Erweiterung einer anderen Klasse deklariert wird und Zustand oder Verhalten ergänzt, während sie als dieser allgemeinere Typ verwendbar bleibt.
Eine Unterklasse GraduateStudent kann auf Student aufbauen und weiterhin überall dort verwendet werden, wo eine Person erwartet wird.
Upcasting
Upcasting behandelt eine Referenz als Oberklassen- oder implementierten Interface-Typ und erweitert ihre Sicht sicher, ohne das Objekt selbst zu ändern.
Ein Upcast eines Telefons zu Chargeable erhält seine Batteriedaten, macht aber nur die über diesen Typ verfügbaren Operationen zugänglich.

Ü

Überladen
Überladen deklariert Methoden mit demselben Namen, aber unterschiedlichen Parameterlisten, sodass der Compiler eine anwendbare Signatur auswählen kann.
Durch Überladen kann print(String text) neben print(int number) ergänzt werden, ohne eine der beiden Methoden zu ersetzen.
Überschreiben
Überschreiben stellt in einer Unterklasse eine Implementierung einer geerbten Instanzmethode mit demselben Namen und passenden Parametertypen bereit.
Das Überschreiben von yearlyPay() ermöglicht einer Führungskraft, einen Bonus hinzuzufügen, während aufrufender Code weiterhin die Mitarbeiteroperation verwendet.

V

Verdecken von Feldern
Verdecken von Feldern tritt auf, wenn eine Unterklasse ein gleichnamiges Feld zu einem geerbten Feld deklariert. Es bleiben separate Felder, die anhand des deklarierten Typs ausgewählt werden.
Durch das Verdecken von Feldern kann p.label das Feld der Oberklasse lesen, während p.show() eine Überschreibung der Unterklasse ausführt, die ein anderes Feld label verwendet.
Verdecken von Methoden
Verdecken von Methoden tritt auf, wenn eine Unterklasse eine statische Methode mit derselben Signatur wie eine von ihrer Oberklasse verfügbare statische Methode deklariert.
Beim Verdecken von Methoden wählt der Typ zur Kompilierzeit eine statische Methode aus, selbst wenn eine Referenz auf ein Unterklassenobjekt verweist.
Vererbung
Vererbung ist eine Beziehung, bei der eine Unterklasse auf einer Oberklasse aufbaut und die über diese Oberklasse verfügbaren Operationen unterstützt.
Vererbung ermöglicht einem Student, Person.getName() wiederzuverwenden und eigene Leistungspunkte sowie Lernverhalten zu ergänzen.

W

Wertgleichheit
Wertgleichheit behandelt Objekte als gleich, wenn ihre ausgewählten Daten übereinstimmen, selbst wenn die Referenzen auf verschiedene Objektinstanzen verweisen.
Wertgleichheit kann zwei separat erzeugte Bücher gleich machen, wenn ihr Katalogcode und ihre Auflage übereinstimmen.

Z

Zugriffsmethode
Eine Zugriffsmethode ist eine Methode, die einen gespeicherten Wert zurückgibt; eine generierte Record-Zugriffsmethode verwendet den exakten Namen ihrer Komponente ohne Getter-Präfix.
Die Zugriffsmethode für eine Record-Komponente count heißt count(), sodass getCount() nicht verfügbar ist, sofern es nicht ausdrücklich deklariert wird.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten