5.2 Objektorientierung in Java

5.2 Objektorientierung in Java

Duke in 3D

Stefan Schneider Tue, 08/24/2010 - 17:57

5.2.1 Javaklassen und -objekte

5.2.1 Javaklassen und -objekte

Bisher wurden Klassen nur benutzt um mit Methoden zu arbeiten. Die main() Methode wurde immer als Hauptprogramm genutzt um Methoden aufzurufen. Klassen sind jedoch Strukturen die auch Variablen und Konstanten in Form von Attributen aufnehmen können.

Javaklasse

Javaklassen bestehen aus

 

Im folgenden Beispiel wird ein Kraftwagen modelliert:

class Kraftwagen {
   public String nummernschild;
   public double leergewicht;
   public int neupreis;

   /**
   * Ein selbstgewählter Konstruktor
   * @param kennzeichen Das Kennzeichen des Fahrzeugs
   */
   public Kraftwagen(String kennzeichen) {
       nummernschild = kennzeichen;
   }

   public void drucken() {
      System.out.println("Nummernschild: " + nummernschild);
      System.out.println("Leergewicht: " +  leergewicht );
      System.out.println("Neupreis: " +  neupreis );
   }
}

Die Klasse Kraftwagen unterscheidet sich in einer Reihe von Aspekten von den bisher benutzten Klassen:

  • Sie besitzt keine main() Methode. Sie ist nicht als Hauptprogramm ausführbahr.
  • die drucken() Methode is nicht als static deklariert
  • die Klasse verfügt über Attribute

Die Klasse Kraftwagen kann nun von anderen Klassen verwendet werden:

class Fuhrpark{
   public static void main( String[] args ) {
      Kraftwagen wagen1 = new Kraftwagen("M-O-4711");
      Kraftwagen wagen2 = new Kraftwagen("MA-Y-11");
      Kraftwagen wagen3 = new Kraftwagen("S-AY-7");
      wagen1.leergewicht = 890.0d;
      wagen1.neupreis = 20000;
   
      wagen2.leergewicht = 1050.0d;
      wagen2.neupreis = 15000;

      wagen3.leergewicht = 1250.0d;
      wagen3.neupreis = 28000;

      wagen1.drucken();
      wagen2.drucken();
      wagen3.drucken();

   }
}

Die Methode main() der Klasse Fuhrpark legt hier drei Objekte (Instanzen) der Klasse Kraftwagen an. Die Attribute der drei Objekten können mit individuellen Werten belegt werden. Die Attribute wagen1wagen2 und wagen3 nennt man Objektvariablen. Objektvariablen können auf Objekte eines bestimmten Typs zeigen. Im Beispiel oben können sie auf ein beliebiges Objekt vom Typ Kraftwagen zeigen.

Die Beziehung zwischen einer Klasse und ihren Instanziierungen, den Objekten beschreibt man in UML wie rechts zu sehen ist.

Instanzen

Erzeugen von Objekten

Die Instanzen von Klassen, also die Objekte, haben einen anderen Lebenszyklus als normale Variablen mit einfachen Typen. Instanzen (Objekte) können auch ausserhalb eines Blocks oder einer Methode existieren. Sie liegen nicht auf dem Stapel (Stack). Sie werden dynamisch im (prozess-)globalen Adressraum angelegt und sie bestehen so lange sie von Objektvariablen referenziert werden.

Mit dem new() Operator werden neue Objekte von Klassen angelegt. Beim Anlegen eines Objekts werden alle Attribute durch die Konstruktoren initialisiert. Im Beispiel oben wird ein Konstruktor mit einem Übergabeparameter benutzt. In diesem Fall wird der selbst definierte Konstruktor mit einem Parameter der Klasse zur Initialisierung aufgerufen. Existiert dieser Konstruktor nicht, so wird ein Standardkonstruktor aufgerufen.

Der Standardkonstruktor initialisiert alle Basistypen auf Nullwerte. Nach der Initialisierung steht das Objekt zur Verfügung und belegt Platz im Prozessspeicher.

Die erzeugten Objekte liegen auf dem "Heap", dem Freispeicher. Im Gegensatz zu Variablen können mehrere Referenzvariablen auf das gleiche Objekt zeigen wie im folgenden Beispiel zu sehen ist:

Kraftwagen wagen1 = new Kraftwagen ("M-O-4711");
Kraftwagen wagen2 = wagen1;
Kraftwagen wagen3 = new Kraftwagen("S-AY-7");

Benutzen der Objektvariablen und Methoden (Punktoperator)

Wie im Beispiel oben gezeigt kann man in der Klasse Fuhrpark mit der Objektvariablen wagen1 auch auf die Attribute und Methoden der Klasse Kraftwagen zugreifen. Hierzu dient der Punktoperator:

      ...
      wagen1.leergewicht = 980.0d;
      wagen1.drucken();
      ...

Man kann mit dem Punktoperator

  • auf Attribute (hier leergewicht) zugreifen oder man kann
  • Methoden (hier drucken() ) aufrufen.

Information Hiding in Java mit Hilfe des Schlüsselworts "private"

Java erlaubt es den Zugriff auf Methoden und Variablen mit Hilfe des Schlüsselworts private zu beschränken. Ist eine Objektvariable oder eine Methode mit private gekennzeichnet, so kann man sie nur innerhalb der Klasse benutzen. Der Zugriff ist dann nur noch über öffentlich zugängliche Methoden möglich.

Mit dem Schlüsselwort public wird der öffentliche Zugriff explizit gewährt. Ein Schlüsselwort das den Zugriff regelt ist protected. Es wird später behandelt.

Im folgenden Beispiel sieht man wie man die Variable neupreis der Klasse Kraftwagen vor dem Zugriff der Klasse Fuhrpark schützt. Die Methode getNeupreis erlaubt den Zugriff von ausserhalb der Klasse.

class Kraftwagen {
   public String nummernschild;
   public double leergewicht;
   private int neupreis;

   public int getNeupreis() { return neupreis;}

   ...
}

In UML werden private Attribute mit einem vorgestellten Minus "-" gekennzeichnet. Öffentliche Attribute erhalten ein vorgestelltes Plus "+":

Klassenvariablen

  • Klassenvariablen sind Variablen die zur Klasse gehören und nicht zu einem einzelnen Objekt.
  • Klassenvariablen sind Variablen die für alle Objekte einer Klasse gelten.
  • Klassenvariablen können auch ohne die Existenz von Objekten einer Klasse verwendet werden
  • Klassenvariablen werden mit dem Schlüsselwort static gekennzeichnet.

Beispiel:

class Fuhrpark{
   static int anzahlFahrzeuge;
   public static void main( String[] args ) {
      int a = Fuhrpark.anzahlFahrzeuge;   // Aufruf ausserhalb der Klasse Fuhrpark
          a = anzahlFahrzeuge;            // Aufruf innerhalb der Klasse Fuhrpark
      ...
   }
}

Hinweis: Die "normalen" nicht mit static gekennzeichneten Attribute einer Klasse nennt man zur Unterscheidung auch Instanzvariablen, da sie nur im Kontext einer Instanz (Objekt) existieren können.

Klassenmethoden

Klassenmethoden werden ebenfalls mit dem Schlüsselwort static gekennzeichnet. Sie können wie normale Methoden im Kontext von Objekten aufgerufen werden.

Unterschied zu normalen Methoden: Sie können jedoch auch ohne die Existenz von Objekten benutzt werden.

Will man eine Methode einer Klasse ohne eine existierende Instanz aufrufen, muss die Methode vorher mit static gekennzeichnet worden sein.

Im Falle der Klasse Fuhrpark kann man sie zum Beispiel zum Auslesen der Anzahl der Fahrzeuge benutzen:

class Fuhrpark{
   private static int anzahlFahrzeuge;
   public  static int getAnzahlFahrzeuge() {return anzahlFahrzeuge;}
      ...
   }
}

Die main() Methode sowie die Methoden der Klasse Math sind typische Methoden, die mit static deklariert sind, da sie ohne Objektinstanzen auskommen (müssen).

Stefan Schneider Tue, 08/24/2010 - 17:13

Anonymous (not verified)

Thu, 11/26/2015 - 13:04

Hier wird einmal der wagen1 als Objektvariable benannt und später in der Klasse Kraftwagen das Attribut nummernschild.
Was ist also eine Objektvariable?

Objektvariablen sind Variablen die auf Objekte zeigen. Man kann z.Bsp. mit dem Punktoperator den man auf eine solche Variable anwendet die Attribute und Methoden benutzen.

Objektvariablen können Attribute von Klassen sein. Sind die Attribute keine Objektvariablen so müssen es Basistypen oder Felder sein.

Objektvariablen zeigen auf Objekte.
Instanzvariablen sind Attribute einer Klasse die für jedes Objekt anders belegt werden können.
Klassenvariablen (static) sind Attribute einer Klasse die zwar auch veränderlich sind. Sie sind aber für alle Objekte(Instanzen) die gleichen. Sie gelten also für die Klasse und nicht für individuelle Objekte.

Anonymous (not verified)

Tue, 12/15/2015 - 22:26

Wieso macht es Sinn eine Referenzvariable auf das gleiche Objekt zeigen zu lassen ?

Die Frage habe ich mir nie gestellt. Sie ist aber recht interessant.
Hier ein Beispiel:
Meine Tochter (Instanz von Person) eines Kraftwagen's (Instanz von Kraftwagen) hält eine Referenzvariable auf das Fahrzeug weil Sie es ab zu einmal fährt. Sie interessiert typischerweise für den Tankinhalt (Attribut von KFZ) wenn er zu niedrig ist. Es sollte dann ja jemand tanken...
Der Familienvater (Instanz von Person) begleicht alle Rechnungen. Dazu benutzt er ein Feld (Array) mit Zeigern auf alles das etwas kostet.
Beide Mitglieder der Familie (Zwei Objekte) haben Zeiger auf das KFZ da sie verschiedene Attribute des Fahrzeugs benutzen.
Es kann jetzt zum Beispiel vorkommen, dass das Fahrzeug umgemeldet werden muss und ein neues Kennzeichen erhält. Der Zeiger beider Personen auf das Fahrzeug (Objekt) bleibt erhalten. Das Attribut mit dem Kennzeichen wird aber nur genau einmal geändert, da es das Kfz-Objekt nur einmal gibt.

5.2.2 this Referenz

5.2.2 this Referenz

Java verfügt über das Schlüsselwort this um auf die aktuelle Instanz innerhalb eines Methodenrumpfes zu referenzieren. Mit Hilfe der folgenden Notation kann man mit dem Schlüsselwort  this die Methoden und Attribute der eigenen Klasse referenzieren:

  • this.Attributname
  • this.methodenName()

Hinweis: Die this Referenz ist final. D.h. man kann ihr keinen anderen Wert zuweisen.

Es gibt eine Reihe von typischen Anwendungsfällen:

Einfache Selbstreferenz

Eine Methode einer Klasse soll die Instanz der Klasse einem Konsumenten bekannt machen. Zum Beispiel:

  • Selbst Hinzufügen bzw. Registrieren zu einem Addressbuch innerhalb des Konstruktors der Klasse Person
  • Methoden oder Attributaufruf (siehe Aufruf von setNachName(nn) im Konstruktor)
  • Rückgabe einer Referenz auf sich selbst (siehe setNachName() )
import class AddressBuch;
class Person {
   private String nachName;
   private String vorName;
...
   public Person (String vn, String nn) {
      this.setNachName(nn);
      vorName  = vn;
      Adressbuch.eintragen(this);
      ...
   }
   public Person setNachName(String nn) {
      nachName = nn;
      return this;
   }
}

Auflösen von Mehrdeutigkeiten

Die this Referenz erlaubt auch das Auflösen von Mehrdeutigkeiten innerhalb eines Namensraumes. Im folgenden Beispiel verdecken die Übergabeparameter nachName, vorName im Konstruktor der Klasse Person die gleichnamigen Attribute:

import class AddressBuch;
class Person {
   private String nachName;
   private String vorName;
...
   public Person (String vorName, String nachName) {
      this.nachName = nachName;
      this.vorName  = vorName;
      Adressbuch.eintragen(this);
      ...
   }
}

Aufruf alternativer Konstruktoren mit this()

Das Schlüsselwort this kann auch als Methodenaufruf this() verwendet werden. Dies erlaubt den Aufruf von Konstruktoren der eigenen Klasse.

Diese Technik ist sehr nützlich um bei mehreren Konstruktoren die Replikation von Code zu vermeiden. Dies geschieht im folgenden Beispiel der Klasse Person() bei einem Konstruktor ohne Parameter. Er benutzt den Konstruktor mit Parameter zum Setzen des Namens indem er einen Standardnamen verwendet:

import class AddressBuch;
class Person {
   private String nachName;
   private String vorName;
...
   public Person (String vorName, String nachName) {
      this.nachName = nachName;
      this.vorName  = vorName;
      Adressbuch.eintragen(this);
      ...
   }
   public Person() {
      /* Rufe den Konstruktor Person(String vorName, String nachName) auf
         Dieser Kommentar ist das einzige Kommando welches vor einem this()
         Aufruf stehen darf! 
      */
      this("John","Doe");
   }
}

Der Vorteil dieser Programmiertechnik liegt in der Vermeidung von Redundanzen. Beim Aufruf des Konstruktors ohne Parameter wird durch den Aufruf des Konstruktors mit Name, Vorname zum Beispiel auch das Addressbuch gepflegt.

Stefan Schneider Sat, 09/11/2010 - 15:08

Anonymous (not verified)

Sat, 12/19/2015 - 12:22

Ich habe Schwierigkeiten zu verstehen, wieso man nur beim Aufruf des Konstruktors ohne Parameter, das Adressbuch weiter pflegt.
Wäre dies nicht auch bei einer ganz normalen Verwendung des Konstruktors der Fall gewesen ?

Hmm,
nur das unterste Beispiel besitzt zwei Konstruktiven. Ich gehe davon aus, dass es sich hierum handelt.
Da der Konstruktion mit zwei Parameter immer den Konstruktor ohne Parameter aufruft, wird der Code mit dem Pflegen des Adressbuchs immer aufgerufen wenn ein Konstruktor aufgerufen wird.
Beim Aufruf des Konstrukteurs ohne Parameter wird also auch das Adressbuch gepflegt (aber nicht nur).
Ich hoffe das hilft.
Beste Grüße

5.2.3 Konstruktoren (2)

5.2.3 Konstruktoren (2)

Die schon früher angesprochenen Konstruktoren sind eine wichtige Komponente der Datenkapselung, da man mit ihnen eine komplexe und korrekte Initialisierung von Objekten erzwingen kann.

Konstruktoren mit Parametern

Konstruktoren können wie Methoden Übergabeparameter enthalten. Sie werden vom new() Operator erfasst, der dann nach dem Initialisieren der Datenstrukturen den entsprechenden Konstruktor aufruft.

Punkt p1 = new Punkt (3.3D, 4.1D);

Hier wird ein Konstruktor der Klasse Punkt aufgerufen der folgende Methodenparameter hat

package s1.block5;

   class Punkt {
      private double x;
      private double y;

      public Punkt (double xKoord, double yKoord) {
         x = xKoord;
         y = yKoord;
      }
   }

Das Schreiben von Konstruktoren ist optional. Es können mehrere Konstruktoren implementiert werden. Alle Konstruktoren müssen sich jedoch in der Parameterliste unterscheiden. Hier zählt die Reihenfolge und Anzahl der Parametertypen, jedoch nicht der Name der Parametervariablen. Der Übersetzer braucht diese Information um die unterschiedlichen Konstruktoren auszuwählen.

Überladene Konstruktoren

Überladen von Methoden und Konstruktoren
Das Implementieren von mehreren namensgleichen Methoden oder Konstruktoren mit unterschiedlichen Eingabe-Parameterlisten nennt man überladen.

Java unterscheidet die unterschiedlichen Methoden und Konstruktoren an den Eingabelisten der Parameter jedoch nicht am Rückgabeparameter!

Es kann sehr nützlich sein mehrere Konstruktoren zur Initialisierung einer Klasse zur Verfügung zu stellen wie man am Beispiel der Klasse Punkt sehen kann. Die Klasse Punkt erlaubt hier die folgenden Initialisierungsarten:

  • Initialisierung mit Nullpunkt(0,0)
  • Initialisierung mit x,y Koordinate
  • Initialisierung mit den Werten eines anderen Punkts
package s1.block5;

public class Punkt {
private double x;
private double y;

public Punkt (double xKoord, double yKoord) {
x = xKoord;
y = yKoord;
}

public Punkt (Punkt p) { this(p.x,p.y);}

public Punkt () { this(0,0); }

public static void main(String[] args) {
Punkt p1 = new Punkt(); // Initialisierung mit (0,0)
Punkt p2 = new Punkt(1.1D,2.2D); // Initialisierung mit (1.1,2.2)
Punkt p3 = new Punkt(p2); // Initialisierung mit (1.1,2.2) durch Punkt p2 }
}

Der Default-Konstruktor (ohne Parameter) wurde hier selbst implementiert. Er ruft mit Hilfe des Schlüsselworts this den Konstruktor mit den beiden Fliesskommazahlen als Parameter auf. Er initialisiert die Datenstruktur nicht selbst, sondern er delegiert die Initialisierung an einen anderen Konstruktor. Dies ist nicht notwendig aber üblich.

Der Konstruktor mit der Parameterliste Punkt verfährt ähnlich.

Aufrufe von Konstruktoren durch Konstruktoren (der gleichen Klasse)

Konstruktoren können andere Konstruktoren der gleichen Klasse mit this(parameter-liste) aufrufen.

Wichtig: Der this() Aufruf muss jedoch der erste Befehl in einem Konstruktor sein.

Ein optionaler this() Aufruf muss das erste Kommando im Codeblock des Konstruktors sein um die Integrität des mit this() aufgerufenen Konstruktors zu gewährleisten.

Ein Konstruktor ist die erste Methode die ein Objekt initialisiert. Das ist aber nicht mehr für den aufgerufenen Konstruktor geährleistet, wenn dem aufrufenden Konstruktor schon Objektmanipulationen vor dem this() Aufruf erlaubt sind.

Regeln zum Aufruf von Konstruktoren

Java stellt einen Standardkonstruktor (Default-Konstruktor) ohne Parameter zur Verfügung der alle Attribute mit Standardwerten belegt. Implementiert man eigene Konstruktoren gelten die folgenden Regeln für die Benutzer von Konstruktoren:

  • Wurde kein Konstruktor implementiert, generiert Java einen Default-Konstruktor ohne Parameter.
  • Wird mindestens ein Konstruktor selbst implementiert so generiert Java zur Laufzeit keinen Standardkonstruktor. Benutzer müssen einen der selbst implementierten Konstruktoren verwenden.
    • Wird ein Konstruktor ohne Parameter implementiert wird dieser anstatt eines Standardonstruktor benutzt, da er für den Anwender die gleiche Syntax hat.

Dieses Vorgehen ist notwendig um eine Datenkapselung zu erzwingen.

Bei einer Implementierung mit eigenen Konstruktoren aber ohne einen Default-Konstruktor führt der folgende Konstruktoraufruf zu einem Übersetzungsfehler:

class Punkt {
   private double x;
   private double y;

   public Punkt (double xKoord, double yKoord) {...}
   public Punkt (Punkt p) { this(p.x,p.y);}
...
}

...
Punkt p1 = new Punkt();        // Initialisierung mit Default-Konstruktor ist nicht möglich
...

Erklärung der von Java geforderten Semantik:

  • Hat ein Entwickler keinen Konstruktor implementiert, so war ihm die Initialisierung des Objekts nicht wichtig. 
    • Das Objekt wird von einem automatisch generierten Konstruktor mit Nullwerten initialisiert
    • Der Standardkonstruktor wird benutzt wie ein Konstruktor ohne Parameter
  • Hat der Entwickler mindestens einen Konstruktor selbst implementiert, so ist eine spezielle Initialisierung des Objekts gewünscht
    • Ein Konsument muss das Objekt mit Hilfe einer der selbstimplementierten Konstruktoren initialisieren
    • Würde das Java einen zusätzlichen Standardkonstruktor generieren könnte man die vom Entwickler gewünschte Initialisierung umgehen. Dies ist nicht gewünscht.

Verbieten des Instanziieren von Objekten einer Klasse

Java bietet die Möglichkeit einen Konstruktor als private zu deklarieren. Hiermit kann niemand ausserhalb der Klasse Instanzen erzeugen. Diese Möglichkeit erlaubt die Anzahl von Instanzen bei Bedarf genau zu kontrollieren. Ein Anwendungsfall ist ist das Benutzen einer statischen Methode und eines privaten Kontruktors:

class Punkt {
   private double x;
   private double y;

   public static Punkt createPunkt (double xKoord, double yKoord) {
      Punkt pp = new Punkt(xKoord, yKoord);
      return pp;
      }

   private Punkt (double xx, double yy) { x=xx; y=yy;}

}
...
Punkt p1 = Punkt.createPunkt(1.1D,2.2D); 
Punkt p2 = new Punkt(4.4D,5.5D);      // Fehler!!
...

Ein anderer Anwendungsfall ist die Benutzung des Entwurfsmodell "Singleton" d.h. einer Klasse von der es genau eine Instanz gibt. Ein Beispiel ist die Klasse Addressbuch:

package s1.block5;

public class Adressbuch {
private static Adressbuch myInstance;

private Adressbuch() {
// Initialisiere Adressbuch
}

public static Adressbuch getAdressbuch() {
if (myInstance == null) myInstance = new Adressbuch();
return myInstance;
}

public static void main(String[] args) {
Adressbuch ab = getAdressbuch();
Adressbuch cd = getAdressbuch();
}
}

Mit dieser Implementierung wird beim ersten Anfordern eines Adressbuchs genau einmal ein Adressbuch angelegt.

Stefan Schneider Sat, 09/11/2010 - 15:11

5.2.4 Ablauf der Initialisierung einer Klasse

5.2.4 Ablauf der Initialisierung einer Klasse

 Begriffsbestimmung:

Instanziierung einer Klasse
Erzeugen eines neuen Objekts einer Klasse auf dem Java Heap (Freispeicher)

 

Initialisierung (von Datenfeldern)
Das Belegen von existierenden Datenfeldern mit Standardwerten oder mit von Konstruktoren individuell implementierten Werten

Beim Erzeugen eines Javaobjekts wie zum Beispiel einer Instanz der Klasse Punkt:

class Punkt {
   private double x;
   private double y;

   public Punkt (double xKoord, double yKoord) {
      x = xKoord;
      y = yKoord;
      }
}
...
Punkt p2 = new Punkt(1.1,2.2);
...

Mit der Variable p2 l laufen die folgenden Schritte beim Aufruf der Programmzeile Punkt p2=new Punkt() ab:

  1. Die Referenzvariable p2 wird angelegt und mit dem Wert null belegt
  2. Der new Operator der Klasse Punkt wird aufgerufen. Auf dem Java-Heap wird der Platz für ein Objekt der Klasse Punkt zur Verfügung gestellt. Es wird instanziiert.
  3. Die Datenfelder des Objekts werden initialisiert. Dies geschieht je nach Typ mit den Belegungen: null, false, 0, 0f etc.
  4. Der Methodenblock des entsprechenden Konstruktors wird ausgeführt.
  5. Der new Operator gibt die Referenz auf das Objekt zurück welches in p2 gespeichert wird.
Stefan Schneider Sat, 09/11/2010 - 15:12