Universität Hamburg - Fachbereiche - Fachbereich Mathematik

Java-Kurs (6)

WiSe 01/02

Bodo Werner

Zurück zum Inhaltsverzeichnis.

6. Klassen und Objekte (Teil 1)

class, new

Jetzt wollen wir uns der OOP-Konzeption nähern. Der zentrale Begriff ist der einer Klasse (class). Bisher haben wir nur Appliktionsklassen erstellt (die man an der main()-Methode erkennt und die hierdurch ausführbar werden). Von diesen will man nie "Instanzen" (Objekte) schaffen, sie sind Beispiele statischer Klassen, deren sämtliche Methoden und Variable den Zusatz static tragen.

Die Klassen, die wir jetzt besprechen wollen, sind neue Datentypen, die die ProgramiererIn selbst schafft, oder die ein von "Profis" zur Verfügung gestelltes package anbietet. Variable vom Typ einer Klasse heißen Objekte, sie erhalten durch ihre Deklaration einen Bezeichner. Zu einer solchen (nicht statischen) Klasse gehört immer ein Konstruktor, der ihre Objekte initialisiert. Eine Klasse hat gewisse Daten (auch Attribute oder Variable der Klasse genannt) und verfügt über Methoden. Wenn die Klasse nicht den Zusatz static besitzt, also keine statische Klasse ist, kann man eine Instanz (ein Objekt) dieser Klasse mit Hilfe des new-Operators und des Konstruktors initialisieren (allokieren). Danach kann auf die Daten und die Methoden des Objektes mit Hilfe der Punktnotation zugegriffen werden.

Wir kennen dies im Prinzip schon für die klassenverwandten Datentypen Zeichenkette (String) und Feld: So wird ein double-Feld z.B. durch double[] Vektor1=new double[6]; deklariert und initialisiert. Seine Länge (6) kann durch Vektor1.length ermittelt werden, d.h. length ist der Name eines Attributs aller Objekte vom Typ eines Feldes. Entsprechend haben wir gesehen, dass equals(), indexOf() und length() Methoden von Variablen ("Objekten") des Typs String bezeichnet.

6.1 Die Klasse "KomplexeZahl"

Auf den mathematischen Hintergrund komplexer Zahlen werde ich in der Vorlesung eingehen. Festhalten will ich hier nur, dass eine komplexe Zahl durch zwei reelle Zahlen, nämlich ihren Real- und ihren Imaginärteil charakterisiert ist. Komplexe Zahlen kann man multiplizieren, addieren, man kann durch von Null verschiedene komplexe Zahlen dividieren: sie gehorchen denselben Rechenregeln wie die reellen Zahlen, sie bilden einen Körper.

Die Daten (Attribute, Variable) von Objekten einer Klasse, die komplexe Zahlen darstellt, werden also sinnvollerweise Real- (Re) und Imaginärteil (Im) sein (wie verwenden gegen die Konvention große Anfangsbuchstaben). In der darauf folgenden Applikationsklasse Komplex1 wird eine komplexe Zahl als Objekt der Klasse KomplexeZahl1 durch Übergabe des Real- und des Imaginärteils an den Konstruktor konstruiert. Spätere Erweiterungen dieser Klasse werden KomplexeZahl2, KomplexeZahl3, ... heißen.

Wir wollen der Klasse noch mit einer Methode versehen, die feststellt, ob die komplexe Zahl reell, d.h., ob ihr Imaginärteil null ist (da man wegen Rundungsfehler double-Zahlen nicht auf null hin prüfen sollte, sind wir etwas großzügiger in Verteilung des Etiketts reell).


class KomplexeZahl1{
  //Daten:
  double Re, Im;//gegen Konvention mit grossen Anfangsbuchstaben
  //Konstruktor:
  KomplexeZahl1(double x, double y){
    Re=x; Im=y; 
  }//Ende Konstruktor
  boolean reell(){
    boolean w=false;
    if (Math.abs(Im)<1E-12) w=true;
    return w;
  }//Ende reell()
}//Ende class KomplexeZahl1

Diese Klasse kann zwar kompiliert, aber nicht ausgeführt werden, da es sich nicht um eine Applikationsklasse handelt (sie enthält keine main()-Methode). Eine Applikationsklasse, die ein Objekt vom Typ KomplexeZahl1 instanziert, könnte so aussehen:


class Komplex1{
    static void main(String[] args){
	KomplexeZahl1 z = 
           new KomplexeZahl1(1.1, 2.3);
    boolean w=z.reell();//false
    }//Ende main()(
}//Ende class Komplex1
Die wichtigste Zeile ist die, in der der Konstruktor in Verbindung mit dem new-Operator eingesetzt wird, um ein Objekt namens z zu konstruieren. Der Bezeichner des Konstruktors muss stets identisch mit dem Namen der zugehörigen Klasse sein. Die Methode reell() kann man als eine Anfrage an das Objekt z verstanden werden, ob es als reelle Zahl aufgefasst werden kann.

Die imaginäre Einheit i hätte man mit KomplexeZahl1 i = new KomplexeZahl1(0, 1); konstruieren können.

Jetzt geben wir eine Applikationsklasse an, die eine Methode vorsieht, die komplexe Zahlen miteinander multipliziert und eine solche Multiplikation auch für konkrete Zahlen ausführt:


class Komplex2{
    static KomplexeZahl1 mal(KomplexeZahl1 z1, KomplexeZahl1 z2){
      double w1, w2;
      w1=z1.Re*z2.Re-z1.Im*z2.Im;
      w2=z1.Re*z2.Im+z1.Im*z2.Re;
      return new KomplexeZahl1(w1,w2);
    }//Ende mal()
    static void main(String[] args){
	KomplexeZahl1 z1 = 
           new KomplexeZahl1(1.1, 2.3);
        KomplexeZahl1 z2 = 
           new KomplexeZahl1(1.1, -2.3);
        KomplexeZahl1 w=mal(z1,z2);
        if (w.reell()) System.out.println("z1*z2 ist reell");
    }//Ende main()(
}//Ende class Komplex2
Beachten Sie, dass sowohl die beiden Parameter als auch der Rückgabewert der Methode mal() vom Typ der Klasse KomplexeZahl1 sind. Die Rückgabe kann ohne Deklaration einer Variable diesen Typs erfolgen.
Im main()-Block wird das Produkt w nicht durch einen Konstruktor initialisiert, sondern durch Ausführung der Methode mal(). Achten Sie auf die Punktnotation, z.B. in z1.Re, wenn auf die Daten eines Objektes zugegriffen werden soll. Auf den Imaginärteil des Produktes kann man mittels w.Im, auf die Methode reell() mittels w.reell() zugreifen.

6.2 Konstruktoren

Bezeichner, Parameter, this

Der Konstruktor einer Klasse muss stets den gleichen Namen wie die Klasse haben. Ein Klassenblock muss zwar keinen Konstruktor enthalten (dann gibt es den Default-Konstruktor ohne Parameter), diesen Fall wollen wir aber in diesem Kurs ausschließen, wenn wir von statischen Klassen (wie z.B. die Applikationsklassen), für die Konstruktoren keinen Sinn machen, da nie eine Instanz dieser Klasse gebildet werden soll, absehen. Konstruktoren sind Methoden verwandt: sie haben i.a. Parameter, jedoch keinen Rückgabewert, ohne dass dies jedoch mit dem Zusatz void vermerkt wird. Wie bei Methoden kann man auch Konstruktoren überladen, d.h. es kann mehrere Konstruktoren geben. Diese müssen aber gleiche Namen haben, sich jedoch in der Parameterliste unterscheiden, s.u.

Zuweilen möchte man den Parametern eines Konstruktors den gleichen Namen geben wie sie die Daten der Klasse haben. Dies geht unter Verwendung des Schlüsselworts this:


class KomplexeZahl2{ 
  //Daten: 
  double Re, Im; 
  //Konstruktor: 
  KomplexeZahl2(double Re, double Im){ 
     this.Re=Re; 
     this.Im=Im; 
  }//Ende Konstruktor 
  boolean reell(){
      return (Math.abs(Im)<1E-12);  
  }//Ende reell()
}//Ende class KomplexeZahl2 
Die Verwendung von this bedarf einer Erklärung: Wenn die Bezeichner von Parametern von Konstruktoren mit denen von Daten der Klasse identisch sind, haben die Parameter als lokale Parameter Vorrang. Um auf die Daten zugreifen zu können, muss man this in der Punktnotation voranstellen, welches stellvertretend für den Bezeichner eines später zu konstruierenden Objektes steht.

6.3 Zugriffsrechte (Sichtbarkeit) Teil 1

public, private

Klassen, Daten, Methoden und Konstruktoren enthalten in der Regel einen vorgestellten Zusatz (Modifizierer oder (engl.) Modifier genannt), der die Zugriffsrechte bestimmt, die eine Rolle spielen, wenn in anderen Klassen, z.B. in Applikationsklassen, Objekte der betreffenden Klasse instanziert werden und auf Daten und Methoden des Objekts zugegriffen werden soll.

Will man z.B. ausschließen, dass ein Objekt namens z der Klasse KomplexeZahl1 nachträglich z.B. durch die Anweisungen z.Re=-7.12 verändert wird, sollte man dies dadurch verhindern, dass Daten den Zusatz private erhalten und sie damit für andere Klassen nicht direkt zugänglich (unsichtbar) gemacht werden:

class KomplexeZahl
.......
//Daten:
private double Re, Im;
......
Allerdings ruft dann die Zeile System.out.println("Re z="+z.Re+" Im z="+z.Im); in einer Applikationsklasse die Compiler-Fehlermeldung Variable Re in class KomplexeZahl not accessible from class Komplex2 hervor. Will man wirklich den direkten Zugriff auf die Daten Re, Im verbieten, aber doch zulassen, dass sie gelesen werden können, muss man weitere öffentliche Methoden in der Klasse KomplexeZahl zur Verfügung stellen, etwa so
//KomplexeZahl3
......
private double Re, Im;
......

//Methoden:
....
public double getRe(){
return Re;
}
....
}//Ende class
Jetzt erreicht die folgende Applikationsklasse das gleiche Ziel wie die Klasse Komplex2:
class Komplex3{
  static void main(String[] args){
    KomplexeZahl3 z = 
       new KomplexeZahl3(1.1, 2.3);
    System.out.println("Re z="+
    z.getRe()+" Im z="+z.getIm());
  }//Ende main()
}//Ende class Komplex3
Auf jeden Fall sollte die Klasse KomplexeZahl und ihr Konstruktor öffentlich (public) sein, was schon dadurch erreicht wird, dass der Zusatz weggelassen wird (wie bisher). Das bedeutet, dass jede andere Klasse ein Objekt des Typs KomplexeZahl initialisieren und seine öffentlichen Methoden benutzen kann.

Ein wesentlicher Vorteil bei einer Einschränkung von Zugriffsrechten bzw. der Sichtbarkeit ist, dass der Benutzer sich nur die öffentlichen Daten, Klassen und Methoden merken muss. Will man z.B. von den ungeheuer mächtigen Grafikklassen von Java Gebrauch machen, muss man in jedem Detail die Bezeichner der öffentlichen Klassen, ihrer öffentlichen Daten und Methoden, aber auch den Typ dieser Daten und die Paramterliste dieser Methoden kennen, im Zweifelsfall nachschlagen oder auswendig lernen. Die intern vom Programmierer verwendeten nicht öffentlichen Klassen, Daten und Methoden interessieren nicht!

Neben private, public ist noch der Zusatz protected wichtig. Er tritt aber nur in Verbindung mit Unterklassen auf und erlaubt die Sichtbarkeit innerhalb aller Unterklassen.

6.4 Die Klasse "Winkel", Polarkoordinaten einer komplexen Zahl und ein zweiter Konstruktor der Klasse "KomplexeZahl"

Komplexe Zahlen sind nicht nur durch ihre Real- und Imaginärteile, sondern auch durch ihre Polarkoordinaten (Betrag und Winkel) bestimmt. Die Winkel werden im Bogenmaß gemessen und können auf das Intervall [0, 2*PI) beschränkt werden. Ich führe jetzt eine etwas künstlich anmutende Klasse Winkel mit dem Attribut double w (dem eigentlichen Winkel im Bogenmaß) ein, dessen Konstruktor dafür sorgt, dass w in [0, 2*PI) liegt:
class Winkel{
 private double phi;
 Winkel(double a){
   while (a>= 2*Math.PI) a-=2*Math.PI;
   while (a<0) a+=2*Math.PI);
   phi=a;
 }//Ende Konstruktor Winkel()
 public double getphi(){
    return phi;
 } 
}//Ende class Winkel
Jetzt können wir einen zweiten Konstruktor in der Klasse KomplexeZahl vorsehen, dem Polarkoordinaten übergeben werden und der den ersten Konstruktor überlädt:
class KomplexeZahl4{
  private double Re, Im;
  //Konstruktor 1
  KomplexeZahl4(double x, double y){
     Re=x; Im=y;
  } 
  //Konstruktor 2
  KomplexeZahl4(double r, Winkel W){
    double phi=W.getphi();
    Re=r*Math.cos(phi);
    Im=r*Math.sin(phi);
  }
  public double getRe(){
  ...
  }
 }//Ende class KomplexeZahl4
Beachten Sie, dass beide Konstruktoren den gleichen Namen haben, dass sich ihre Parametertypen aber unterscheiden. Hätte ich nicht die Klasse Winkel eingeführt, sondern einen Winkel als einfache double-Zahl angesehen, hätten die beiden Konstruktoren jeweils zwei double-Parameter und hätten nicht unterschieden werden können. Das kann natürlich nicht ausschlaggebender Grund sein, die Klasse Winkel einzuführen.
class Komplex4{
    static KomplexeZahl4 mal(KomplexeZahl4 z1, KomplexeZahl4 z2){
	double w1, w2;
        w1=z1.getRe()*z2.getRe()-z1.getIm()*z2.getIm();
        w2=z1.getRe()*z2.getIm()+z1.getIm()*z2.getRe();
        return new KomplexeZahl4(w1,w2);
    }//Ende mal()
  
   static void main(String[] args){
	double ph=1.2*Math.PI; 
        Winkel w=new Winkel(ph);
        double r=2.2;
	KomplexeZahl4 z = new KomplexeZahl4(r,w);//2.Konstruktor
        System.out.println("Re(z)="+z.getRe()+" Im(z)="+z.getIm());
    }//Ende main()
}//Ende class Komplex4

6.5 Die Klasse Math

Wir haben ganz pragmatisch mathematische Funktionen (Wurzel, Potenz, sin, cos) sowie die Kreiszahl PI durch Voranstellen von Math mit einem anschließenden Punkt verwendet. Dabei sind sqrt(), pow(), sin(), cos() Methoden der von Java zur Verfügung gestellten (statischen) Klasse Math, während PI eine Konstante dieser Klasse ist. Weitere Methoden der Klasse Math sind abs() (Betragsfunktion), tan(), atan() (arcustangens), min(), max() (Minimum, Maximum von zwei Zahlen), exp() (Exponentialfunktion), log() (natürlicher Logarithmus) und wenige mehr.

Weiter mit Klassen und Objekte (Teil 2).

(Die abstrakte Klasse Funktion)