Quiz
Quiz: Überdecken von Attributen
class A {
private int k = 10;
public int j = 20;
public void f() {
System.out.println(k);
}
}
public class B extends A {
private int j = 3000;
public static void main(String[] args) {
A a = new A();
A b = new B();
B b2 = new B();
System.out.printf("%d %d %d%n", a.j, b.j, b2.j);
a.f();
}
}
20 20 3000
10
Das Ergebnis kommt dadurch zu Stande, dass bei überdeckten Attributen die Entscheidung, ob das Attribut vom Sub- oder Supertyp genommen wird, bereits zur Compile-Zeit getroffen wird. Daher geht ausschließlich der statische Typ in diese Entscheidung ein und nicht der dynamische Typ.
In dem vorliegenden Beispiel sind die statischen Typen für die Variablen a
, b
und b2
entsprechend A
, A
und B
. Somit wird beim Lesen des Attributes j
bei a.j
und b.j
auf das Attribut von A
und bei b2.j
auf das von B
zugegriffen.
Beim Aufruf der Methode f()
über a.f()
greift die Methode natürlich auf das Attribut der eigenen Klasse zu - woher soll sie auch wissen, dass es ein gleichnamiges Feld irgendwo in der Vererbungshierarchie weiter unter existiert.
Quiz: Vererbung
class A {
}
class B extends A {
}
public class C extends A, B {
public C() {
super(null);
}
}
de/smits_net/pr2/quiz/vererbung/C.java:6: '{' expected
public class C extends A, B {
^
1 error
Da Java nur Einfachvererbung unterstützt, scheitert dieses Beispiel bereits am Compiler, da hier versucht wurde, die Klasse C
gleichzeitig von A
und B
erben zu lassen. Die Java-Syntax erlaubt hinter dem Schlüsselwort extends
ausschließlich die Angabe einer Klasse. Hier wurden aber zwei angegeben, sodass ein Compile-Fehler entsteht.
Quiz: Überladen
class A {}
class B extends A {}
public class BindingQuiz {
public static void f(A a) { System.out.print("A "); }
public static void f(B b) { System.out.print("B "); }
public static void main(String[] args) {
A a1 = new A(); A a2 = new B(); B b = new B();
f(a1);
f(a2);
f(b);
}
}
A A B
Die Auswahl einer überladenen Methode findet ausschließlich anhand des statischen Typs statt. Es gibt also keinen virtuellen Methodenaufruf für überladene Methoden und damit kein spätes Binden. Stattdessen wählt bereits der Compiler die Methode, die aufgerufen werden soll anhand des statischen Typs aus.
Die statischen Typen der Variablen a1
, a2
und b
sind A
, A
und B
. Somit wird auch zweimal die Methode f(A)
und nur einmal die Methode f(B)
gerufen, was zur entsprechenden Ausgabe A A B
führt.
Quiz: Überschreiben
class A {
public void f() { System.out.print("A "); }
}
class B extends A {
public void f() { System.out.print("B "); }
}
public class Polymorphie {
public static void main(String[] args) {
A a1 = new A(); A a2 = new B(); B b = new B();
a1.f(); a2.f(); b.f();
}
}
A B B
Die Auswahl einer überschriebenen Methode findet anhand des dynamischen Typs statt. Der statische Typ wird vom Compiler nur dazu genutzt, um festzustellen, welche Methoden überhaupt vorhanden sind. Die eigentliche Auswahl der Methode findet aber erst zur Laufzeit statt. Es gibt also einen virtuellen Methodenaufruf damit spätes Binden.
Die dynamischen Typen der Variablen a1
, a2
und b
sind A
, B
und B
. Somit wird einmal die Methode f()
von A
und zweimal die Methode f()
von B
gerufen, was zur entsprechenden Ausgabe A B B
führt.
Quiz: Konstruktoren
class A {
public A() { System.out.println("A"); }
}
class B extends A {
public B() { System.out.println("B"); }
}
public class ConstructorQuiz {
public static void main(String[] args) {
new B();
}
}
A
B
Java gewährleistet, dass beim Erzeugen einer Instanz einer Subklasse die Konstruktoren aller Superklassen (bis hinauf zu Object
) durchlaufen werden. Der Grund hierfür ist, dass Attribute, die von Superklassen bereitgestellt werden, korrekt initialisiert werden sollen. Der Konstruktor einer Superklasse läuft grundsätzlich vor dem Konstruktor der Subklasse ab.
Um diese vollständige Initialisierung zu erreichen, wird im vorliegenden Beispiel ein implizites super()
vom Compiler als erstes Statement in die Konstruktoren von A und B generiert. Hierdurch ruft der Konstruktor von B den Konstruktor von A auf, noch bevor er selbst die Ausgabe machen kann. Als Folge sehen wir als Ausgabe A B.
Quiz: Statische Methoden
public class QuizStatic {
public static void main(String[] args) {
String b = null;
System.out.println(b.valueOf(15));
System.out.println(((String) null).valueOf(17));
}
}
15
17
Statische Methoden werden direkt auf der Klasse aufgerufen; ein (gültiges) Objekt wird nicht benötigt. Obwohl man also beim obigen Beispiel eine NullPointerException
erwarten würde, wird diese nicht geworfen, da vom Compiler nur der statische Typ der Objektreferenz ausgewertet wird, um den Aufruf der Methode auf der entsprechenden Klasse zu generieren. Danach (also insbesondere zur Laufzeit) wird die Objektreferent nicht mehr benötigt und von der Laufzeit gar nicht angefasst.
Quiz: Static Initializer
public class Initializer {
static {
initialize();
}
private static boolean initialized = false;
private static void initialize() {
initialized = true;
}
public static void main(String[] args) {
System.out.println(initialized);
}
}
false
Statische Initialisierungen werden strikt von oben nach unten durchgeführt, dazu gehört auch die Initialisierung einer Variable mit einem Wert, wie es mit der Variable initialized
passiert. Wenn man das Programm von oben nach unten liest, sieht man, dass zuerst die Methode initialize()
aufgerufen wird, die die Variable auf den Wert true
setzt, danach aber die Zuweisung erfolgt die Zusammen mit der Deklaration der Variable erfolgt, sodass die Variable wieder auf false
gesetzt wird.
Quiz: Initialisierung
public class Ente {
private static final Ente INSTANCE = new Ente();
private static final int NORMAL_GROESSE = Integer.parseInt("12");
private int groesse = NORMAL_GROESSE - 10;
public int getGroesse() {
return groesse;
}
public static void main(String[] args) {
System.out.println(INSTANCE.getGroesse());
}
}
-10
Die Ausgabe von -10
mag auf den ersten Blick verwirrend erscheinen. Der Grund liegt darin, dass bei diesem Beispiel zwei Regeln von Java miteinander kollidieren und zwar
- eine Klasse muss vollständig initialisiert werden, bevor Instanzen erzeugt werden und
- statische Initialisierungen werden streng von oben nach unten ausgeführt.
In der Zeile private static final Ente INSTANCE = new Ente();
wird eine Ente erzeugt. Hierzu muss aber die Klasse Ente initialisiert werden, was aber die Erzeugung eines Objekts von Ente erfordert. Damit kommt es zu einem Widerspruch der beiden Regeln, der so aufgelöst wird, dass die Initialisierung der Klasse abgebrochen wird, d. h. die Zuweisung von NORMALE_GROESSE = Integer.parseInt("12")
wird nicht ausgeführt. Hierdurch ist NORMALE_GROESSE
dann nur mit dem Default-Wert 0
initialisiert, sodass bei der Objekterzeugung groesse
den Wert 0 - 10
also -10
hat.
Quiz: Konstruktor
public class NameHolder {
private String name;
public void NameHolder() {
name = "Hallo";
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) {
NameHolder s = new NameHolder();
System.out.println(s);
}
}
null
Dieses Beispiel nutzt eine syntaktische Besonderheit von Java aus: Methoden dürfen genauso heißen, wie der Konstruktor, der bekanntermaßen so heißt wie die Klasse (hier also NameHolder
). Durch das Voranstellen von void
vor den Konstruktor wird dieser zu einer normalen Methode. Der Compiler generiert dann entsprechend einen Default-Konstruktor. Bei der Erzeugung eines Objektes von NameHolder
wird hier dann natürlich der generierte Default-Konstruktor aufgerufen, mit der Folge, dass name
keinen Wert hat, sondern null
ist.