Die Klasse Object
¶
Die Klasse java.lang.Object
ist die Basisklasse (Elternklasse) aller in Java existierenden Klassen. Object
wird häufig auch als die Mutter aller Klassen in Java bezeichnet.
Eine Klasse kann entweder explizit von einer anderen Klasse erben (mithilfe von extends
) oder sie erbt implizit von der Klasse Object
.
Das bedeutet, dass jede Klasse von der Klasse Object
erbt.
Betrachten wir nochmal zur Wiederholung unsere Vererbungshierarchie aus dem vorherigen Abschnitt Vererbung:
- Dort hatten wir zunächst die Klasse
Viereck
erstellt, die explizit von keiner Klasse geerbt hat.Viereck
erbt somit implizit vonObject
. - Die Klasse
Rechteck
erbt vonViereck
und somit auch vonObject
. - Die Klasse
Quadrat
erbt vonRechteck
und somit auch vonViereck
und somit auch vonObject
.
Wenn wir uns nun noch daran erinnern, dass wir beim Erstellen der Konstruktoren gesagt haben, dass bei der Objekterzeugung auch immer ein Objekt der Elternklasse erzeugt wird, dann bedeutet das, dass für jedes Objekt auch immer ein Objekt der Klasse Object
erzeugt wird.
Wenn wir uns nun auch noch daran erinnern, dass in einer Vererbungshierarchie immer die is-a-Relation (ist ein) gilt (jedes Rechteck
ist ein Viereck
, jedes Quadrat
ist ein Rechteck
ist ein Viereck
), dann gilt dass jedes Objekt auch ein Objekt vom Typ Object
ist. Das bedeutet insbesondere, dass jedes Objekt alle Objekteigenschaften (Objektmethoden) der Klasse Object
geerbt hat.
Jedes Objekt (egal von welchem Referenztyp) ist auch ein Objekt vom Typ
Object
und hat alle Objektmethoden vonObject
geerbt.
Objektmethoden von Object
¶
Jedes Objekt in Java hat also automatisch die Methoden von Object
geerbt. Einige davon betrachten wir nun etwas genauer:
Objektmethode von Object |
Bedeutung |
---|---|
getClass() |
gibt den Laufzeittyp der Klasse zurück |
toString() |
gibt einen String zurück → sollte in jeder Klasse überschrieben werden, um eine geeignete textuelle Beschreibung der Objekte zu haben |
equals(Object) |
für den Vergleich zweier Objekte → sollte in jeder Klasse überschrieben werden, um Gleichheit von Objekten zu beschreiben (default: Referenzvergleich) |
hashCode() |
gibt einen HashCode (ein int ) für ein Objekt zurück, wird benötigt zum Einsortieren in hashbasierten Containern → später in Collections |
clone() |
gibt eine Kopie (einen Clone) des Objektes zurück |
wait() , notify() , notifyAll() |
für Threads → machen wir viel später |
finalize() |
für die Garbage Collection → ist seit Java 9 deprecated |
Die Objektmethoden aus den letzten beiden Zeilen der Tabelle betrachten wir hier nicht weiter. Die anderen Objektmethoden werden im Folgenden genauer untersucht. Wir beginnen mit getClass()
.
Die Objektmethode getClass()
¶
Angenommen, wir haben die Klassen Viereck
, Rechteck
und Quadrat
aus dem vorherigen Kapitel Vererbung gegeben:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Wenn wir nun in z.B. einer main()
-Methode folgende Anweisungen haben:
1 2 3 |
|
, dann wissen wir, dass v1
vom Typ Viereck
ist, r1
vom Typ Rechteck
und q1
vom Typ Quadrat
. Die Deklarationen dieser Variablen geben den sogenannten Compilertyp an. Und tatsächlich haben wir ja im obigen Fall auch die dazu passenden Objekte erzeugt, die genau dem jeweiligen Typ entsprechen. Wenn wir nun also jeweils die getClass()
-Methode aufrufen, dann bekommen wir die jeweiligen Typen zurückgegeben:
1 2 3 4 5 6 |
|
class Viereck
class Rechteck
class Quadrat
getClass()
gibt jedoch nicht den Compilertyp, sondern den Laufzeittyp zurück.
Compilertyp vs. Laufzeittyp¶
Was sind Compiler- und Laufzeittypen? Compilertyp wissen wir schon. Bei der Deklaration einer Variablen geben wir den Compilertypen der Variablen an. Was wir aber auch wissen, ist, dass jedes Rechteck
ist auch ein Viereck
. Das erlaubt uns, auch Folgendes zu schreiben:
1 |
|
Jetzt ist v
vom (Compiler-)Typ Viereck
, aber vom Laufzeittyp Rechteck
. Die Referenzvariable v
zeigt auf ein Rechteck
-Objekt. Mit getClass()
erfragen wir den Laufzeittyp, d.h.
1 2 |
|
class themen.vererbung.Rechteck
Wir können also auch soetwas machen:
1 2 3 4 5 6 7 8 |
|
Das bedeutet auch, dass sogar soetwas möglich ist:
1 2 3 4 5 6 7 |
|
Der Compilertyp einer (Referenz-)Variablen wird durch die Deklaration bestimmt. Der Laufzeittyp wird bestimmt durch das konkrete Objekt, auf das die Referenzvariable zeigt.
Welche Objektmethoden anwendbar? - Typecast¶
Wenn wir schonmal bei der Unterscheidung zwischen Compilertyp und Laufzeittyp sind, dann können wir gleich der Frage nachgehen, welche Objektmethoden anwendbar sind. Erinnern wir uns dazu nochmal an die Erweiterung der Klasse Rechteck
. Dort hatten wir eine Objektmethode flaecheninhalt()
definiert, die in der Klasse Viereck
nicht existiert.
Wir hatten folgenden Fall:
1 2 3 4 |
|
Wenn wir nun
1 |
|
v
also den Compilertyp Viereck
hat und den Laufzeittyp Rechteck
. Können wir dann
1 |
|
Viereck
steht diese Methode aber nicht zur Verfügung. Das geht also nicht. Was wir aber in diesem Fall machen können, ist eine explizite Typkonvertierung.
1 2 3 |
|
Nochmal im Detail:
- In Zeile
1
definieren wir eine Referenzvariablev
vom CompilertypViereck
. - In Zeile
2
definieren wir eine Referenzvariabler
vom CompilertypRechteck
. - Weil
r
vom CompilertypRechteck
ist, können wir fürr
die Objektmethodeflaecheninhalt()
aufrufen (fürv
nicht!). - Dass die Typkonvertierung in Zeile
2
auch tatsächlich gelingt, liegt (zur Laufzeit) daran, dass der Laufzeittyp vonv
Rechteck
ist. Wäre das nicht der Fall, würde die Typkonvertierung scheitern - aber erst zur Laufzeit (mit einerClassCastException
).
instanceof
vs. getClass()
¶
Wie gesagt, ermitteln wir mit getClass()
den Laufzeittypen einer Referenzvariablen. Dafür gibt es auch noch ein anderes Schlüsselwort in Java, nämlich instanceof
. Das ist ein Operator, mit dessen Hilfe wir einen Vergleich mit Typen anstellen können. Zunächst ein Beispiel:
1 2 3 4 5 |
|
In Zeile 2
sehen wir die Anwendung des instanceof
-Operators. Er gibt ein boolean
zurück, je nachdem die Variable vom angegebenen Typen ist oder nicht. Der obige Code erzeugt also die Ausgabe
v ist vom Typ Viereck
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
true
, d.h. es wird folgende Ausgabe erzeugt:
o ist vom Typ Object
o ist vom Typ Viereck
o ist vom Typ Rechteck
o ist vom Typ Quadrat
instanceof
prüft also jeden möglichen Laufzeittyp (wir wissen ja, dass ein Objekt vom Typ Quadrat
ist ein Objekt vom Typ Rechteck
ist ein Objekt vom Typ Viereck
ist ein Objekt vom Typ Object
). Das gleiche gilt auch für:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
true
, d.h. es wird folgende Ausgabe erzeugt:
q ist vom Typ Object
q ist vom Typ Viereck
q ist vom Typ Rechteck
q ist vom Typ Quadrat
Die Methode
getClass()
liefert also den konkretesten (speziellsten) Laufzeittypen zurück. Mitinstanceof
können alle Laufzeittypen abgefragt werden. Für eine beliebige Variablevar
, egal welchen Referenztyps, gilt immer, dassvar instanceof Object
true
ergibt, d.h. jede Referenzvariable ist immer auch vom (Laufzeit-)TypObject
.
Die Objektmethode toString()
¶
In jeder Klasse, die wir erstellen, erben wir von Object
die Objektmethode toString()
. Wenden wir diese Methode also einmal für unsere Klasse Viereck
an:
1 2 |
|
Viereck@279f2327
Viereck
für die Klasse steht und @279f2327
scheint irgendeine Referenzadresse zu sein. Interessant an der toString()
-Methode ist, dass wir die gleiche Ausgabe auch dann erzielen, wenn wir nur
1 2 |
|
System.out.println()
nur v
und nicht v.toString()
übergeben. Das liegt daran, dass System.out.println()
überladen ist und - unter anderem - die beiden Implementierungen
System.out.println(String s) {}
System.out.println(Object o) {}
System.out.println(v.toString());
aufrufen, wird die Implementierung von System.out.println(String s) {}
verwendet (der String s
wird ausgegeben). Wenn wir System.out.println(v);
aufrufen, wird die Implementierung von System.out.println(Object o) {}
verwendet und dabei wird nämlich System.out.println(o.toString());
aufgerufen.
Wenn wir nun also die Methode toString()
überschreiben (ist ja von Object
geerbt), dann gewinnen wir zwei Effekte:
- wir erstellen eine textuelle Reprässentation unserer Objekte und
- wir müssen
System.out.println()
nur noch die Referenzvariableref
auf unser Obejkt übergeben (und nichtref.toString()
)
Erweitern wir also die Klasse Viereck
um eine Implementierung der toString()
-Methode:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Wir verwenden auch hier die @Override
-Annotation, um dem Compiler zu sagen, dass wir die toString()
-Methode von Object
überschreiben wollen (nicht, dass wir z.B. ausversehen tostring()
schreiben und somit eine neue Objektmethode erstellen). In der toString()
-Methode implementieren wir eine geeignete Repräsentation des Objektes (hier die Seitenlängen des Vierecks). Nun erzeugen die Anweisungen
1 2 |
|
[ a=10, b=20, c=30, d=40 ]
Wir sollten uns angewöhnen, die
toString()
-Methode immer, d.h. in allen Klassen, die wir erstellen, zu überschreiben!
Die Objektmethode equals()
¶
Wir wiederholen zunächst nochmal in Kürze den Abschnitt über Referenzvergleiche von Objekten. Angenommen, wir haben folgende Vierecke:
Viereck v3 = new Viereck(10,20,30,40);
Viereck v4 = new Viereck(10,20,30,40);
System.out.println(v3==v4); // Referenzvergleich!! false - zwei Objekte
v3==v4
true
ergibt, weil für uns die beiden Objekte von Viereck
gleich sind. Aber woher soll der Compiler oder die Laufzeitumgebung wissen, dass diese Viereck
-Objekte gleich sind?
Der Operator
==
vergleicht nur die Referenzen und istfalse
, wenn die Referenzen auf zwei verschiedene Objekte zeigen.
Mithilfe der equals()
-Methode können wir definieren, wann Objekte der Klasse gleich sein sollen. Wir können aber nicht den Operator ==
überschreiben. Dieser bleibt für Referenztypen immer ein Referenzvergleich!
Wir wollen unsere Klasse Viereck
also um eine equals()
-Methode erweitern und in dieser equals()
-Methode festlegen, wann zwei Viereck
-Objekte gleich sein sollen (wenn ihre Seitenlängen gleich sind). Wir versuchen folgendes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Das führt leider zu einem Fehler. Der Compiler beschwert sich darüber, dass wir die geerbte equals()
-Methode so gar nicht überschreiben. Tatsächlich erben wir nicht equals(Viereck v)
, sondern equals(Object o)
(woher sollte Object
auch Viereck
kennen?). Wir müssen also folgende Methode überschreiben:
@Override
public boolean equals(Object o)
{
// Implementierung von equals()
}
Natürlich erwarten wir, dass sich das aufrufende Viereck mit einem anderen Viereck vergleicht. Die Methode ist aber so implementiert, dass jedes beliebige Objekt als Parameter übergeben werden kann. Theoretisch wäre also z.B. folgender Aufruf möglich:
Viereck v = new Viereck(10,20,30,40);
Point p = new Point(3,4);
Systems.out.println(v.equals(p));
Das soll natürlich false
ergeben. Die Implementierung von equals(Object o)
muss folgende Bedingungen erfüllen:
- Null-Akzeptanz: für jede Referenz
x
ungleichnull
liefertx.equals(null)
den Wertfalse
- Reflexivität: für jede Referenz
x
ungleichnull
liefertx.equals(x)
den Werttrue
- Symmetrie: wenn
x.equals(y)
true
ergibt, dann muss auchy.equals(x)
true
ergeben (und umgedreht) - Transitivität: wenn
x.equals(y)
undy.equals(z)
jeweilstrue
ergeben, dann muss auchx.equals(z)
true
ergeben - Konsistenz: der Aufruf
x.equals(y)
muss immer den gleichen Wert ergeben
Das hört sich komplizierter an, als es ist. Wir werden sehen, dass wir bei der Implementierung von equals(Object o)
immer gleich vorgehen. Wir führen zunächst ein paar Prüfungen durch:
- prüfen, ob
null
-Referenzen vorliegen → (wenn ja, dannfalse
) - prüfen, ob keine identischen Objekte verglichen werden (dasselbe Objekt vergleicht sich mit sich selbst) → (wenn ja, dann
true
) - prüfen, ob Objekte des gewünschten Typs verglichen werden → (wenn nein, dann
false
)
1 2 3 4 5 6 7 8 9 10 11 12 |
|
- in Zeile
4
prüfen wir, ob das als Parameter übergebene Objekt überhaupt existiert. Wenn nicht (Referenznull
), geben wirfalse
zurück. - in Zeile
5
prüfen wir, ob das aufrufende Objekt dasselbe ist, wie das als Parameter übergebene Objekt (vergleich mit sich selbst). Wenn ja, geben wirtrue
zurück. - in Zeile
6
prüfen wir, ob das aufrufende Objekt und das als Parameter übergebene Objekt den gleichen Typ haben (also hierViereck
). Wenn nicht, geben wirfalse
zurück.
Wenn diese Prüfungen alle false
waren, dann wissen wir danach, dass other
vom (Laufzeit-)Typ Viereck
ist und auf ein Viereck
-Objekt zeigt. Nun können wir den eigentlichen Objektvergleich durchführen. Dazu müssen wir jedoch other
in den Typ Viereck
konvertieren:
- da beide Objekte vom gleichen Typ sind (
Viereck
), kann das Objekt aus dem Parameter in den vergleichenden Typ umgewandelt werden (z.B.Object
nachViereck
) - dann können wir die Eigenschaften vergleichen, die für die „Gleichheit“ relevant sind (z.B.
radius
beiCircle
,kontonummer
beiKonto
,a
undb
beiRectangle
usw. - hier: die vier Seiten des Vierecksa
,b
,c
undd
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
- in Zeile
15
führen wir eine explizite Typkonvertierung durch. Wir wissen an dieser Stelle ja bereits (aus Zeile6
), dass es sich beiother
um den LaufzeittypViereck
handelt. Die Konvertierung klappt also. Weilother
den CompilertypObject
hat, können wir fürother
nicht auf die Objektvariablena
,b
,c
,d
zugreifen. Wir müssen also konvertieren. - in Zeile
16
führen wir dann den eigentlichen Vergleich durch. Hier vergleichen wir die Seiten miteinander. Wir berücksichtigen nicht, dass Vierecke auch gedreht gleich sein können. Das ist Auslegungssache und Ihre Entscheidung, wann Objekte tatsächlich gleich sein sollen.
Jetzt können wir die Gleichheit von zwei Viereck
-Objekten mithilfe von equals()
ermitteln:
Viereck v3 = new Viereck(10,20,30,40);
Viereck v4 = new Viereck(10,20,30,40);
Viereck v5 = new Viereck(11,22,33,44);
System.out.println(v3.equals(v4)); // true
System.out.println(v3.equals(v3)); // true
System.out.println(v4.equals(v3)); // true
System.out.println(v3.equals(v5)); // false
System.out.println(v3.equals(null)); // false
Success
Mithilfe der equals()
-Methode haben wir eine einheitliche Möglichkeit, die Gleichheit von Objekten zu definieren. Die Implementierung der equals()
-Methode folgt immer dem gleichen Schema. Wir führen zunächst die drei Prüfungen auf Null-Akzeptanz, Reflexivität und ungleiche Typen aus, konvertieren other
dann in unseren Klassentyp und führen den eigentlichen Vergleich auf Gleichheit der Objekte durch. Wir sollten equals()
, wie auch toString()
, von nun an für alle unsere Klassen implmentieren.
Die Objektmethode hashCode()
¶
Die Idee der hashCode()
-Methode besteht darin, ein Objekt durch eine ganze Zahl zu repäsentieren. Diese Zahl wird verwendet, um Objekte in sogenannte hashbasierte Container einzusortieren. Das sind Datenstrukturen, in denen viele Objekte gespeichert werden und die Speicherung über einen Hashwert verteilt wird. Wir werden solche hashbasierten Container im 2. Semester kennenlernen, wenn wir uns mit Collections beschäftigen. Zum jetzigen Stand kümmern wir uns um diese Methode nicht weiter, wollen sie aber doch immer genau dann überschreiben, wenn wir die equals()
-Methode implementieren. Es soll folgendes gelten:
Wenn zwei Objekte laut
equals()
-Methode gleich sind, dann erzeugen sie auch den gleichen Hashcode mit derhashCode()
-Methode.
Es soll also gelten: wenn x.equals(y)==true
, dann x.hashCode()==y.hashCode()
. Wenn wir also die equals()
-Methode überschreiben, dann überschreiben wir auch die hashCode()
-Methode, um die genannte Bedingung zu erfüllen. Da wir für das Viereck
die Seitenlängen verwendet haben, um die Gleichheit von zwei Vierecken zu definieren, können wir diese Seitenlängen auch verwenden, um einen HashCode zu erzeugen:
@Override
public int hashCode()
{
return this.a + this.b + this.c + this.d;
}
Mit dieser Implementierung ist gegeben, dass zwei Vierecke, die laut equals()
-Methode gleich sind (haben die gleichen Seitenlängen), auch den gleichen HashCode haben. Es muss (zum Glück) nicht gelten, dass zwei Vierecke, die laut equals()
-Methode ungleich sind, einen unterschiedlichen HashCode haben müssen.
Die Objektmethode clone()
¶
Die Objektmethode clone()
liefert einen identischen Clone (eine identische Kopie) des aufrufenden Objektes zurück. Wir wollen uns an dieser Stelle gar nicht weiter detailliert um clone()
kümmern. Wir kommen darauf zurück, wenn wir im 2. Semester Interfaces kennenlernen. Die Methode clone()
ist auch nicht unumstritten - das aber nur zur Information, wie auch ein Beispiel für die folgende mögliche Implementierung der Methode in der Klasse Viereck
.
@Override
protected Object clone()
{
return new Viereck(this.a, this.b, this.c, this.d);
}
Das dient nur zum Verständinis der Idee von clone()
. Im Gegensatz zu toString()
und equals()
(und also auch hashCode()
) werden wir clone()
nicht so oft überschreiben.
Success
Wir haben mit Object
die Mutter aller Klassen in Java kennengelernt. Jede Klasse in Java erbt (implizit) von Object
. Jede Referenzvariable ist somit (auch) vom Laufzeittyp Object
. Für alle Klassen, die wir in Zukunft schreiben, werden wir die Objektmethoden toString()
und equals()
(und also auch hashCode()
) überschreiben.
Polymorphie¶
Polymorphie gehört neben der Datenkapselung und der Vererbung zu den wesentlichen Konzepten der objektorientierten Programmierung. Die Grundidee der Polymorphie ist, dass es verschiedene Methoden gibt, die gleich heißen und dass entweder der Compiler (statisch) oder die Laufzeitumgebung (dynamisch) auswählt, welche dieser Methoden ausgeführt wird. Man unterscheidet zwischen statischer und dynamischer Polymorphie.
Statische Polymorphie¶
Statische Polymorphie haben wir in Verbindung mit dem Überladen von Methoden. Der Compiler kann (an der Methodensignatur) erkennen, welche Methode aufgerufen wird. Angenommen, wir haben folgende Methoden:
public void printArray(int[] arr)
{
// Ausgabe eines int[]-Arrays
}
public void printArray(char[] arr)
{
// Ausgabe eines char[]-Arrays
}
public void printArray(double[] arr)
{
// Ausgabe eines double[]-Arrays
}
public void printArray(String[] arr)
{
// Ausgabe eines String[]-Arrays
}
public void printArray(Object[] arr)
{
// Ausgabe eines Object[]-Arrays
}
Dynamische Polymorphie¶
Dynamische Polymorphie wird durch Vererbung und insbesondere durch das Überschreiben von Methoden ermöglicht. Wir betrachten folgendes Beispiel - gegeben sind drei Klassen Base
, Sub
und SubSub
:
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 |
|
1 2 3 4 5 6 7 8 |
|
Die Klasse Sub
erbt von Base
und die Klasse SubSub
erbt von Sub
. In beiden Kindklassen wird die Methode methodBase()
überschrieben, die in Base
erstmalig definiert wird. Angenommen, wir haben nun folgende main()
-Methode:
1 2 3 4 5 6 7 8 9 10 11 |
|
Wir erstellen uns also ein Array, deren Elemente vom Compilertyp Base
sind. Das erste Element ist eine Referenz auf ein Base
-Objekt, das zweite Element ist eine Referenz auf ein Sub
-Objekt und das dritte zeigt auf ein SubSub
-Objekt. Für alle drei Referenzvariablen wird nun die methodBase()
-Methode aufgerufen. Es werden folgende Ausgaben erzeugt:
Base
Sub
SubSub
Das bedeutet, dass die Laufzeitumgebung von Java die speziellstmögliche Implementierung der Methode auswählt. Mit speziellstmöglich ist gemeint, dass die Implementierung des speziellsten Laufzeittypen ausgewählt wird. In der Vererbungshierarchie SubSub
→ Sub
→ Base
ist SubSub
der speziellste Typ, Sub
ist spezieller als Base
, aber allgemeiner als Sub
und Base
ist allgemeiner als Sub
und erst recht als SubSub
.
- Der speziellste Laufzeittyp von
base[0]
istBase
und somit wird diemethodBase()
-Implementierung der KlasseBase
verwendet. - Der speziellste Laufzeittyp von
base[1]
istSub
und somit wird diemethodBase()
-Implementierung der KlasseSub
verwendet. - Der speziellste Laufzeittyp von
base[2]
istSubSub
und somit wird diemethodBase()
-Implementierung der KlasseSubSub
verwendet.
Success
Polymorphie ist ein tolles Konzept der objektorientierten Programmierung. Der Nutzen von Polymorphie wird uns jetzt noch nicht vollständig deutlich. Wir werden aber immer wieder darauf hinweisen, wenn wir Polymorphie im Einsatz sehen. Vielleicht erkennen Sie ja jetzt besser, warum z.B. die Methode System.out.println(Object o)
so funktioniert. Spätestens, wenn wir Interfaces behandeln, kommen wir auf dieses Konzept zurück.