Vererbung – Abstrakte Klassen und Interfaces
1. Abstrakte Klassen mit (optional) abstrakten Methoden
Oft ist die gemeinsame Superklasse mehrerer ähnlicher Klassen für den angestrebten Zweck recht allgemein und unvollständig. Es hat also keinen Sinn, direkt Objekte dieser Superklasse zu erzeugen.
Um dies deklarativ erzwingen zu können, gibt es die Möglichkeit, eine Klasse als abstrakt festzulegen. Das geschieht mit dem Schlüsselwort abstract
.
Da in solchen Klassen oft das Vorhandensein bestimmter Methoden festgelegt werden soll, ohne dass eine sinnvolle Implementierung auf dieser Ebene möglich wäre, können in abstrakten Klassen auch Methoden als abstrakt deklariert werden. Sie bestehen dann nur aus dem Methodenkopf mit abschließendem Semikolon (statt dem mit geschwungener Klammer eingeschlossenen "Method Body") und nach der Sichtbarkeit dem eingefügten Schlüsselwort abstrakt – z.B.:
public abstract void methodenName(int par1, String par2);
Diese abstrakten Methoden müssen in den Kindklassen konkret implementiert werden (es sei denn, eine Kindklasse ist selbst noch abstrakt). Damit ist sichergestellt, dass Instanzen der konkreten Klassen alle Methoden funktionsfähig zur Verfügung haben. Beispiel:
public abstract class Person { // xxx RX Test
private String name;
private int birthYear;
private char gender;
public Person(String name, int birthYear, char gender) {
setName(name); setBirthYear(birthYear); setGender(gender); }
public void setName(String name) { this.name = name; }
public void setBirthYear(int birthYear) { this.birthYear = birthYear; }
public void setGender(char gender) { this.gender = gender; }
public String getName() { return name; }
// other getters ...
public void printInfo(double wkHrs) {
System.out.println("Name: " + name);
System.out.println("BirthYear: " + birthYear);
System.out.println("Gender: " + gender);
System.out.println("Income (" + wkHrs + "hrs):" + calcIncome(wkHrs));
}
public abstract double calcIncome(double workHrs); // depending on kind of work -> implement in subclass
}
public class Employee extends Person {
private int saleryLevel;
public Employee(String name, int birthYear, char gender, int saleryLevel) {
super(name, birthYear, gender);
this.saleryLevel = saleryLevel;
}
@Override public void printInfo(double wkHrs) {
super.printInfo(wkHrs);
System.out.println("SaleryLevel: " + saleryLevel);
}
@Override public double calcIncome(double workHrs) {
double base = 1500 + saleryLevel*120;
double overtime = workHrs - 168;
return base + overtime*(10 + saleryLevel)*1.2;
}
}
public class Freelancer extends Person {
private double hourlyWage;
public Freelancer(String name, int birthYear, char gender, double hourlyWage) {
super(name, birthYear, gender);
this.hourlyWage = hourlyWage;
}
@Override public void printInfo(double wkHrs) {
super.printInfo(wkHrs);
System.out.println("HourlyWage : " + hourlyWage);
}
@Override public double calcIncome(double workHrs) {
return workHrs * hourlyWage;
}
}
public class RichMan extends Person {
private double wealth;
public RichMan(String name, int birthYear, char gender, double wealth) {
super(name, birthYear, gender);
this.wealth = wealth;
}
@Override public void printInfo(double wkHrs) {
super.printInfo(wkHrs);
System.out.println("Wealth : € " + wealth);
}
@Override public double calcIncome(double workHrs) {
return wealth*0.1;
}
}
2. Monohierarchische Klassenstruktur in Java — aber es gibt Interfaces
In Java kann eine Klasse nur eine einzige Elternklasse haben - das ist in manchen Fällen ein Problem.
Beispiel: Ein Amphibienfahrzeug ist gleichzeitig Wasserfahrzeug und Landfahrzeug - d. h. es sollte von beiden erben können.
Um verschiedene damit entstehende Probleme zu vermeiden, ist in Java auf Mehrfachvererbung verzichtet worden. Anstelle dessen wurde das Konzept von Interfaces bereitgestellt.
Ein Interface verhält sich in vielen Hinsichten wie eine abstrakte Klasse, in der es nur abstrakte Methoden gibt - d.h. keine einzige Methode ist implementiert (stimmt seit Java 8 so nicht mehr - nun gibt es "Default Implementationen" von Interface-Methodendeklarationen).
Ein Interface ist immer public (muss daher nicht extra geschrieben werden).
Durch diese Situation fallen viele Mehrfachvererbungsprobleme weg und somit ist es für eine Klasse möglich, mehrere Interfaces gleichzeitig zu implementieren (Erben von einer Vaterklasse bleibt ebenfalls aufrecht). Ein einfaches Beispiel findet sich ganz unten (Interface ChildAndSickSecured).
Interfaces können von mehreren anderen Interfaces erben (dann allerdings mit dem Schlüsselwort extends - da ja das Interface erweitert und nicht implementiert wird).
Interfaces werden oft in 2 leicht unterschiedlichen Arten verwendet: einmal, um einen Aspekt zu spezifizieren (oft, aber nicht immer Endung "able" o.ä.), das tun alle unsere Interfaces und zweitens als "Gesicht" einer Klasse - dann nennt man das Interface oft wie sonst die Klasse heißen würde und hängt der implementierenden Klasse z.B. ein "Impl" (für Implementation) o.ä. an den Namen. Wenn nur Interfaces als Typen verwendet werden, kann die Implementation leicht ausgetauscht werden - z.B. die Voll-Implementation gegen eine primitive Test-Implementierung, etc. .
Hier ein Klassendiagramm mit obigen Klassen und der nachfolgend gezeigten Erweiterung mit Interfaces (Richman2, Employee2, Freelancer2)
Beispiel mit Interfaces:
public interface SickLeaveable { // krankenstandsfähig
void setSickDays(int sickDays);
int getSickDays();
}
public interface ChildSupportable { //Kinderbeihilfe-fähig
static final double AMOUNT_PER_CHILD = 300;
int getNumChildren(); // kann nicht als default implem. werden - keine InstanzVars!
void setNumChildren(int numChildren); // detto
default double calcChildSupport() { // nur Konstante und deklarierte Meth. erlaubt
return AMOUNT_PER_CHILD * getNumChildren(); // auch noch unimplem. Meth. nutzbar
}
}
public interface Stockholder {
double getStockValue();
void setStockValue(double stockValue);
}
public class Employee2 extends Person implements SickLeaveable, ChildSupportable {
private int saleryLevel;
private int sickDays = 0;
private int numChildren = 0;
public Employee2(String name, int birthYear, char gender, int saleryLevel) {
super(name, birthYear, gender);
this.saleryLevel = saleryLevel;
}
@Override public void printInfo(double wkHrs) {
super.printInfo(wkHrs);
System.out.println("SaleryLevel: " + saleryLevel);
System.out.println("SickDays: " + sickDays);
System.out.println("NumChildren: " + numChildren);
}
@Override public double calcIncome(double workHrs) {
double base = 1500 + saleryLevel*120;
double overtime = workHrs - 168;
return base + overtime*(10 + saleryLevel)*1.2;
}
@Override public int getSickDays() { return sickDays; }
@Override public void setSickDays(int sickDays) { this.sickDays = sickDays; }
@Override public int getNumChildren() { return numChildren; }
@Override public void setNumChildren(int numChildren) {this.numChildren = numChildren; }
}
public class Freelancer2 extends Person implements ChildSupportable {
private double hourlyWage;
private int numChildren = 0;
public Freelancer2(String name, int birthYear, char gender, double hourlyWage) {
super(name, birthYear, gender);
this.hourlyWage = hourlyWage;
}
@Override public void printInfo(double wkHrs) {
super.printInfo(wkHrs);
System.out.println("HourlyWage : € " + hourlyWage);
}
@Override public double calcIncome(double workHrs) {
return workHrs * hourlyWage;
}
@Override public int getNumChildren() { return numChildren; }
@Override public void setNumChildren(int numChildren) {this.numChildren = numChildren; }
}
public class RichMan2 extends Person implements Stockholder {
private double wealth;
private double stockValue;
public RichMan2(String name, int birthYear, char gender, double wealth) {
super(name, birthYear, gender);
this.wealth = wealth;
}
@Override public void printInfo(double wkHrs) {
super.printInfo(wkHrs);
System.out.println("Wealth: € " + wealth);
System.out.println("StockValue: " + stockValue);
}
@Override public double calcIncome(double workHrs) { return wealth*0.1; }
@Override public double getStockValue() { return stockValue; }
@Override public void setStockValue(double stockValue) { this.stockValue = stockValue; }
}
Um die "Mitgliedschaft" eines Objekts zu einem bestimmten Typ zu prüfen, gibt es einen eigenen Operator: instanceof.
Verwendungsbeispiel:
public class Test1 {
public void testInstanceOf() {
Person p = new Employee2("Evi", 2002, 'f', 1);
if (p instanceof SickLeaveable) {
System.out.println("the person IS AN employee"); // Dieser Text wird ausgegeben
} else {
System.out.println("the person is NOT AN employee"); // wird NICHT ausgegeben
}
}
}
Es wird auch für Instanzen der Subklassen true zurückgeliefert, da sie ja "nebenbei" auch den Typ der Superklasse haben.
Im Interface ChildSupportable wird eine Methode implementiert, obwohl das der oben gegebenen Definition für Interfaces widerspricht. Die Methode hat den Kopf "default double calcChildSupport()". Diese Möglichkeit wurde in Java 8 ergänzt, v.a. um bestehende Interfaces nachträglich erweitern zu können.
Denn wenn eine Klasse ein Interface implementiert, müssen alle enthaltenen Methoden implementiert werden. Falls nun das Interface erweitert würde, müssten ALLE Klassen (in viel verwendeten Bibliotheken können weltweit tausende Applikationen betroffen sein!) um diese Methoden ergänzt werden.
Mit dem Bereitstellen einer Default-Implementation (die allerdings nur selbstdefinierte Methoden und Konstante nutzen darf und keinen Zugriff auf Instanzvariable hat) verschwindet dieses Problem. Falls das Interface direkt (oder per Vererbung) Getter oder Setter enthält, kann auf diesem Weg auf Instanzvariable zugegriffen werden - es ist also oft eine gute Idee, diese im Interface zu definieren.
Beispiel Interface - Vererbung (es könnten darin ganz normal weitere Methoden deklariert werden - hier ist das Interface leer, fasst also nur 2 andere Interfaces zusammen):
public interface ChildAndSickSecured extends ChildSupportable, SickLeaveable {
}
Als Extremfall gibt es Interfaces, die vollständig leer sind - sogenannte Marker-Interfaces. Ein oft benutztes Marker-Interface des SDK ist z.B. java.io.Serializable
. (Für Details s. Oracle - Java Object Serialization Specification: 1 - System Architecture)
Solche Marker-Interfaces werden z.B. verwendet, um Charakteristika zuzusichern, die nicht formal spezifiziert werden können.