next up previous contents index
Nächste Seite: Vererbung Aufwärts: Programmieren in Java Vorherige Seite: Ein Streifzug durch Java   Inhalt   Index

Unterabschnitte

Klassen und Objekte

Klassen sind Strukturen aus Daten und Methoden.


   public class Kreis
   { public double x,y;  // Koordinaten
     public double r;    // Radius

     public double umfang()
     { return 2*3.14159*r;
     }
     public double fläche()
     { return 3.14159*r*r;
     }
   }

Klassen dienen (meist) dazu, den Inhalt und die Fähigkeiten von Objekten zu beschreiben.

Objekte sind Instanzen von Klassen.

Sie leben z.B. in Variablen von geeignetem Typ (siehe Seite [*]).


    Kreis c;

und werden grundsätzlich dynamisch erzeugt:


    c = new Kreis();


Zugriff auf Objekte-Daten durch Feld-Selektion:


    Kreis c = new Kreis();
    c.x = 2.0;

Aufruf von Instanzmethoden durch Methoden-Selektion:


    Kreis c = new Kreis();
    double f;
    c.r = 2.7;
    f = c.fläche();

Technische Realisierung: Übergabe der Selbstreferenz this (siehe Seite [*]) an den Code, der fläche implementiert.

Konstruktoren


    Kreis c = new Kreis();

Ablauf:

Konstruktoren initialisieren Objekte.

Wichtige Eigenschaften:


   public Kreis (double x, double y, 
                  double r)
   { this.x = x;  // 'this' löst
     this.y = y;  // Namens-
     this.r = r;  // konflikt
   }
   ...
   Kreis c = new Kreis(1.4,2.0.5.3);


Der parameterlose Konstruktor


   Kreis();
heißt Default-Konstruktor. Er wird vom Compiler zur Verfügung gestellt, falls es keinen Benutzer-definierten Konstruktor gibt.



Der Compiler-generierte Default-Konstruktor ruft lediglich den Default-Konstruktor der Oberklasse auf.




Wir bekommen also Compiler-Fehlermeldungen,



Die Selbstreferenz this in Konstruktoren

this findet in Konstruktoren Anwendung, die andere Konstruktoren verwenden:


   public Kreis (double x, double y, 
                  double r)
   { this.x = x;
     this.y = y;
     this.r = r;
   }
   public Kreis(double r)
   { this(0.0, 0.0, r);
   }
   public Kreis(Kreis c)
   { this(c.x, c.y, c.r);
   }   
   public Kreis()
   { this(0.0, 0.0, 1.0);
   }

Andere typische Anwendungen von this siehe Seite [*] und Seite [*].

Solche Aufrufe anderer Konstruktoren stehen vor anderen Anweisungen im Konstruktorrumpf.



Solch ein expliziter Konstruktor-Aufruf über this

All dieses führt zu Compiler-Fehlermeldungen.


   public class Tisch
   { Bein[] beine;
     int anz_beine;
     public Tisch()
     { this(anz_beine-1); // FALSCH!
     }
     public Tisch(int n)
     { anz_beine = n;
     }
   }

Der Java-Compiler meldet:


   Can't reference anz_beine before the 
   superclass constructor has been called.
    { this(anz_beine-1);
           ^




Klassenvariablen

Klassenvariablen beschreiben im Gegensatz zu Instanzvariablen Eigenschaften der Klasse (siehe auch Seite [*]).

Klassenvariablen werden mit dem Schlüsselwort static deklariert:


   public class Kreis
   { static int anz_Kreise = 0;
     public double x,y;  // Koordinaten
     public double r;    // Radius
     public Kreis (double x, double y, 
                   double r)
     { this.x = x; this.y = y; this.r = r;
       anz_Kreise++;
     }
   }

Zugriff erfolgt außerhalb der Klassen qualifiziert mit dem Klassennamen:


    if (Kreis.anz_kreise == 12)
       System.out.println(``Hello'');

static-Felder werden außerdem benutzt für

(Konstanten siehe Seite [*])


Klassenmethoden

analog zu den Klassenvariablen:


   public class Kreis
   { public double x,y;  // Koordinaten
     public double r;    // Radius

     // Instanz-Methode:
     public Kreis bigger(Kreis k)
     { if (k.r > r) 
          return k;
       else return this;
     }

     // Klassen-Methode:
     public static Kreis bigger(Kreis k1, 
                                Kreis k2)
     { if (k1.r > k2.r) 
          return k1;
       else return k2;
     }
   }


Verwendung der beiden Methoden:


   Kreis a = new Kreis(12.3);
   Kreis b = new Kreis(3.414);
   Kreis c = a.bigger(b);
   Kreis c = Kreis.bigger(a,b);


Besonderheiten:




     // Klassen-Methode:
     public static Kreis bigger(Kreis k1, 
                                Kreis k2)
     { if (k1.r > k2.r && 
           x > 0.0)         // FALSCH! 
          return k1;
       else return k2;
     }

Java-Compiler:


   Can't make a static reference to 
   nonstatic variable x in class Kreis.
     { if (k1.r > k2.r && x > 0.0) 
                          ^



Initialisierer

Attribute können bei der Deklaration initialisiert werden. Dies gilt sowohl für

Instanzvariablen

als auch für

Klassenvariablen


   class Kreis
   { double r = 1.0;
     static int kreiszahl = 100;
     ...
   }

Instanzvariablen:

Initialisierung wird jedesmal bei Erzeugung eines Objektes ausgeführt.



Klassenvariablen:

Initialisierung wird nur einmal durchgeführt, wenn die Klasse geladen wird.


Initialisierung von Klassenvariablen


    class Rechteck
    { static float max_sv = 2.0;
      static float max_h = 10.0
      static float max_b = max_sv * max_h;
      ...
    }

Die Regeln:



Daher bekommen wir hiermit 2 Compiler-Fehlermeldungen:


    class FalscheStaticInits
    { static float f = j;     // VORWÄRTS!
      static int j = 1;
      public int max;
      static int k = max + 1; // INSTANZVAR!
      ...
    }



Für komplexere Initialisierungen, die nicht als einfacher Ausdruck geschrieben werden können, gibt es die statischen Initialisierungsblöcke:


   public class Kreis
   { static private double pi = 3.14;
     static private double[] sins 
                  = new double[1000];

     // Statischer Initialisierungsblock
     static
     { double x = 0.0;
       double delta_x = pi / 2 / (1000-1);
       int i;
       for (i=0; i < 1000; i++)
         { sins[i] = Math.sin(x);
           x += delta_x;
         }
     }
   }

Es darf mehrere solcher Blöcke geben. Sie werden behandelt wie ein einziger, der durch Hintereinanderhängung entsteht.


Initialisierung von Instanzvariablen


   public class Tisch
   { Bein[] beine;
     int anz_beine = 4;
   }

Hier gelten ähnliche Regeln wie bei den Klassenvariablen (Seite [*]):



Daher bekommen wir hiermit 2 Compiler-Fehlermeldungen:


    class FalscheInstanzInits
    { float f = j;     // VORWÄRTS!
      int j = 1;
      int k = k + 1;   // REKURSIV!
      ...
    }


Für komplexerer Initialisierung, die nicht als einfacher Ausdruck geschrieben werden können, gibt es die Instanz-Initialisierungsblöcke (ab Java1.1):


public class Tisch
{ String platte = "Holz";
  String[] beine;
  int anz_beine = 4;

  { int i;
    for (i = 0; i < anz_beine; i++)
      beine[i] = "Bein " + i;
  }
  
  public Tisch(String p_material)
    { platte = p_material;
    }
}

Es darf mehrere solcher Blöcke geben. Sie werden behandelt wie ein einziger, der durch Hintereinanderhängung entsteht.

Nun kennen wir alle Akteure, die bei der Erzeugung neuer Objekte eine Rolle spielen:


Objekterzeugung

Wann werden neue Objekte erzeugt?



Einschub: Die Klasse Class

Für

existiert zur Laufzeit eines Java-Programms ein Objekt der Klasse Class



Diese Objekte können nur von der virtuellen Maschine erzeugt werden, nicht vom Benutzer-Programm.



Zu jedem Objekt findet man das Klassenobjekt durch getClass() aus der Klasse Object:


   Class c = new Diesel().getClass();

Man kann nun z.B. den Klassennamen ausgeben:


   Class c = new Diesel().getClass();
   System.out.println(c.getName());

Nachzulesen in der API-Dokumentation zu java.lang.Class.



[Ende des Einschubs.]

Der Ablauf der Objekterzeugung:

1.
Speicherallokation
2.
Default-Initialisierung
3..
Konstruktion des Objektes


Speicherallokation

Reserviere einen Speicherbereich, groß genug für

Gelingt dies nicht, gibt es einen OutOfMemoryError-Fehler.



Gelingt es, werden anschließend alle Instanzvariablen (auch die der Oberklassen) des neuen Objektes mit ihren Default-Werten initialisiert.


Default-Initialisierung




Typ Inhalt Größe Default
boolean true oder false 1 bit false
char Unicode Character 16 bits \u0000
byte signed integer 8 bits 0
short signed integer 16 bits 0
int signed integer 32 bits 0
long signed integer 64 bits 0
float IEEE 754 floating point 32 bits 0.0
double IEEE 754 floating point 64 bits 0.0
Klasse, Interface, Array Referenz / null





null ist ein Ausdruck von einem speziellen Nulltyp, der keinen Namen hat.

null kann an jede Array-, Objekt- oder Interface-Variable zugewiesen werden.

Die Größe von Referenzen wird von der virtuellen Maschine als ein ``Wort'' definiert (i.d.R. 32 oder 64 bits).


Konstruktion von Objekten

Ablauf:




\psfig {figure=bilder/okonstr.ps}


Objektzerstörung

Nicht unter der Kontrolle des Benutzerprogramms sondern durch einen automatischen Garbage Collector.

Der weiß zur Laufzeit, welche Objekte existieren und prüft, auf welche Objekte nicht mehr verwiesen wird.




Technisch: Low-priority-thread, der im Hintergrund mitläuft. Nutzt z.B. die Zeiten des Wartens auf Benutzereingabe.



Hohe Priorität bekommt der Garbage Collector nur, wenn der Interpreter keinen Speicher mehr hat.

Für die Entsorgung von Objekten muß also nichts getan werden.

Ein Beispielfall, wo wir dem Garbage Collector helfen können:


   public static void meth ()
   { int big_array[] = new int[100000];
     // rechne auf big_array
     int result = compute(big_array);

     // brauchen big_array nicht mehr
     // nach verlassen von meth kann es
     // entsorgt werden. 
     // falls meth aber NICHT verlassen 
     // wird:

     big_array = null;

     for (;;)    // für immer
       handle_user_input();
   }



Finalisierung von Objekten

Um die Freigabe der Ressource ``Speicher'' kümmert sich der Garbage Collector. Andere Ressourcen können in der finalizer-Methode freigegeben werden.



Hier ist die finalizer-Methode aus

java.io.FileOutputStream:


   public void finalize()
     throws IOException; 
     // overrides Object
   { // close the stream
     if (fd != null) 
        close();
   }


Regeln für Finalizer:




Innere Klassen

In Java 1.0:

In Java 1.1 zusätzlich:

Ziel: Definition von Hilfsklassen möglichst nahe an der Stelle, wo sie gebraucht werden.

Geschachtelte und innere Klassen sind wesentlich motiviert durch das neue Ereignismodell im AWT von Java1.1.


Geschachtelte ``top-level'' Klassen und Interfaces

Klassen oder Interfaces, die static deklariert sind und in andere ``top-level''-Klassen oder Interfaces eingeschachtelt sind.

Interfaces sind immer static, d.h. für Interfaces ist nur die ``top-level''-Variante möglich.




Geschachtelte ``top-level'' Klassen und Interfaces verhalten sich wie andere Paket-Elemente auf der äußersten Ebene, außer daß ihrem Namen der Name der umgebenden Klasse vorangestellt wird.





public class LinkedList
{ // interface verkettungselement
  public static interface Linkable
  { public Linkable getNext();
    public void setNext(Linkable n);
  }
  // kopf der liste:
  Linkable head;
  // einfügen:
  public void insert(Linkable n)
  { n.setNext(head);
    head = n;
  }
  // kopfzugriff:
  public Linkable getHead()
  { return head;
  }
}


Benutzung:

a) Definition der Verkettungselemente:


class LinkableInteger 
implements LinkedList.Linkable
{ 
  int i;
  LinkedList.Linkable Next;
  // konstruktor:
  LinkableInteger(int i)
  { this.i = i;
  }
  // versprochene Methoden:
  public LinkedList.Linkable getNext()
  { return Next;
  }
  public void setNext(LinkedList.Linkable n)
  { Next = n;
  }
  // für die ausgabe: String-Umwandlung
  // überschreibe toString aus Object
  public String toString()
  { return i + "";
  }
}


b) Testlauf:


public class LiMain
{
  public static void main (String[] argv)
    {
      LinkedList li = new LinkedList();
      li.insert(new LinkableInteger(16));
      li.insert(new LinkableInteger(6));
      li.insert(new LinkableInteger(-4));
      
      LinkedList.Linkable l;
      for (l = li.getHead(); 
           l != null; 
           l = l.getNext())
        System.out.println(l);
    }
}

c) Ergebnis:


-4
6
16



Elementklassen

Im Gegensatz zu den static-deklarierten geschachtelten ``top-level''-Klassen, die mehr oder weniger nur zur Strukturierung dienen, sind Elementklassen echte innere Klassen.




\psfig {figure=bilder/inner.ps}




Damit kann das Objekt der Elementklasse implizit auf die Instanzvariablen der umgebenden Klasse zugreifen (auch auf private).



Die umgebende Klasse hat Zugriffrechte auf alle Felder aller ihrer Elementklassen. Zwei Elementklassen der selben umgebenden Klasse haben Zugriffrechte auf die Elemente des jeweilig anderen.



Elementklassen dürfen keine statischen Elemente (Attribute, Methoden, Klassen, Interfaces) besitzen. Gilt auch für lokale Klassen (Seite [*]) und anonyme Klassen (Seite [*]).

Beispielprojekt: Einbau eines Iterators in eine Buchstabenmenge




Beschreibung der Buchstabenmenge:



Name Charset
Attribut chars vom Typ BitSet
Konstruktor Charset(String)
Methode toString()
  schreibt die Elemente in einen String.




Ziel: ein Iterator für Charset.



Typisches Verwendungsmuster für einen solchen Iterator:


   Charset cs = new Charset(somestring);
   Enumeration e = cs.characters();
   while (e.hasMoreElements())
     machwasmit(e.nextElement());

(Anderes Iteratorbeispiel auf Seite [*]).






a) Originalversion der Buchstabenmenge:


import java.util.BitSet;

class Charset
{ // Buchstabenmenge:
  private BitSet chars = new BitSet();
  // Konstruktor:
  public Charset(String str)
  { for (int i = 0; i < str.length(); i++)
    chars.set(str.charAt(i));
  }
  // Umwandlung in String:
  public String toString()
  { String result="[";
    int size = chars.size();
    for (int i = 0; i < size; i++)
        if (chars.get(i))
          result += (char)i;
    return result +"]";
  }
}


b) Testen der Originalversion der Buchstabenmenge:


public class CharsetMain
{ // hauptprogramm
  public static void main(String argv[])
    { // erstes kommandozeilenargument
      Charset cs = new Charset(argv[0]);
      System.out.println(cs + "");
    }
}

c) Testlauf:



Eingabe:


   java CharsetMain JavaProgrammierung



Ausgabe:


   [JPaegimnoruv]


c) Einbau einer inneren Iteratorklasse


import java.util.*
// für BitSet, Enumeration
// und NoSuchElementException

class Charset
{ // Buchstabenmenge:
  private BitSet chars = new BitSet();

  // Konstruktor:
  public Charset(String str)
  { for (int i = 0; i < str.length(); i++)
    chars.set(str.charAt(i));
  }

  // innere Elementklasse:
  private class Cs_iterator 
  implements Enumeration
  { private int pos;
    private int setsize = chars.size();
    
    // versprochenes "hasMoreElements":
    public boolean hasMoreElements()
    { while (pos < setsize && 
             !chars.get(pos))
        pos++;
      return pos < setsize;
    }



    // versprochenes "nextElement":
    public Object nextElement()
    throws NoSuchElementException
    { if (hasMoreElements())
        return new Character((char) pos++);
      else
        throw new NoSuchElementException();
    }
  }
  
  // liefert einen Iterator:
  public Enumeration characters()
    { return new cs_iterator();
    }
}


d) Testen des neuen Charset mit Iterator:


import java.util.*;

public class It_CharsetMain
{ public static void main(String argv[])
  { // erstes kommandozeilenargument
    Charset cs = new Charset(argv[0]);

    Enumeration e = cs.characters();
    while (e.hasMoreElements())
      System.out.println(e.nextElement());
  }
}
e) Testlauf:



Eingabe:


   java It_CharsetMain Java



Ausgabe:


   J
   a
   v


Syntax-Erweiterungen und neue Regeln für Elementklassen




a) Wie sprechen ``innere Objekte'' das ihnen zugeordnete Objekt der umgebenden Klasse an?




Im Charset-Beispiel:




\psfig {figure=bilder/inner2.ps}




pos und setsize sind Attribute der Selbstreferenz this .

Ihr expliziter Name wäre also this.pos, bzw. this.setsize.



Um chars explizit anzusprechen, schreiben wir:


    Charset.this.chars


b) Tiefere Schachtelung von Elementklassen?



Beliebig möglich. Allerdings darf keine der eingeschachtelten Klassen so heißen wie eine der sie umgebenden!




Das bedeutet:

Obige neue Syntax:


    classname.this

ist völlig ausreichend:


public class A
{ public string name = "a";
  public class B
  { public string name = "b";
    public class C
    { public string name = "c";
      public void print()
      { System.out.println(name);
        System.out.println(this.name);
        System.out.println(C.this.name);
        System.out.println(B.this.name);
        System.out.println(A.this.name);
      }
    }
  }
}


c) Welches ist das umgebende Objekt?



In unserem Beispiel haben wir den Default-Fall benutzt:


  public Enumeration characters()
    { return new cs_iterator();
    }

Hier: Keine Angabe des umgebenden Objektes für die neue Instanz der inneren Klasse. Daher wird die Selbstreferenz this als umgebendes Objekt angenommen:



Äquivalent zu obigem ist die neue explizite Schreibweise:


  public Enumeration characters()
    { return this.new cs_iterator();
    }

Für andere Fälle brauchen wir die neue allgemeine Syntax:


   containing_instance.new inner_class(...)

wobei containing_instance eine Instanz der umfassenden Klasse von inner_class ist.

d) Innere Objekte außerhalb der umgebenden Klasse erzeugen?



Geht, wenn die innere Klasse nicht wie in unserem Charset-Beispiel privat ist.



Also ändern:


  // innere Elementklasse:
  public class Cs_iterator 
  implements Enumeration
  { private int pos;
    ....
  }




Dann funktioniert auch folgendes Hauptprogramm:


  public static void main(String argv[])
    { // erstes kommandozeilenargument
      Charset cs = new Charset(argv[0]);

      Enumeration e = cs.new cs_iterator();
      while (e.hasMoreElements())
        System.out.println(e.nextElement());
    }


Noch ein Beispiel für die externe Erzeugung innerer Objekte. Klassenhierarchie von Seite [*]:


A a = new A;           // A Instanz
A.B b = a.new B();     // B Instanz in a
A.B.C c = b.new C();   // C Instanz in b
c.print();             // Methode aus C





e) Kann man von inneren Klassen erben?



Ja ( ``strange as it may seem'').

Damit gibt es aber auch eine zweite Art, wie Objekte innerer Klassen erzeugt werden können: mit dem Oberklassen-Konstruktoraufruf super.




Zusätzlich zur Syntax


   containing_instance.new inner_class(...)



brauchen wir also


   containing_instance.super(...)


f) Ändern sich durch innere Klassen die Regeln für die Namensbindung?




Ja. Wir haben jetzt drei Hierarchien in Java-Programmen:

1.
die klassische Blockschachtelung im Anweisungsteil
2.
die Vererbungshierarchie
3.
die Klassenschachtelung

Aufgabe der Namensanalyse: Finde zu jedem Auftreten eines Bezeichners die laut Sprachdefinition dazugehörige Definition.



Beispiel:



Eine Methode der Klasse K verwendet in einer Anweisung den Bezeichner x.



Ist x



Vererbungshierarchie und Klassenschachtelung im Bild:




\psfig {figure=bilder/scope.ps}

Die beiden Hierarchien sind völlig unabhängig voneinander.

Es ist wichtig, sie nicht zu verwechseln!

Konfliktfall zwischen umfassender Klasse und Oberklasse



In Java 1.1. gelöst zugunsten der Oberklasse, d.h.

der Bezeichner x der Oberklasse verdeckt den der umfassenden Klasse.



Zusätzlich zu dieser Regel erfordert Java im Konfliktfall die explizite Benennung des Bezeichners, d.h.







Lokale Klassen

Lokale Klassen sind innere Klassen, die nicht auf oberer Ebene in andere Klassen eingeschachtelt sind, sondern lokal in Anweisungsblöcken von Methoden, statischen Initialisierern oder Instanz-Initialisierern.



Sehr ähnlich zu Elementklassen (siehe Seite [*]) mit folgenden wichtigen Unterschieden:




Anonyme Klassen

Anonyme Klassen werden wie lokale Klassen (siehe Seite [*]) z.B. innerhalb von Anweisungsblöcken definiert.

Anonyme Klassen haben keinen Namen. Sie entstehen immer zusammen mit einem Objekt.



Ihr Zweck: ``Einweg-Klassen''



import java.io.*;
// gibt die Namen aller .java-Dateien im 
// angegebenen Verzeichnis aus
public class Dirlist
{public static void main(String argv[])
 { File f = new File(argv[0]);
   String[] list = 
     f.list (new FilenameFilter() 
     { public boolean accept(File f, 
                             String s)
       { return s.endsWith(".java");
       }
     }
             ); // Formatierung schwierig!
   for (int i = 0; i < list.length; i++)
     System.out.println(list[i]);
 }
}


Die Syntax für anonyme Klassen ist


   new-expression class-body

Für die anonyme Klasse kann man keine extends- oder implements-Klauseln angeben.



Daher:



In unserem Beispiel implementiert die anonyme Klasse ein Interface:


   f.list(new FilenameFilter() 
          { ...
          }
         ):



Da anonyme Klassen keinen Namen haben, können sie auch keine Konstruktoren haben. Deshalb hat Java 1.1 die Instanzinitialisierungsblöcke eingeführt (Seite [*]).


Sichtbarkeitsspezifikationen

Die Sichtbarkeitsspezifikationen

geben an, wo Attribute und Methoden einer Klasse benutzt werden können.




Benutzbar in Sichtbarkeit
  \rotatebox {60}{\tt public} \rotatebox {60}{\tt protected} \rotatebox {60}{Paket} \rotatebox {60}{\tt private}
Selber Klasse ja ja ja ja
Klasse, selbes Paket ja ja ja nein
Unterklasse, anderes Paket ja ja nein nein
Klasse, anderes Paket ja nein nein nein


Besonderheit im Zugriff auf protected-Elemente aus Unterklassen:



Erlaubt ist der Zugriff auf protected-Elemente des eigenen Oberklassenanteils.

Nicht Erlaubt ist der Zugriff auf protected-Elemente in anderen Objekten vom Oberklassentyp.



Beispiel:



Klasse A im Paket pack:


   package pack;

   public class A
   { protected int x;
   }



Klasse B in einem anderen Paket:




   class B extends pack.A
   { void doit()
     { x = 18;
       pack.A va = new pack.A();
      va.x = 19;    // NICHT ZULÄSSIG !!
     }
   }




next up previous contents index
Nächste Seite: Vererbung Aufwärts: Programmieren in Java Vorherige Seite: Ein Streifzug durch Java   Inhalt   Index
Peter Pfahler, 1997