Vererbung, Interfaces und Polymorphie in Java
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
- Baue mit
extendseine neue Klasse auf einer bestehenden auf, erkenne, was die neue Klasse erbt, und stelle mitsupersicher, dass der geerbte Teil jedes Objekts korrekt initialisiert wird.- Erstelle eine Unterklasse mit extendsDeklariere 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.
- Erstelle eine Unterklasse mit extends
- Ändere geerbtes Verhalten durch das Überschreiben von Methoden, baue mit
superauf der Version der Oberklasse auf und ergänze deine Klassen um aussagekräftige MethodentoString,equalsundhashCodeausObject.- Überschreibe geerbte MethodenErsetze oder erweitere eine geerbte Methode durch eine korrekte Überschreibung, rufe die Version der Oberklasse mit
superauf und unterscheide Überschreiben vom Überladen. - Verwende und überschreibe die Methoden von ObjectErkläre, warum jedes Objekt
toString,equals,hashCodeundgetClasshat, lies ihr Standardverhalten ab und schreibe eine nützlichetoString-Methode. - Überschreibe equals und hashCode korrektDefiniere mit einer korrekten Methode
equals(Object), wann zwei Objekte gleich sind, implementierehashCodepassend dazu und behandle Gleichheit über eine Klassenhierarchie hinweg.
- Überschreibe geerbte Methoden
- 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 MethodenDeklariere abstrakte Klassen und Methoden, vervollständige sie in konkreten Unterklassen und verwende eine abstrakte Klasse, um Felder und Code zu teilen.
- Definiere und implementiere InterfacesSchreibe 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 MethodenGib 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 InterfaceVergleiche abstrakte Klassen und Interfaces und wähle für einen bestimmten Entwurf die passende Lösung oder eine Kombination aus beiden.
- Schreibe abstrakte Klassen und abstrakte Methoden
- 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 zuUnterscheide 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 wirdVerfolge 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 instanceofFühre Downcasts mit
instanceofund Mustererkennung sicher aus, erkenne eineClassCastExceptionund verstehe, wann ein Typtest auf eine fehlende überschriebene Methode hinweist. - Entwirf flexiblen Code mit PolymorphieSchreibe Methoden und Collections für allgemeine Typen, sodass neue Klassen ohne Änderungen hineinpassen, und mache Klassen mit
Comparablesortierbar.
- Greife über Oberklassen- und Interface-Typen auf Objekte zu
- 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 darDeklariere Aufzählungstypen, verwende ihre eingebauten Methoden und
switchund gib Aufzählungskonstanten Felder, Konstruktoren und Verhalten. - Modelliere Daten mit RecordsDeklariere Records für einfache Daten, verwende den generierten Konstruktor, die Zugriffsmethoden,
equals,hashCodeundtoStringund validiere Daten in einem kompakten Konstruktor.
- Stelle feste Wertevorräte mit Aufzählungstypen dar
Vererbung in Java einfach erklärt
1Erweitere 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
| Zugriffsmodifikator | Wo auf das Mitglied zugegriffen werden kann |
|---|---|
public | Jede Klasse mit Zugriff auf den deklarierenden Typ |
protected | Dasselbe Paket sowie Zugriff durch Unterklassen über Paketgrenzen hinweg |
| Kein Modifikator | Dasselbe Paket |
private | Die 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
| Deklaration | Wirkung |
|---|---|
| Dieselbe Instanzmethodensignatur in einer Unterklasse | Überschreibt geerbtes Verhalten |
| Derselbe Name, andere Parameterliste | Fügt eine Methodenüberladung hinzu |
| Dieselbe statische Methodensignatur in einer Unterklasse | Verdeckt 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.
@Overridedeckt 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
Objectstellt allen Java-Objekten einschließlich Arrays gemeinsame Methoden bereit.- Ausgabe und String-Konkatenation verwenden
toString()und stellen eine Nullreferenz als „null“ dar. Object.equalsvergleicht 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.
3Definiere 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 Implementierungen | Regel |
|---|---|
| Geerbte öffentliche konkrete Oberklassenmethode und Interface-Default-Methode | Die Klassenmethode gewinnt |
| Default-Methode aus einem Interface und aus dem Interface, das es erweitert | Das spezifischere Interface gewinnt |
| Default-Methoden aus nicht verwandten Interfaces | Die 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
| Frage | Abstrakte Klasse | Interface |
|---|---|---|
| Kann sie beziehungsweise es Felder pro Objekt speichern? | Ja | Nein |
| Kann sie beziehungsweise es Konstruktoren haben? | Ja | Nein |
| Kann sie beziehungsweise es funktionsfähige Instanzmethoden bereitstellen? | Ja | Ja, mit Default-Methoden |
| Wie viele kann eine Klasse direkt verwenden? | Eine Oberklasse | Mehrere 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.
4Schreibe 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 |
Person | Zugängliche Operationen von Person, einschließlich geerbter Operationen |
Student | Zugä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 alsList<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 Aufruf | Was die Auswahl bestimmt |
|---|---|
| Überschriebene Instanzmethode | Tatsächliche Klasse des Empfängerobjekts nach Auswahl der Signatur |
| Statische Methode | Für den Aufruf verwendeter Typ zur Kompilierzeit |
| Verdecktes Feld | Deklarierter Typ der Referenz |
| Private Methode | Code 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
ClassCastExceptionwerfen. instanceofist 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.
compareTodrückt die Ordnung durch ein negatives Ergebnis, null oder ein positives Ergebnis aus.Integer.comparevermeidet 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.
5Modelliere 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 undvalueOfverlangt 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
newkeine 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
1Entwickle Schritt für Schritt ein Gerät mit Vererbung
12 Schritte
- 1Einfü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.
- 2BeispielBeginne 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; } }Devicekontrolliert seine private Beschriftung. Anderer Code liest sie überlabel(). 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. - 3BeispielErgä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; } }Sensorerweitert eine Klasse und ergänzt ein Feld. Der Aufrufsuper(label)überträgt die Verantwortung für die Beschriftung anDevice; der Sensorkonstruktor speichert die Einheit selbst. - 4KernaussageLies beide Teile über den Sensor
Nach
Sensor s = new Sensor("Room", "C");gibts.label()„Room“ unds.unit()„C“ zurück. Die erste Methode wird geerbt; die zweite gehört zuSensor.Beide Aufrufe betreffen dasselbe Objekt. Das Erweitern von
Devicehat kein zweites Geräteobjekt neben dem Sensor erzeugt. Die private Beschriftung ist vorhanden, obwohl Sensorcode über eine Methode auf sie zugreift. - 5HerausforderungWähle die Beziehung für ein Labor
Ein
Labenthält mehrere Sensoren. SollteLabdie KlasseSensorerweitern 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
Sensorwü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. - 6ErklärungRufe den Oberklassenkonstruktor auf
Der Aufruf
new Sensor("Room", "C")wählt den Sensorkonstruktor mit zwei Argumenten. Seinsuper(label)ruftDevice(String)für dasselbe Objekt auf.Deviceerreicht zuerst seine eigene OberklasseObjectund weist dann die Beschriftung zu. WennDevicezurückkehrt, werden die Sensorfeldinitialisierung und der restliche Sensorkonstruktorrumpf ausgeführt. Schließlich steht die neue Referenz dem aufrufenden Code zur Verfügung. - 7TippFinde den fehlenden parameterlosen Konstruktor
Entfernst du
super(label)aus dem Sensorkonstruktor, fügt Javasuper()ein. Für diesen Aufruf gibt es keinen passenden Konstruktor:Devicedeklariert nurDevice(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 implizitensuper()bleibt bestehen. Übergib die Beschriftung stattdessen an den Oberklassenkonstruktor. - 8BeispielDelegiere für eine optionale Standardeinheit
Füge diesen Konstruktor innerhalb von
Sensorhinzu:Sensor(String label) { this(label, "unknown"); }new Sensor("Hall")delegiert an den vorhandenen Konstruktor mit zwei Argumenten, dersuper(label)aufruft. Das entstehende Objekt hat die Beschriftung „Hall“ und die Einheit „unknown“. Schreibe nicht zusätzlichsuper(...)in den delegierenden Konstruktor: Ein Konstruktor hat höchstens einen expliziten Konstruktoraufruf. - 9VerbindungValidiere 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.
- 10HerausforderungSage eine Konstruktormeldung voraus
In einer separaten Version gibt
Devicenach seinem eigenen Oberklassenaufruf „device“ aus.Sensorgibt nachsuper(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.
- 11HerausforderungKorrigiere eine Zuweisung an ein geerbtes Feld
Eine neue Unterklasse versucht, die private Gerätebeschriftung mit
this.label = label;zu initialisieren, und lässtsuper(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. Verwendesuper(label), damitDeviceseinem eigenen privaten Zustand Werte zuweist, und weise danach nur Feldern Werte zu, die in der Unterklasse deklariert sind. - 12ZusammenfassungLokalisiere 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.
2Gib Katalogeinträge korrekt aus und vergleiche sie
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungLege die Bedeutung eines Katalogeintrags fest
- 2BeispielErweitere eine grundlegende Beschreibung
- 3TippVermeide den Aufruf der Überschreibung aus sich selbst
- 4HerausforderungPrüfe einen scheinbaren Ersatz
- 5BeispielGib der Ausgabe eine exakt passende Methode
- 6KernaussageLies die tatsächliche Klasse, ohne sie zu ändern
- 7HerausforderungUntersuche einen standardmäßig aussehenden Ausgabetext
- 8BeispielDeklariere die gleichheitsrelevanten Daten
- 9BeispielLehne das falsche Objekt vor dem Feldvergleich ab
- 10BeispielBerechne den Hashcode aus denselben gleichheitsrelevanten Feldern
- 11HerausforderungErkenne die Überladung, die eine Collection nicht verwendet
- 12KernaussageSichere Vergleiche in beiden Richtungen
- 13HerausforderungUnterscheide gleichen Text von gleichen Werten
- 14ZusammenfassungPrüfe eine Werteklasse an ihrer öffentlichen Schnittstelle
3Erstelle einen Vertrag und teile seine Implementierung
18 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungTrenne gemeinsame Daten von der gemeinsamen Fähigkeit
- 2BeispielBeginne mit einer abstrakten Nachrichtenquelle
- 3BeispielVervollständige die fehlende Operation
- 4HerausforderungEntscheide, ob eine Unterklasse konkret sein kann
- 5BeispielBenenne das Lesen als Fähigkeit
- 6VerbindungLass die bestehende Basis das Interface erfüllen
- 7BeispielSchreibe aufrufenden Code für den Vertrag
- 8HerausforderungKorrigiere eine Implementierung mit eingeschränktem Zugriff
- 9BeispielErgänze eine nützliche Default-Operation
- 10ErklärungBelasse statische und private Hilfsmethoden in ihren Rollen
- 11BeispielErzeuge einen echten Konflikt zwischen Default-Methoden
- 12BeispielWähle eine Default-Methode ausdrücklich
- 13HerausforderungGib der Klassenimplementierung ihren Vorrang
- 14KernaussageBiete eine Basis an, ohne sie für jede Implementierung vorzuschreiben
- 15HerausforderungOrdne Beschriftung und Konstruktor dem richtigen Typ zu
- 16TippPrüfe neue Default-Methoden auf Konflikte mit geerbten Methoden
- 17HerausforderungFüge einen neuen Leser hinzu, ohne seine bestehende Oberklasse zu ändern
- 18ZusammenfassungBeurteile einen vorgeschlagenen gemeinsamen Typ
4Verfolge einen Berichtsaufruf über allgemeine Typen
17 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungVerfolge, was die Variable zusichert und was das Objekt tut
- 2BeispielGib einem spezialisierten Bericht eine allgemeine Referenz
- 3ErklärungPrüfe, was die Referenz verfügbar macht
- 4HerausforderungWähle Typen für eine gemischte Berichtsliste
- 5BeispielLöse zuerst den äußeren Aufruf auf
- 6BeispielLöse den inneren Aufruf auf demselben Empfängerobjekt auf
- 7BeispielWähle eine Überladung anhand des deklarierten Arguments
- 8HerausforderungTrenne zwei Entscheidungen in einem Aufruf
- 9TippKennzeichne Felder und statische Aufrufe im Ablauf getrennt
- 10BeispielRufe die Verkaufsoperation sicher auf
- 11HerausforderungSage eine Typumwandlung bei geändertem Objekt voraus
- 12BeispielSammle Überschriften über die gemeinsame Operation
- 13VerbindungVerwende den Vergleich als weitere gemeinsame Operation
- 14HerausforderungSortiere Werte durch die Wahl einer Implementierung
- 15KernaussageVerwende Untertyptests für untertypspezifische Aufgaben
- 16HerausforderungUnterstütze eine Prüfüberschrift ohne neuen Zweig
- 17ZusammenfassungVerfolge einen unbekannten Aufruf der Reihe nach
5Erstelle eine feste Auswahl und einen geschützten Datenwert
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungWähle zwischen benannten Optionen und gruppierten Daten
- 2BeispielGib den Serviceoptionen einen eigenen Typ
- 3BeispielVerknüpfe die Optionen mit nützlichen Zahlen
- 4TippHalte die Servicedaten unabhängig von der Deklarationsreihenfolge
- 5HerausforderungSage das Ergebnis einer Suche und eines Feldzugriffs voraus
- 6VerbindungVerbinde Verhalten mit der festen Auswahl
- 7BeispielDeklariere die Komponenten eines Lieferauftrags
- 8BeispielNormalisiere eingehende Daten vor der Zuweisung
- 9HerausforderungVergleiche die normalisierten Aufträge
- 10BeispielFinde das veränderliche Objekt in einer finalen Komponente
- 11BeispielSpeichere eine nicht modifizierbare Kopie
- 12TippGib einen neuen Record zurück, wenn Werte geändert werden müssen
- 13HerausforderungWähle die passende Korrektur für eine veränderliche Route
- 14HerausforderungModelliere neue Serviceoptionen und Kundenaufträge
- 15ZusammenfassungPrüfe generiertes Verhalten, bevor du Code hinzufügst
Vererbung in Java: Übungsaufgaben
Set 1: Erstelle Klassen und verfolge geerbtes Verhalten10 Übungsfragen
Frage 1 von 10
Library speichert mehrere Shelf-Objekte, und jedes Regal speichert Bücher. Wie sollte Library ihre Beziehung zu Shelf darstellen?Antwort zeigenAntwort ausblenden
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?Antwort zeigenAntwort ausblenden
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() { }
}
Antwort zeigenAntwort ausblenden
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); }
}
Antwort zeigenAntwort ausblenden
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?Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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
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?Antwort zeigenAntwort ausblenden
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?Antwort zeigenAntwort ausblenden
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() { }
}
Antwort zeigenAntwort ausblenden
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; }
}
Antwort zeigenAntwort ausblenden
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
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?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 { }
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 { }
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?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();?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();?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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
area() auf. Sie muss sowohl ArrayList<Shape> als auch LinkedList<Shape> akzeptieren. Welcher Parametertyp stellt die benötigten Listenoperationen bereit?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"; }
}
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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?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
Shape s = null; folgt s instanceof Circle. Es gilt Circle extends Shape. Was ist das Ergebnis der Prüfung?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;
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";
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();
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
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?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();
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; }
}
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));
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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?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
enum Status { NEW, SENT, DONE }. Was bewirkt Status.valueOf("sent")?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
record Reading(int count, String unit) { } den Zählwert aus einer Variablen r?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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
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?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))?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));
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
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?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());
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));
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);
}
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
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 { }
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
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?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);
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
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?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";
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());
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 { }
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Abstrakte Klasse
Shape kann einen Namen speichern und zugleich von konkreten Kreisen und Rechtecken verlangen, eine Fläche bereitzustellen.Abstrakte Methode
area() verhindert, dass eine unvollendete konkrete Form unbemerkt einen Platzhalter mit der Fläche null verwendet.Aufruf des Oberklassenkonstruktors
super(...), um Argumente für das aktuelle Objekt an einen Konstruktor der direkten Oberklasse zu übergeben.Person, wo das private Namensfeld deklariert ist.Aufzählungskonstante
CupSize.LARGE kann ein Fassungsvermögen von 500 mL speichern und auf Referenzgleichheit verglichen werden.Aufzählungstyp
NEW, SENT und DONE beschränken.C
ClassCastException
Animal-Objekt in seine spezifischere Unterklasse Fox umgewandelt wird.Comparable
compareTo mit einer anderen vergleicht.Comparable<Runner>, damit eine Sortiermethode Läufer nach ihrer Zielzeit ordnen kann.D
Default-Methode
needsCharge() kann batteryPercent() verwenden, ohne die Struktur der privaten Felder eines Geräts zu kennen.Deklarierter Typ
Person p = new Student(...) ist der deklarierte Typ Person, sodass Methoden, die nur Studenten besitzen, nicht direkt verfügbar sind.Downcasting
Dynamische Bindung
hello(), das who() aufruft, die Methode B.who() zu erreichen, wenn das Empfängerobjekt zur Klasse B gehört.Dynamischer Typ
Person-Referenz kann auf einen Studenten verweisen, dessen dynamischer Typ nach dem Upcast Student bleibt.E
Einfachvererbung
Student als auch Teacher als direkte Oberklassen erweitert.Empfängerobjekt
shape.area() ist das Empfängerobjekt das Formobjekt, selbst wenn die Variable einen abstrakten deklarierten Typ hat.F
Fähigkeit
Feldinitialisierer
final-Methode
Flache Unveränderlichkeit
Flexibler Konstruktorrumpf
G
Ganzzahlüberlauf
Integer.compare vermeidet diesen Vergleichsfehler.Gemeinsamer Zustand
Geschützter Zugriff
Gleichheitsvertrag
Gleichstand in der Ordnung
compareTo null zurückgibt und zwei Objekte nach der gewählten Ordnungsregel denselben Vergleichsrang einnehmen.H
Hashcode
Hashkollision
I
Identität
a == b falsch ergibt.Implementierung
Entity geerbte öffentliche Implementierung kann Named.name() für eine Unterklasse Asset erfüllen.instanceof
instanceof-Test ermöglicht zu prüfen, ob eine Person den Typ Student hat, bevor eine studentenspezifische Methode verwendet wird.Interface
Chargeable ermöglicht einem Telefon und einem Roller dieselben Aufrufe, obwohl sie unterschiedlich schnell laden.Interface-Konstante
Chargeable.FULL kann 100 enthalten, während jedes Gerät sein eigenes veränderliches Batteriefeld speichert.Interface-Typ
List als Interface-Typ ermöglicht einer Methode, sowohl Array-basierte als auch verkettete Listen zu akzeptieren.K
Kanonischer Konstruktor
Point(int x, int y) nimmt zuerst die x-Koordinate und danach die y-Koordinate entgegen.Kompakter Konstruktor
Komposition
Customer, eine Address zu enthalten, ohne jeden Kunden als Adresse verwendbar zu machen.Konflikt zwischen Default-Methoden
SmartLamp die übereinstimmenden Beschreibungen von Lamp und Speaker überschreibt.Konkrete Klasse
Rectangle ist eine konkrete Klasse, sobald sie die von ihrer abstrakten Formbasis verlangte Flächenoperation vervollständigt.Konkrete Methode
Shape.describe() kann den Namen wiederverwenden und die Implementierung von area() der jeweiligen Unterklasse aufrufen.Konstantenspezifischer Klassenrumpf
MULTIPLY, eine Operation anders zu implementieren als die Konstante ADD des Aufzählungstyps.Konstruktorkette
Kovarianter Rückgabetyp
String zurückzugeben, wenn die Oberklassenmethode Object zurückgibt.L
Laufzeitklasse
getClass() unabhängig vom deklarierten Typ seiner Referenz meldet.Student, selbst wenn eine Methode ihn über einen Person-Parameter erhält.M
Methodensignatur
Object zu String ändert die Methodensignatur und erzeugt eine Überladung.Mustervariable
student in person instanceof Student student macht die Kennungsmethode des passenden Objekts sicher zugänglich.N
Natürliche Ordnung
Nicht modifizierbare Kopie
List.copyOf schützt die Mitgliederliste eines Teams vor Ergänzungen der veränderlichen Liste des aufrufenden Codes.O
Oberinterface
Chargeable ist ein Oberinterface von SmartDevice, wenn Letzteres es zusammen mit Connectable erweitert.Oberklasse
Person ist die Oberklasse von Student, sodass eine Methode, die eine Person akzeptiert, auch einen Studenten erhalten kann.Object
Object-Variable kann einen Studenten oder ein Array enthalten, und beide unterstützen die Methoden, die jedes Objekt hat.Ordinalwert
LOW, HIGH deklarierten Aufzählungstyp ändert sich der Ordinalwert von HIGH von 1 auf 2, wenn MID davor eingefügt wird.Override-Annotation
@Override, weist den Compiler an zu prüfen, ob eine Methode eine geerbte Methode überschreibt oder implementiert.tostring(), wenn eigentlich toString() ersetzt werden sollte.P
Polymorphie
area() auf Kreisen, Rechtecken und Dreiecken ohne separate Typzweige aufzurufen.Private Interface-Methode
R
Record
Point(int x, int y) gruppiert zwei Koordinaten und stellt komponentenbasierte Gleichheit ohne handgeschriebene Methoden bereit.Record-Komponente
seconds kann bei einem Läufer mit der generierten Methode runner.seconds() gelesen werden.S
Spezifischeres Interface
Detailed liefert das gewählte name(), wenn es Basic erweitert und dessen Default-Methode ersetzt.Standardkonstruktor
super().Person(String name) entfernt den Standardkonstruktor, auf den eine Unterklasse möglicherweise angewiesen war.Statische Interface-Methode
Chargeable.clamp(130) kann 100 zurückgeben, ohne ein Geräteobjekt zu erzeugen.switch-Ausdruck
Symmetrie
book.equals(ebook) wahr, aber ebook.equals(book) für dieselben Objekte falsch ist.T
Teilimplementierung
Storable kann die Pfadvalidierung teilen, während konkrete Klassen den gespeicherten Text bereitstellen.Textdarstellung
toString() für ein Objekt zurückgibt und der verwendet wird, wenn das Objekt ausgegeben oder mit anderem Text verknüpft wird.Transitivität
U
Unterklasse
GraduateStudent kann auf Student aufbauen und weiterhin überall dort verwendet werden, wo eine Person erwartet wird.Upcasting
Chargeable erhält seine Batteriedaten, macht aber nur die über diesen Typ verfügbaren Operationen zugänglich.Ü
Überladen
print(String text) neben print(int number) ergänzt werden, ohne eine der beiden Methoden zu ersetzen.Überschreiben
yearlyPay() ermöglicht einer Führungskraft, einen Bonus hinzuzufügen, während aufrufender Code weiterhin die Mitarbeiteroperation verwendet.V
Verdecken von Feldern
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
Vererbung
Student, Person.getName() wiederzuverwenden und eigene Leistungspunkte sowie Lernverhalten zu ergänzen.W
Wertgleichheit
Z
Zugriffsmethode
count heißt count(), sodass getCount() nicht verfügbar ist, sofern es nicht ausdrücklich deklariert wird.- 1Java-Programme kompilieren und starten
- 2Bedingungen und Schleifen in Java
- 3Java-Methoden und Problemzerlegung
- 4Listen, Arrays und Strings in Java
- 5Java-Klassen und Objekte
- 6Objektreferenzen in Java
- 7HashMaps und Collections in Java
- 8Vererbung in Java
- 9Ausnahmen und Dateien in Java
- 10Modultests in Java
- 11Lambda-Ausdrücke und Streams in Java
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.