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.


Copyright © 2025 Thomas Smits