Tech Archive Help

11 Java – Objekte

Nachdem ich euch jetzt gefühlt ein Dutzend Mal das Wort „Objekt“ um die Ohren gehauen habe, wird es höchste Zeit, dass wir uns endlich diesem mysteriösen Thema widmen. Was sind eigentlich Objekte? Fast alles, was in Java existiert, besteht aus Objekten, die auf sogenannten Referenzdatentypen basieren – die primitiven Datentypen wie int und boolean einmal ausgenommen.

Ein Objekt ist im Wesentlichen eine Instanz einer Klasse und diese Instanz kann so ziemlich alles sein, was ihr euch vorstellen könnt – von Bällen, über Menschen, bis hin zu abstrakten Konzepten wie Aktivitäten oder sogar Dimensionen.

Stellt euch vor, ihr seid im Spielzeugladen: Da gibt es Bälle, Figuren, Autos und ja, vielleicht sogar eine mysteriöse Dimension in eine andere Welt. All diese Spielzeuge, äh, Objekte, stammen aus einer Art Bauanleitung – der Klasse. Und hier kommt der Clou: Jede dieser Klassen ist ein stolzer Nachfahre der Klasse Object – der Urmutter aller Klassen in Java. Aber keine Sorge. Die Details dazu werden wir in Kapitel 14 – OOP: Vererbung noch genau unter die Lupe nehmen.

Also egal, ob ihr euch einen Ball oder eine neue Dimension vorstellen könnt, in Java ist es immer ein Objekt. Dieses Objekt ist nicht nur irgendein zufälliger Datenhaufen, sondern ein ordentlicher Codebaustein der brav die Regeln der objektorientierten Programmierung befolgt und durch die Eigenschaften (Objektvariablen) und Funktionalitäten (Objektmethoden) beschrieben wird.

Zwei bestimmte Typen wurden bereits in früheren Kapiteln betrachtet.

Auch eigene Objekte bzw. Referenztypen können erstellt werden, wie z.B. ein Dog oder Movie.

Initialisierung

In Kapitel 10 – Klassen wurde bereits eine Klasse Dog erstellt.

public class Dog { public String name; public int age; public Dog(String name, int age) { this.name = name; this.age = age; } public Dog(String name) { this(name, 0); } public void eat() { System.out.println(this.name + " is eating!"); } public void bark() { System.out.println(this.name + " barks!"); } }

Nun werden wir zu dieser Klasse zwei Dog-Objekte in der Main-Klasse anlegen.

public class Main { public static void main(String[] args) { Dog bello = new Dog("Bello"); Dog killer = new Dog("Killer", 5); } }

Die runden Klammern müssen genau die Anzahl der Werte enthalten, die die Konstruktoren erwarten. Da wir einen Konstruktor mit einem Wert und einen mit zwei Werten haben, sind beide Varianten gültig. Mit dem Keyword new wird ein neues Objekt (Instanz) der Klasse erstellt und im Java-Heap allokiert.

Objektvariablen auslesen und verändern

Wollen wir nun die Werte der Objekte auf der Konsole ausgeben, können wir nicht einfach nur das Objekt an die println()-Methode übergeben. Dadurch erhalten wir eine ziemlich merkwürdig aussehende Zeichenfolge, die weiter unten im Abschnitt "Die toString()-Methode" erklärt wird.

public static void main(String[] args) { Dog bello = new Dog("Bello"); Dog killer = new Dog("Killer", 5); System.out.println(bello); // kesares.techarchive.Dog@65ab7765 System.out.println(killer); // kesares.techarchive.Dog@1b28cdfa }

Um auf die Variablen zugreifen zu können, sprechen wir einfach über das Objekt den Namen der Objektvariablen an.

public static void main(String[] args) { Dog bello = new Dog("Bello"); Dog killer = new Dog("Killer", 5); System.out.println(bello.name); // Bello System.out.println(killer.name); // Killer }

Auf diese Weise können wir nicht nur die Werte abgreifen, sondern auch verändern.

public static void main(String[] args) { Dog bello = new Dog("Bello"); Dog killer = new Dog("Killer", 5); System.out.println(bello.name); // Bello System.out.println(killer.name); // Killer System.out.println(bello.age); // 0 System.out.println(killer.age); // 5 bello.name = "Balu"; killer.name = "Rocky"; bello.age = 10; killer.age = 3; System.out.println(bello.name); // Balu System.out.println(killer.name); // Rocky System.out.println(bello.age); // 10 System.out.println(killer.age); // 3 }

Die beiden Objekte haben jeweils eine Objektvariable für den Namen und eine für das Alter. Die Objektvariablen sind abhängig von einem konkreten Objekt und somit auch dessen Werte. Werden die Werte eines Objekts verändert, hat dies keine Auswirkungen auf die Werte eines anderen Objekts.

Zugriff auf Objektvariablen und Objektmethoden

Aber wollen wir wirklich, dass die Werte der Objekte von überall einfach so verändert werden können? Nein! Lösung: Wir setzen den Modifier der Objektvariablen auf private.

private String name; private int age;

Jetzt können die Werte nicht mehr einfach so verändert werden, da sie nur noch innerhalb der Klasse Dog sichtbar sind.

public static void main(String[] args) { Dog bello = new Dog("Bello"); Dog killer = new Dog("Killer", 5); System.out.println(bello.name); // No access System.out.println(killer.name); // No access System.out.println(bello.age); // No access System.out.println(killer.age); // No access bello.name = "Balu"; // No access killer.name = "Rocky"; // No access bello.age = 10; // No access killer.age = 3; // No access System.out.println(bello.name); // No access System.out.println(killer.name); // No access System.out.println(bello.age); // No access System.out.println(killer.age); // No access }

Nur haben wir jetzt auch keinen Zugriff mehr auf die eigentlichen Werte. Hier kommen die in Kapitel 10 – Klassen angesprochenen get()-Methoden zum Einsatz. In der main()-Methode müssen wir jetzt nur noch diese Objektmethoden über das Objekt aufrufen. Auch alle weiteren Objektmethoden können auf diese Weise aufgerufen werden.

public String getName() { return name; } public int getAge() { return age; }
public static void main(String[] args) { Dog bello = new Dog("Bello"); Dog killer = new Dog("Killer", 5); System.out.println(bello.getName()); // Bello System.out.println(killer.getName()); // Killer System.out.println(bello.getAge()); // 0 System.out.println(killer.getAge()); // 5 bello.name = "Balu"; // No access killer.name = "Rocky"; // No access bello.age = 10; // No access killer.age = 3; // No access System.out.println(bello.name); // No access System.out.println(killer.name); // No access System.out.println(bello.age); // No access System.out.println(killer.age); // No access }

Für das Starten des Programms müssen jedoch die restlichen Zeilen mit den Errors entfernt oder auskommentiert werden. Andernfalls wird auch auf der Konsole, ein Error ausgegeben, dass die jeweiligen Objektvariablen nicht sichtbar sind. Abhängig von der IDE kann sich dieser jedoch unterscheiden.

Sollte gewünscht sein, die Werte der Objekte zu ändern, können die dafür vorgesehenen set()-Methoden verwendet werden. Der Wert muss dann beim Aufruf mit übergeben werden.

public void setName(String name) { this.name = name; }

Die toString()-Methode

Die toString()-Methode ist eine Objektmethode der Klasse Object. Da alle Objekte intern von dieser Klasse erben, ist es möglich, diese sowie viele andere Objektmethoden zu überschreiben – siehe Kapitel 14 – OOP: Polymorphismus.

public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }

Wird diese Objektmethode von einem Objekt aufgerufen, welche nicht die Objektmethode überschrieben hat, wird ein bestimmter String zurückgegeben. Der String setzt sich mit dem Namen der aufrufenden Klasse, einem @-Symbol und einer Adresse des Objekts – dem Hashcode in hexadezimaler Form mit 32 Bits kodiert – in folgender Art zusammen:

kesares.techarchive.Dog@65ab7765

Der Teil vor dem Klassennamen bezieht sich auf den Speicherort der Klasse im Projektverzeichnis. Unter der Referenzadresse ist dieses Objekt im Java-Heap abgelegt.

Der Garbage Collector

In Programmiersprachen wie Java spielt der Garbage Collector (GC) eine zentrale Rolle bei der Speicherverwaltung. Er sorgt dafür, dass nicht mehr benötigte Objekte aus dem Speicher entfernt werden, um Platz für neue Objekte zu schaffen und den Heap-Speicher effizient zu nutzen.

In einer Welt ohne GC, wie C oder C++, müsstest ihr selbst darauf achten, wann welche Objekte nicht mehr gebraucht und diese manuell aus dem Speicher entfernt werden. Klingt anstrengend, oder? In Java und in vielen anderen Programmiersprachen übernimmt diese Aufgabe der Garbage Collector für euch.

Was ist der Heap-Speicher?

Der Heap ist ein Bereich des Arbeitsspeichers, der zur Laufzeit dynamisch Speicherplatz für Objekte bereitstellt. Immer wenn ein neues Objekt erstellt wird, wird dafür Platz im Heap reserviert. Da der Speicher im Heap begrenzt ist, ist es wichtig, dass nicht mehr benötigte Objekte freigegeben werden, um Speicherlecks zu vermeiden und die Leistung der Anwendung zu erhalten.

Wie funktioniert der Garbage Collector?

Der Garbage Collector verfolgt, welche Objekte im Heap noch genutzt werden und welche nicht mehr erreichbar sind. Dies geschieht typischerweise durch Algorithmen wie Mark-and-Sweep oder Generational Garbage Collection.

  • Mark-and-Sweep: Der GC markiert alle Objekte, die noch von aktiven Threads oder anderen Objekten referenziert werden. Danach „fegt“ er den Heap, indem er alle nicht markierten Objekte entfernt und den freigewordenen Speicher zurückgibt.

  • Generational Garbage Collection: Diese Methode unterteilt den Heap in verschiedene Generationen (Young Generation, Old Generation, Permanent Generation). Objekte, die gerade erst erstellt wurden, landen in der Young Generation und werden dort häufiger überprüft. Objekte, die länger leben, werden in die Old Generation verschoben, wo sie seltener überprüft werden. Objekte, die während der gesamten Laufzeit des Programms existieren, werden in die Permanent Generation verschoben. Dies ist effizient, da viele Objekte nur kurzzeitig existieren und schnell wieder entfernt werden können.

Wie erkennt der Garbage Collector ungenutzte Objekte?

Der GC identifiziert zerstörbare Objekte, indem er den Referenzgraphen der Objekte analysiert. Wenn ein Objekt keine Referenzen mehr hat, also von keinem anderen Objekt oder aktiven Thread mehr erreicht werden kann, wird es als "garbage" (Müll) betrachtet und zur Entfernung freigegeben. Dies geschieht automatisch, ohne dass der Entwickler sich aktiv um die Speicherfreigabe kümmern muss.

Speichermanagement in C/C++

In Sprachen wie C und C++ liegt die Speicherverwaltung vollständig in der Hand des Entwicklers. Hier gibt es keinen automatischen Garbage Collector. Stattdessen muss der Entwickler den Speicher manuell verwalten – Speicher für Objekte reservieren und ihn nach Gebrauch explizit wieder freigeben.

Diese manuelle Verwaltung ermöglicht eine sehr feinkörnige Kontrolle über den Speicher, was in einigen Szenarien, wie bei der Systemprogrammierung oder Spielen, Vorteile in Bezug auf Leistung und Speicheroptimierung bieten kann.

Die manuelle Speicherverwaltung ist jedoch anfälliger für Fehler wie Speicherlecks oder Dangling Pointers (Referenzen auf Speicher, der bereits freigegeben wurde). Solche Fehler können schwerwiegende Abstürze oder Sicherheitslücken verursachen.

Im Vergleich dazu bietet der Garbage Collector in Java und ähnlichen Sprachen den Vorteil, dass der Entwickler sich nicht um die Speicherfreigabe kümmern muss, wodurch das Risiko von Speicherfehlern stark reduziert wird. Allerdings kommt dies oftmals mit einem kleinen Leistungseinbruch daher, da der Speicherverbrauch regelmäßig analysiert und bereinigt werden muss.

Java vs CPP Memory

Komplexität von Referenzdatentypen

Wie wir am Beispiel unserer Dog-Klasse gesehen haben, können Referenztypen auch andere Referenztypen als Eigenschaften besitzen. Der Umfang eines Objekts oder einer Klassenhierarchie kann dabei so flexibel wie die Anforderungen der jeweiligen Anwendung sein – von schlicht und einfach bis hin zu regelrechten Code-Monstern.

Stellen wir uns beispielsweise ein Motor-Objekt vor. Dieses könnte durchaus ein weiteres Objekt, sagen wir Cylinder, als Eigenschaft haben. Und weil sich Zylinder gerne in Gesellschaft von Schrauben befinden, könnte dieses Cylinder-Objekt wiederum ein Array von Screw[]-Objekten beinhalten, um alles schön zusammenzuhalten.

Um einmal ganz kurz das Thema der Vererbung vorwegzunehmen, könnte es in einem anderen Szenario notwendig sein, eine Anwendung zu entwickeln, die Lebewesen voneinander unterscheidet. Hier könnte man eine Klassenhierarchie aufbauen, in der Animal und Human von einer übergeordneten Klasse Creature erben, während Dog von Animal abstammt. In einer anderen, weniger komplexen Anwendung, die sich ausschließlich mit Menschen befasst, könnte man es einfach bei einer Human-Klasse belassen, ohne tiefer in die Stammesgeschichte abzutauchen.

Kurz gesagt: Die Komplexität und Struktur der Klassen hängt ganz von den Bedürfnissen und Anforderungen eurer Anwendung ab. Braucht ihr eine einfache Lösung, reichen vielleicht ein paar grundlegende Objekte. Wird es komplizierter, könnt ihr eine detailliertere Klassenhierarchie aufbauen und eure Referenztypen nach Herzenslust schachteln. Es ist ein bisschen wie beim Kochen: Manchmal reicht ein Sandwich und manchmal wird es ein Fünf-Gänge-Menü – es kommt ganz darauf an, was ihr auf den Tisch bringen wollt.

Last modified: 19 August 2024