michael.eichberg@dhbw.de, Raum 149B
1.0.1
Implementierung einer Raumverwaltung
Ein Vorlesungsraum an der DHBW
public class Vorlesungsraum {
private int zustand; // 0: nutzbar,
// 1: zu renovieren
private String gebaeude;
private int ebene;
private int raumNummer;
private int kapazitaet;
private boolean klimatisiert;
private Ausstattung[] ausstattung;
public void setzeZustand(int zustand)...
public void setzeEbene(int ebene)...
public void setzeRaum(int raumNr)...
public void generiereBeschreibung()...
public void reserviere()...
}
Eine Teeküche an der DHBW
public class Teekueche {
private int zustand; // 0: nutzbar,
// 1: zu renovieren
private String gebaeude;
private int ebene;
private int raumNummer;
private Kuechgeraete[] geraete;
public void setzeZustand(int zustand)...
public void setzeEbene(int ebene)...
public void setzeRaum(int raumNr)...
public void generiereBeschreibung()...
public void setzeSchliessberechtigung()...
}
Identifikation der Gemeinsamkeiten und Modellierung einer allgemeinen Klasse
Klassen können durch eine Vererbungshierachie in Oberklassen (Superklassen) (hier: Raum
) und Unterklassen (Subklassen) (hier: Vorlesungsraum
, Buero
, Teekueche
, ...) eingeteilt werden.
Unterklassen spezialisieren eine Oberklasse: Die Oberklasse definiert gemeinsame Attribute und Methoden. Eine Unterklasse kann neue Attribute und Methoden hinzufügen bzw. überschreiben. Dabei ist darauf zu achten, dass die Unterklasse sich verhaltenskonform zur Oberklasse verhält.
Erlaubt es, eine Klasse von einer anderen abzuleiten und deren Eigenschaften und Methoden zu erben.
Wiederverwendbarkeit des Codes
Hierarchische Strukturierung
Klassen werden in Vererbungshierachien eingeteilt.
public class <Subklassenname>
extends <Superklassenname> { ...
}
class Auto { // Basisklasse
String marke;
void fahren() { System.out.println("Das Auto fährt."); }
}
class Elektroauto extends Auto { // Abgeleitete Klasse
int batteriestand;
void aufladen() {
System.out.println("Das Elektroauto wird aufgeladen.");
} }
Eine Unter- bzw. Subklasse erbt alle Attribute und Methoden der Super- bzw. Oberklasse.
Auf public
und protected
Attribute und Methoden der Superklassen kann direkt zugegriffen werden.
Auf private
Attribute und Methoden kann nicht zugegriffen werden (zBei Attributen ggf. nur über entsprechende get
- und set
-Methoden)
Zyklen in der Vererbungshierarchie sind nicht erlaubt
Zugriff auf Methoden und Attribute von Superklassen
Mittels super
ist der direkte Zugriff auf die Attribute und Methoden der Superklasse (wenn diese protected
oder public
sind) möglich.
Dies ist notwendig, wenn die Vaterklasse Attribute bzw. Methoden mit gleichem Namen enthält (ansonsten kann man super
auch weglassen).
Verwendung von super
für Aufruf der Methode der Superklasse
1class Person {
2String name;
3int age;
4void display() {
5System.out.print("Name: " + name + " Age: " + age);
6}
7}
89
class Kind extends Person {
10String hobby;
11void display() {
12super.display();
13System.out.print(" Hobby: " + hobby);
14}
15}
Initialisierung von Superklassen
Wird ein Objekt erzeugt (mittels new
), so wird automatisch auch Speicher für die Attribute der Superklasse reserviert und initialisiert.
Mittels eines super(...)
Aufrufs ist es möglich einen bestimmten Konstruktor der Superklasse (innerhalb des Konstruktors der Subklasse) aufzurufen.
Ruft der Konstruktor nicht explizit einen Konstruktor mit super(...)
auf, dann wird der parameterlose Konstruktor super()
implizit aufgerufen, wenn keiner explizit definiert wurde.
Die Initialisierung startet immer bei der Superklasse und arbeitet sich dann rekursiv durch die Vererbungshierarchie nach unten.
Verwendung von super
während der Initialisierung
1class Angestellter {
2private String name;
3Angestellter(String name) { this.name = name; }
4String getName() { return name; }
5}
6class Professor extends Angestellter {
7private String fachgebiet;
8Professor(String name, String fachgebiet) {
9super(name); // Aufruf des Konstruktors der Superklasse
10this.fachgebiet = fachgebiet;
11}
12public String toString() {
13return "Professor { name = " + super.getName() // super hier optional
14+ ", fachgebiet = " + fachgebiet + " }";
15} }
java.lang.Object
Jede Klasse in Java erbt von der Klasse java.lang.Object
.
Die Klasse java.lang.Object
definiert allgemein relevante Methoden wie toString()
, equals()
und hashCode()
.
Die Methode toString()
gibt eine String-Repräsentation des Objekts zurück und wird aufgerufen, wenn ein Objekt in einem String-Kontext verwendet wird.
1void main() {
2println("Mein Prof.: " + new Professor("Max Mustermann", "Informatik"));
3}
Die Methode getClass()
erlaubt den Zugriff auf die Klasse eines Objekts und ermöglicht Reflection
. Thema für spätere Vorlesung(en).
Methoden überschreiben
Eine Methode in einer Subklasse kann eine Methode in der Superklasse überschreiben.
Eine Methode, die eine Methode in der Superklasse überschreibt hat den Kontrakt der Superklasse immer einzuhalten!
D. h. Vorbedingungen können in der Subklassen entspannt und Nachbedingungen verschärft werden, aber nie umgekehrt.
Einfach- vs. Mehrfachvererbung
Jede Klasse kann nur eine Superklasse in der Vererbungshierachie besitzen
Jede Klasse kann mehrere Superklassen in der Vererbungshierachie besitzen
Java unterstützt nur Einfachvererbung bei Klassen (und Mehrfachvererbung bei Schnittstellen).
Im Folgenden gehen wir von der folgenden Vererbungshierarchie aus:
Alle Attribute und Klassen sein public
.
Statischer und Dynamischer Typ
Eine Referenzvariable (für ein Objekt) hat einen statischen und einen dynamischen Typ.
Der statische Typ ist durch die Deklaration der Referenzvariablen gegeben.
Beispiel: Konto k; //Statischer Typ "Konto"
Der dynamische Typ hängt vom konkreten Objekt ab; es ist der Typ der Klasse, von der das Objekt instanziiert wurde mittels new
.
Beispiel: k = new Sparkonto(...); // Dynamischer Typ "Sparkonto“
Der dynamische Typ muss von einer (nicht echten) Unterklasse des statischen Typs sein (z. B. „Sparkonto“ als dynamischer und „Konto“ als statischer Typ.)
Über die Referenzvariable sind nur die sichtbaren Attribute und Methoden des statischen Typs ansprechbar.
Im Fall von
Konto k = new Sparkonto(...);
kann nicht aufsparzins
zugegriffen werden.
Implizite Typkonvertierung
Eine implizite Typkonvertierung (ohne cast-Operator) ist in der Vererbungshierarchie aufwärts möglich (Upcast).
Beispiel: Ein Tagesgeldkonto kann immer in ein Sparkonto konvertiert werden. Nach der Konvertierung sind über die Referenzvariable nur noch Attribute und Methoden des statischen Typs Sparkonto „sichtbar“.
Das Objekt selbst wird bei einer impliziten Konvertierung nicht geändert, nur die sichtbaren Attribute und Methoden unterscheiden sich.
Die implizite Typkonvertierung ist sicher; es kann kein Fehler bei der Typkonvertierung entstehen.
Explizite Typkonvertierung
Typkonvertierung in der Vererbungshierarchie abwärts (Downcast) ist nur durch explizite Typkonvertierung (mit cast-Operator) möglich
Beispiel - ein Konto kann „möglicherweise“ in ein Sparkonto konvertiert werden:
Sparkonto sk = (Sparkonto) konto
;
Nach der Konvertierung sind über die Referenzvariable die Attribute und Methoden des statischen Typs Sparkonto „sichtbar“.
Das Objekt selbst wird bei einer expliziten Konvertierung nicht verändert!
Die Typkonvertierung ist nicht sicher; es kann ein Fehler bei der Typkonvertierung entstehen. Eine sogenannte Typecast Exception ist dann die Folge.
Typkonvertierung - Details
Eine explizite Konvertierung eines Objektes ist nur dann möglich wenn der dynamische Typ des Objektes gleich der Ziel-Klasse ist bzw. der dynamische Typ des Objektes eine Subklasse der Ziel-Klasse ist.
Beispiele:
Ein Objekt wird als Festgeldkonto angelegt und implizit in ein Konto konvertiert (d. h. der dynamische Typ ist Festgeldkonto). Eine explizite Konvertierung in ein Sparkonto ist möglich.
Wird allerdings ein Objekt als Sparkonto angelegt, dann kann es nicht explizit in ein Tagesgeldkonto konvertiert werden.
Typtest mit instanceof
Der instanceof
-Operator testet ob ein Objekt kompatibel zu einer Klasse ist (d. h. ob das Objekt in die Klasse konvertierbar ist). Der Operator gibt true
oder false
zurück:
<Objekt> instanceof <Klasse>
k instanceof Sparkonto
testet ob das Objekt k
in ein Sparkonto explizit konvertiert werden kann. Hier nur möglich, wenn k
den dynamischen Typ Sparkonto, Festgeldkonto oder Tagesgeldkonto hat.
Sollte k
null sein, dann ist das Ergebnis immer false
.
Beispiele
Konto k1 = new Festgeld (1, "Matt", 100, 2.5, 36);
// Test der Typkompatibilität mit instanceof Festgeld
if(k1 instanceof Festgeld){
// Explizite Konvertierung ist jetzt sicher:
Festgeld k2 = (Festgeld)k1;
System.out.println(k2);
}
Bzgl. des Zugriffs auf Methoden mit Default Sichtbarkeit gelten die Standardregeln.
Neben der klassischen Einfach- und Mehrfachvererbung gibt es noch viele weitere Konstrukte (z. B. traits, mixins, ...), die in anderen Programmiersprachen verwendet werden und ähnliche Konzepte ermöglichen.
Eine Referenzvariable mit einem statischen Typ kann auf Objekte mit unterschiedlichem dynamischen Typ verweisen.
Überschreiben von Methoden (Runtime Polymorphism`)
Parameter und Rückgabewerte: Methoden können als Parameter Objekte einer beliebigen Subklasse übergeben bekommen bzw. zurückgeben.
ein Array kann Objekte jeder Subklasse enthalten (z. B. ein Array mit dem Datentyp Konto[]
kann alle Subklassen enthalten.)
Festgeld k1 = new Festgeld(1, "Matt", 100, 2.5, 36);
Sparkonto k2 = new Sparkonto(1, "Michael", 100, 3);
// Objekte mit unterschiedlichem dynamischen Typ in einem Array
Konto[] konten = {k1, k2};
for(int i=0; i<konten.length; ++i){
println(konten[i]);
}
Beispiel: Methode fahren wird in verschiedenen Klassen unterschiedlich implementiert.
class Auto {
void fahren() {
System.out.println("Das Auto fährt.");
}
}
class Elektroauto extends Auto {
void fahren() { // Überschreiben der Methode
System.out.println("Das Elektroauto fährt leise.");
}
}
Wir sprechen hier vom überschreiben (overriding) von Methoden.
Methoden überschreiben:
Deklaration einer Methode mit der gleichen Schnittstelle (Name, Rückgabetyp, Parameter) aber ggf. mit neuem Methodenrumpf.
Eine Methode kann in einer Subklasse eine erhöhte Sichtbarkeit haben, aber keine verringerte!
Methoden die final
sind können in Subklassen nicht überschrieben werden.
Methoden die private
sind, sind in Subklassen nicht sichtbar und können daher nicht überschrieben werden.
Wenn die Subklasse eine Methode mit dem gleichen Namen und den gleichen Parametern definiert, dann handelt es sich um eine neue Methode und nicht um eine Überschreibung. Ob diese neue Methode auch (wieder) private
ist, ist nicht weiter von belang!
overriding und Overloading sind zwei verschiedene Konzepte. Bei Overloading wird eine Methode mit dem gleichen Namen aber unterschiedlichen Parametertypen definiert. Bei Overriding wird eine Methode mit dem gleichen Namen und den gleichen Parametertypen in einer Subklasse neu definiert.
Schützt die Daten und kontrolliert den Zugriff.
Vereinfacht die Komplexität des Codes.
Ermöglicht Code-Wiederverwendung und Hierarchien.
Erlaubt flexiblen Code durch unterschiedliche Implementierungen.
Meine Erste Klassenhierarchie
Erstelle eine einfache Tier
-Klasse mit einem Attribut decibel
vom Typ float
und einer Methode lautGeben()
, die den Laut des Tieres auf der Konsole ausgibt und einer Methode decibel
, die die Lautstärke als String
zurückgibt. Erstelle dann die Klassen Hund
und Katze
, die Tier
erweitern bzw. von Tier
erben. Überschreibe die Methode lautGeben() mit unterschiedlichen Ausgaben.
Die Fehlerbehandlung in Java erfolgt mittels Exceptions.
Exceptions sind Objekte, die eine Fehlermeldung und den Stacktrace enthalten und erben direkt oder indirekt von Throwable
.
Exceptions können geworfen (mit throw
) und gefangen (mit try
und catch
) werden.
Exceptions können checked oder unchecked sein:
Checked Exceptions (Klassen, die von Throwable
erben aber nicht von RuntimeException
oder Error
) müssen gefangen oder deklariert werden.
Unchecked Exceptions (Exceptions, die von java.lang.RuntimeException
erben) können im Code ignoriert werden; d. h. müssen nicht explizit beachtet werden. Sollten/müssen aber nicht.
Exceptions Typhierearchie
Einige ausgewählte typische Exceptions
Unchecked Exceptions:
ArithmeticException
: Division durch 0.
NullPointerException
: Ein Objekt wird verwendet, obwohl es null
ist.
ArrayIndexOutOfBoundsException
: Ein ungültiger Index wird verwendet.
IllegalArgumentException
: Ein ungültiges Argument wird übergeben.
Checked Exceptions:
IOException
: Fehler beim Lesen oder Schreiben von Dateien.
FileNotFoundException
: Datei nicht gefunden.
ParseException
: Fehler beim Parsen von Strings.
Handling von Unchecked Exceptions (try ... catch (E e)
)
1import static java.lang.System.err;
2void main(String[] args) {
3try {
4int i = Integer.parseInt(args[0]);
5int j = Integer.parseInt(args[1]);
6try {
7println(i / j);
8} catch (ArithmeticException e) {
9err.println("Division durch 0 nicht erlaubt.");
10}
11} catch (NumberFormatException e) {
12err.println("Fehler beim Parsen der Argumente.");
13} catch (ArrayIndexOutOfBoundsException e) {
14err.println("Zu wenige Argumente.");
15} }
Handling von Checked Exceptions (try ... catch (E e)
)
1import java.text.DateFormat;
2import static java.lang.System.err;
34
void main(String[] args) {
5DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
6try {
7println("Thats the day: " + df.parse(args[0]));
8} catch (Exception e) {
9err.println("Error: " + e.getMessage()+
10" Expected format: "+df.format(new Date()));
11}
12}
Identische Behandlung von mehreren Exceptions (... catch (A | B e
))
1import static java.lang.System.err;
2void main(String[] args) {
3try {
4int i = Integer.parseInt(args[0]);
5int j = Integer.parseInt(args[1]);
6try {
7println(i / j);
8} catch (ArithmeticException e) {
9err.println("Division durch 0 nicht erlaubt.");
10}
11} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
12err.println("Argumente falsch.");
13} }
Es wäre auch möglich gewesen die gemeinsame Superklasse zu nehmen (RuntimeException
). Dies würde jedoch dazu führen, dass man Ausnahmen fängt, die man gar nicht fangen will!
Deklaration, dass eine Checked Exceptions geworfen werden könnte (throws
)
1import java.text.DateFormat;
2import java.text.ParseException;
34
void main(String[] args) throws ParseException {
5DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
6println("Thats the day: " + df.parse(args[0]));
7}
Try-with-Resources (try(var i = <Ressource>) { ... }
)
Stellt sicher, dass eine Ressource (z. B. eine Datei) immer geschlossen wird, auch wenn eine Exception auftritt.
1import static java.lang.System.err;
23
void main(String []args){
4try (var in = new BufferedReader(new FileReader(args[0]))) {
5String line;
6while ((line = in.readLine()) != null) { println(line); }
7} catch (IOException e) {
8err.println("Error: " + e.getMessage());
9}
10}
Der explizite Exceptionhandler wird nach dem Schließen der Ressource aufgerufen.
Exceptions können selbstverständlich auch selbst definiert werden. Im Allgemeinen empfiehlt es sich aber, die Standard-Exceptions zu verwenden, da diese von anderen Entwicklern erkannt und verstanden werden.
Errors
sind Exceptions, die nicht gefangen werden sollten. Sie sind für den Programmierer nicht vorhersehbar und können im ganz Allgemeinen nicht sinnvoll behandelt werden. Sie signalisieren zum Beispiel Fehlerzustände der virtuellen Maschine. Ein Beispiel ist der OutOfMemoryError
.
Einfache Fehlerbehandlung
Erweiteren Sie Ihre Methoden zum Berechnen der Kubikwurzel und zur Berechnung der Fibonacci-Zahlen um Fehlerbehandlung. D. h. testen Sie die Parameter auf Gültigkeit und werfen Sie ggf. eine IllegalArgumentException
.
Deklarieren Sie in der Methodensignatur, dass eine IllegalArgumentException
geworfen werden könnte.
Bedenken Sie bei der Berechnung der Methode für die Kubikwurzel, dass Double Werte auch Spezialwerte wie Double.POSITIVE_INFINITY
und Double.NaN
haben können!
Ändern Sie Ihre main
Methode so, dass sie die Exceptions fängt und eine entsprechende Fehlermeldung ausgibt und dann sauber das Program beendet.
Nicht-leere Zeilen zählen
Schreiben Sie eine Methode (countNonEmptyLines
), die die Anzahl der nicht-leeren Zeilen in einem Datenstrom zählt und zurückgibt. Eine Zeile wird als leer angesehen, wenn diese keine Zeichen oder nur Leerzeichen enthält. Verwenden Sie dazu die Klasse BufferedReader
und die Methode readLine()
(siehe Beispiel in den Folien). Die Methode soll sich nicht um Fehlerbehandlung kümmern.
Schreiben Sie eine main
Methode, die die Methode verwendet und sich um jegliche Fehlerbehandlung kümmert. D. h. die main Methode soll bei allen Fehlern eine passende Fehlermeldung ausgeben und das Programm sauber beenden. Verwenden Sie ggf. ein try-with-ressource
Statement.
Hinweis
Studieren Sie die Dokumentation der Klasse String
in Hinblick auf Methoden, die es Ihnen einfacher machen zu erkennen ob eine Zeile gemäß obiger Definition leer ist.
Abstrakte Klassen deklarieren ein Grundgerüst einer Klasse von der keine Objekte erzeugt werden können.
Abstrakte Klassen können abstrakte Methoden enthalten, die nur die Schnittstelle einer Methode definieren, aber auch implementierte Methoden.
Abstrakte Klassen und abstrakte Methoden werden durch den Modifizierer abstract
gekennzeichnet.
Nicht abstrakte Subklassen einer abstrakten Klasse müssen ALLE abstrakte Methoden der Vaterklasse implementieren.
Eine Form-Klasse, die über verschiedene Unterklassen wie Kreis, Quadrat und Dreieck abstrahiert. Alle Formen bieten eine Möglichkeit zur Berechnung der Fläche.
public abstract class EinfacheForm {
protected double hoehe;
abstract double berechneFlaeche();
double berechneVolumen() {
return berechneFlaeche() * hoehe;
} }
class Kreis extends EinfacheForm {
double r = 0.0;
double berechneFlaeche() {
return Math.PI * r * r;
} }
class Quadrat extends EinfacheForm {
double seite = 0.0;
double berechneFlaeche() {
return seite * seite;
} }
Abstrakte Methoden
Abstrakte Methoden, dürfen nicht private
, final
oder static
sein.
Abstrakte Methoden können von nicht-abstrakten Methoden aufgerufen werden.
Abstrakte Klassen können von anderen (auch nicht-abstrakten) Klassen erben.
Konkrete Subklassen müssen alle abstrakten Methoden implementieren.
Statischer Typ
Abstrakte Klassen können als statischer Typ von Referenzvariablen verwendet werden.
Klassen, die von einer abstrakten Klasse erben, sind typkonform zu der abstrakten Klasse und können implizit in diese konvertiert werden.
Referenzvariablen (Abstrakte Klassen) können an den gewohnten Stellen verwendet werden.
final
Modifikator)Durch den Modifikator final
kann das Überschreiben von Methoden bzw. ganzen Klassen verhindert werden.
Methoden, die durch den Modifikator final
gekennzeichnet sind, können in Subklassen nicht überschrieben werden.
Von Klassen, die durch den Modifikator final
gekennzeichnet sind, können keine Subklassen abgeleitet werden
Konto.java
public class Konto {
private String name;
protected double saldo;
public final double getSaldo(){
return saldo;
}
public final void setSaldo(double saldo){
this.saldo = saldo;
}
}
Festgeldkonto.java
public final class Festgeldkonto extends Konto {
private int laufzeit;
//...
}
Attributen, die als final
markiert sind, kann nur einmal einen Wert zuweisen. Dies hat mit Vererbung nichts zu tun.
Vererbung, Exceptions und Abstrakte Klassen
Wir möchten mathematische Ausdrücke repräsentieren, um darauf verschiedene Operationen auszuführen.
Erstellen Sie eine abstrakte Klasse Term
, die eine Methode int evaluate()
deklariert. Die Methode evaluate
soll eine Checked Exception vom neu anzulegenden Typ MathException
werfen, wenn die Auswertung nicht möglich ist. Die abstrakte Klasse Term
hat ein privates Attribut mit der Priorität des Terms (als int Wert), welcher bei der Initialisierung gesetzt wird. Implementieren Sie eine passende finale Methode int getPriority()
in der abstrakten Klasse. Die Priorität eines Terms ist relevant, wenn man einen Ausdruck ausgeben möchte und die Klammern minimieren möchte.
Erstellen Sie dann die Klassen Number
, Plus
und Division
, die von Term
erben und ggf. Referenzen auf weitere Terme halten. Number
repräsentiert eine Zahl, Plus
eine Addition und Division
eine Division. Implementieren Sie die Methode int evaluate()
in den Subklassen. Legen Sie für jede Klasse einen passenden Konstruktor an. Werfen Sie ggf. eine MathException
, wenn die Auswertung nicht möglich ist.
Implementieren Sie für jede konkrete Klasse eine Methode public String toString()
, die den Term als String zurückgibt und Klammerung durchführt wenn notwendig. Die Methode toString()
soll die Klammern so setzen, dass der Ausdruck korrekt ist. D. h. (1 + 2) * 3
soll als (1 + 2) * 3
und nicht als 1 + 2 * 3
ausgegeben werden. Weiterhin soll eine Ausdruck wie 1 + 2 + 3
als 1 + 2 + 3
und nicht als 1 + (2 + 3)
oder (1 + 2) + 3
ausgegeben werden.
Schreiben Sie eine main
Methode und testen Sie mit verschiedenen Termen die Auswertung und die Ausgabe.
Achten Sie darauf, dass im Falle einer Exception eine passende Fehlermeldung ausgegeben wird.
Beispiele für die Verwendung:
System.out.println(
new Division(
new Number(1),
new Plus(
new Plus(new Number(1),new Number(2)),
new Number(1))));
Ausgabe:
1 / (1 + 2 + 1)
System.out.println(
new Plus(
new Number(1),
new Division(new Number(2), new Number(1))));
Ausgabe:
1 + 2 / 1
interface
s)Schnittstellen (Interfaces) werden ähnlich wie Klassen deklariert, spezifizieren aber nur Methoden-Schnittstellen (abstrakte Methoden und default
Methoden) und öffentliche statische finale Attribute.
<public>? interface <Schnittstellenname>{
// statische, finale Attribute
// Methodendeklarationen und "default" Methoden
}
Saeugetier.java
1interface Saeugetier {
2int[] getAnzahlZitzen();
3default int getAnzahlNachkommen() {
4int durchschnittlicheZitzenAnzahl =
5java.util.Arrays.stream(getAnzahlZitzen()).sum() / 2;
6return durchschnittlicheZitzenAnzahl / 2;
7}
8}
Haustier.java
1interface Haustier {
2int MAXIMALES_ALTER = 100;
3public abstract String getRufname();
4}
Von Schnittstellen können keine Objekte erzeugt werden.
Schnittstellen können aber als statischer Typ eines Objektes verwendet werden.
Die Angabe von public abstract
bei Methoden ist optional.
Die Angabe von public final static
bei Attributen ist optional.
Implementierung von Schnittstellen
Eine Klasse kann mehrere Schnittstellen implementieren. Die Methoden der Schnittstellen müssen in der Klasse implementiert werden.
Katze.java
1public class Katze implements Haustier, Saeugetier {
2private String rufname;
3public Katze(String rufname) { this.rufname = rufname; }
45
public String getRufname() { return rufname; } // von Haustier
6public int[] getAnzahlZitzen() { return new int[] {6, 8 }; }// von S.-tier
78
public String toString() {
9return "Katze { rufname = " + rufname + " }";
10} }
Vererbung von Schnittstellen
Eine Schnittstelle kann von einer oder mehrerer Schnittstelle erben.
interface <Schnittstelle>
extends <Schnittstelle> (, <Schnittstelle>)* {
//...
}
Schnittstellenvererbung
1interface Carnivora /*Raubtiere*/ extends Saeugetier {
2Tier bevorzugteJagdBeute();
3}
Statischer Typ
Schnittstellen können (wie Klassen) als statischer Typ von Objekten(Referenzvariablen) verwendet werden.
Klassen, die eine Schnittstelle implementieren, sind typkonform zu der Schnittstelle und können implizit in diese konvertiert werden.
Referenzvariablen (mit den statischen Datentyp einer Schnittstellen) können an den gewohnten Stellen verwendet werden.
Es ist nicht möglich Interfaces mit Methoden mit inkompatiblen Signaturen zu implementieren. Es ist aber möglich, dass eine Klasse mehrere Interfaces implementiert, die Methoden mit gleichen Signaturen haben. In diesem Fall muss die Klasse die Methode nur einmal implementieren.
1interface Foo {
2int m();
3}
45
interface Bar {
6default double m(){return 1;}
7}
89
class FooBar implements Bar, Foo {
10public int m() { /* Main.java:10: error: m() in Main.FooBar
11cannot implement m() in Bar
12public int m() {
13^
14return type int is not compatible
15with double */
16return 2;
17}
18}
1920
void main() {
21FooBar fb = new FooBar();
22System.out.println(fb.m() + " " + ((Bar)fb).m());
23}
Ausdrücke vergleichen (Schnittstellen, instanceof, Type Casts)
Erweitern Sie die Lösung der vorhergehenden Übung wie folgt.
Definieren Sie eine Schnittstelle Comparable
, die eine Methode boolean equal(Term t)
deklariert. Implementierungen der Methode sollen den aktuellen Term vergleichen mit dem Übergebenen und true
zurückgeben, wenn der aktuelle Term (this
) identisch zum übergebenen Term (t
) ist. Beachten Sie die das Kommutativgesetz beim Vergleich; d. h. a + b
ist gleich b + a
.
Die abstrakten Klasse Term
soll die Schnittstelle implementieren. Die Implementierungen der Methoden müssen natürlich in den Subklassen erfolgen.
Beispiel
System.out.println(
new Plus(new Number(1), new Number(2))
.equal(
null
));
System.out.println(
new Plus(new Number(1), new Number(2))
.equal(
new Plus(new Number(1), new Number(2)))
);
System.out.println(
new Plus(new Number(2), new Number(1))
.equal(
new Plus(new Number(1), new Number(2)))
);
Ausgabe:
false true true