Java Standard: Klassen


Aufbau einer Java-KlasseBearbeiten

public class Testklasse {
    private static final int KLASSENKONSTANTE = 42;
    private static int anzahl;

    public static void gib_etwas_aus() {
        System.out.println(KLASSENKONSTANTE + " Anzahl der übergebenen Argumente:" + anzahl);
    }

    public static void main(String[] args) {
        for(String i : args)
          System.out.println(i);
        anzahl = args.length;
        gib_etwas_aus();
    }
}

Jede mit "public" gekennzeichnete Klasse muss in einer eigenen Datei gespeichert werden, die Klassenname.java heißt. Schlüsselwörter wie "public" und "private" sind Zugriffsmodifizierer, "final" bezeichnet in diesem Kontext eine Konstante und wird ebenso wie "static" weiter unten erklärt. Die "Testklasse" enthält zwei Methoden: "gib_etwas_aus()" und "main(String[] args)". "klassenkonstante" und "anzahl" nennt man Klassenvariablen, man erkennt Klassenvariablen am Schlüsselwort "static".

class AndereKlasse {
    public int eineVariable;

    public AndereKlasse(int eingabe) {
        eineVariable = eingabe;
    }
}

public class Testklasse  {
    public static void main(String[] args) {
        AndereKlasse a = new AndereKlasse(42);
        System.out.println(a.eineVariable);
    }
}

Dieses Beispiel enthält zwei verschiedene Klassen. Die Variable "eineVariable" in der Klasse "AndereKlasse" ist eine Instanz-Variable, hingegen ist "a" aus der Methode "main()" eine lokale Variable. In der Variablen "a" ist das Objekt AndereKlasse gespeichert. Klasse und Objekt sind also zwei verschiedene Dinge.

ZugriffsmodifiziererBearbeiten

In der Objektorientierung kennt man so genannte Zugriffsmodifizierer (engl. access modifier), die die Rechte anderer Objekte einschränken (Kapselung) oder die ein bestimmtes Verhalten von einem Unterobjekt verlangen (Abstraktion/Vererbung).

Manchmal hört man auch die Bezeichnung Sichtbarkeitsmodifizierer (engl. visibility modifier). Diese Bezeichnung ist aber eigentlich falsch, weil die Zugriffsmodifizierer den Zugriff auf einen Member verbieten, der Member als solches bleibt jedoch sichtbar, z.B. über Reflection. Dennoch sollte man diese Bezeichnung verstehen, da sie unter Java-Programmierern weit verbreitet ist.

Java kennt folgende Zugriffsmodifizierer:

Die Klasse selbst,
innere Klassen
Klassen im
selben Package
Unterklassen Sonstige
Klassen
private Ja Nein Nein Nein
(default) Ja Ja Nein Nein
protected Ja Ja Ja Nein
public Ja Ja Ja Ja


privateBearbeiten

private ist der restriktivste Zugriffsmodifizierer. Er verbietet jeglichen Zugriff von außerhalb der Klasse auf den entsprechend modifizierten Member. Auf eine private Variable kann nur die Klasse selbst zugreifen, ebenso auf einen privaten Konstruktor, eine private Methode oder einen privaten geschachtelten Datentyp.

Klassenvariablen werden üblicherweise als private deklariert. Das Verändern der Variablen wird über passende Methoden, meist Getter und Setter ermöglicht. Dieses Prinzip nennt man Geheimnisprinzip.

/** Mutable 2D-Punkt-Klasse. */
public class Punkt2D {
    private int x, y; // private Variablen
    public Punkt2D(final int x, final int y) {
        setX(x);
        setY(y);
    }
    public int getX() { return x; }
    public int getY() { return y; }
    public void setX(final int x) { this.x = x; }
    public void setY(final int y) { this.y = y; }
}

Allgemein werden sämtliche Member als private deklariert, die ein Implementierungsdetail darstellen, auf das sich keine andere Klasse verlassen bzw. das von keiner anderen Klasse verwendet werden darf.

(default)Bearbeiten

Als "default" oder "package private" bezeichnet man die Zugreifbarkeit für den Fall, dass kein Zugriffsmodifizierer angegeben wurde. Auf einen package private Member können nur Klassen zugreifen, die sich im selben Paket wie die Klasse des Members befinden.

Der "default" Zugiffsmodifizierer wird dann eingesetzt, wenn eine Klasse auf die Daten einer anderen Klasse (innerhalb des selben Packages) Zugriff haben soll, aber diese Funktionalitäten nach außen nicht verfügbar gemacht werden sollen.

Dieser Zugriffmodifizierer wird insbesondere bei der Entwicklung von API eingesetzt.


protectedBearbeiten

Mit dem Zugriffsmodifizierer protected ist der Zugriff nicht nur Klassen aus dem selben Package (wie "default"), sondern auch Subklassen der Klasse erlaubt. Dies gilt auch, wenn die betreffenden Subklassen aus einem anderen Package sind als die Klasse des betreffenden Members.

Der Zugriffsmodifizierer protected wird verwendet, wenn es nur für Subklassen Sinn ergibt, den betreffenden Member zu verwenden.

Diese Zugriffsmodifizierer findet in der API-Programmierung Einsatz. Auch Muster wie das Schablonenmuster (engl. template pattern) nutzen diesen Mechanismus.


publicBearbeiten

Der Zugriffsmodifizierer public gestattet sämtlichen Klassen Zugriff auf den betreffenden Member. Er ist der freizügigste Zugriffsmodifizierer.

Die Zugreifbarkeit public findet man hauptsächlich bei Methoden, die von anderen Klassen verwendet werden sollen z.B. bei Konstruktoren.


Welcher Zugriffsmodifizierer?Bearbeiten

Welchen Zugriffsmodifizierer soll man nun verwenden? Im einfachsten Fall verwendet man private für Variablen sowie public für Methoden, Konstruktoren und Datentypen. Eine Regel, die sich in der Praxis sehr bewährt hat, lautet "so streng wie möglich, so freizügig wie nötig".

Polymorphie-ModifiziererBearbeiten

Polymorphie-Modifizierer sind dazu da, ein bestimmtes Verhalten einer Unterklasse zu erzwingen, bzw. dieses zu erleichtern. Das heißt, dass wenn eine Klasse von einer abstrakten Klasse abgeleitet werden soll, die Unterklasse diese Attribute bzw. Methoden implementieren muss, wenn es eine konkrete Klasse werden soll. Doch nun zu den einzelnen Modifizierern selbst:

abstractBearbeiten

Es können Methoden und Attribute als abstract bezeichnet (deklariert) werden, was bedeutet, dass entweder die Unterklasse diese implementieren muss oder aber die abgeleitete Klasse ebenfalls als abstrakt deklariert werden muss.

Von einer abstrakten Klasse können keine Instanzen gebildet werden, so dass diese immer erst implementiert werden muss, um das gewünschte Ergebnis zu erreichen. Ein Beispiel:

public abstract class Berechne {
    public abstract int berechne(int a, int b);
}

Dies ist eine abstrakte Klasse mit einer Methode. Was genau berechnet werden soll, steht hier aber nicht - deswegen heißt diese auch "abstrakt". Jetzt erweitern wir diese Klasse um eine Addition zu erhalten:

public class Addiere extends Berechne {
    public int berechne(int a, int b) {
        int c = a + b;
         return c;
     }
}

Wie man an diesem konkreten Beispiel sieht, wird erst in der "konkreten" Klasse der Algorithmus implementiert.

finalBearbeiten

Es können Klassen, Methoden, Attribute und Parameter als final bezeichnet (deklariert) werden. Einfach ausgedrückt bedeutet final in Java "du kannst mich jetzt nicht überschreiben".

Für finale Klassen bedeutet dies, dass man von ihr nicht erben kann (man kann keine Unterklasse erzeugen). Sie kann also nicht als Vorlage für eine neue Klasse dienen. Grundlegende Klassen, wie zum Beispiel die String-Klasse sind final. Wenn sie es nicht wäre, dann könnte man von ihr erben und ihre Methoden überschreiben und damit das Verhalten der erweiterten Klasse verändern.

Finale Methoden können in Subklassen nicht überschrieben werden.

Finale Attribute und auch Klassen-Variablen können nur ein einziges Mal zugewiesen werden. Sobald die Zuweisung erfolgt ist, kann eine finale Variable ihren Wert nicht mehr ändern. Bei Member-Variablen muss die Zuweisung bei der Instanzierung, bei Klassen-Variablen beim Laden der Klasse erfolgen.

Finale Parameter können ausschliesslich den beim Methodenaufruf übergebenen Wert besitzen. In der Methode selbst lassen sie sich nicht überschreiben.

Ein Beispiel:

 public int getSumme (final int summand1, final int summand2) {
    return summand1 + summand2;
 }

Der Compiler hat die Möglichkeit, finale Member-Variablen, denen Konstanten zugewiesen werden, direkt im kompilierten Code zu ersetzen.

Ein Beispiel:

 public class AntwortAufAlleFragenDesUniversums {
     private final long antwort = 23;
     public long gibAntwort() {
         return antwort;
     }
 }

Der Compiler macht daraus:

 public class AntwortAufAlleFragenDesUniversums {
     public long gibAntwort() {
         return 23;
     }
 }


Wie Sie sehen, hat der Compiler hier einfach die finale Variable durch den Wert ersetzt. Diese Optimierung ist aber nur möglich, da der Wert konstant ist und direkt zugewiesen wird.

In folgendem Beispiel kann der Compiler nicht optimieren:

 public class WievielMillisSindVerstrichen {
     private final long antwortInstanz = System.currentTimeMillis();
     private final static long antwortKlasse = System.currentTimeMillis();
 
     public long gibAntwortInstanz() {
         return antwortInstanz;
     }
 
     public long gibAntwortKlasse() {
         return antwortKlasse;
     }
 }

Der Wert der Variable antwortKlasse wird beim Laden der Klasse zugewiesen. Die Klasse wird dann geladen, wenn sie das erste Mal zur Laufzeit benötigt wird. Der Wert der Variable antwortInstanz wird beim Instanzieren der Klasse, genauer gesagt bei der Initialisierung des Objekts auf Ebene der Klasse WievielMillisSindVerstrichen, also mit new WievielMillisSindVerstrichen() zugewiesen.

Noch eine Warnung beim Verwenden von Konstanten in Java. In Java deklariert man Konstanten wie folgt:

 public class Konstante {
     public final static int ICH_BIN_EINE_KONSTANTE = 42;
 }

 public class Beispiel {
     public static void main(String[] args) {
         System.out.println(Konstante.ICH_BIN_EINE_KONSTANTE);
     }
 }
javac Konstante.java Beispiel.java

Der Java Compiler ersetzt dann überall die Variable durch dessen Konstante. Dadurch gelangt die Konstante (42) und nicht die Variable (ICH_BIN_EINE_KONSTANTE) in das Kompilat (Konstante.class und Beispiel.class).

java Beispiel

gibt wie erwartet die Zahl 42 aus.

Wenn ich jetzt den Wert der Konstante von 42 auf 99 ändere und nur die Klasse Konstante compiliere, wird es gefährlich:

 public class Konstante {
     public final static int ICH_BIN_EINE_KONSTANTE = 99;
 }
javac Konstante.java
java Beispiel

gibt immer noch die Zahl 42 aus, obwohl ich doch den Wert der Variable ICH_BIN_EINE_KONSTANTE geändert habe. Warum? Als das Kompilat der Klasse Beispiel (Beispiel.class) erzeugt wurde, war der Wert von ICH_BIN_EINE_KONSTANTE noch 42. Der Compiler hat zu dem Zeitpunkt den Wert 42 in die Klasse Beispiel eingesetzt. Weil die Klasse Beispiel nicht nochmals kompiliert wurde steht im Kompilat immer noch den Wert 42 und der wird dann auch ausgegeben.

javac Beispiel.java
java Beispiel

hilft und gibt dann auch 99 aus.

Konsequenzen: Bei einer Änderung von public/protected final static sollte immer der gesamte Code neu kompiliert werden. Wenn das nicht möglich ist (z.B. in einer Klasse, die von verschiedenen anderen Projekten benutzt wird), sollte man auf die Verwendung von Konstanten verzichten und stattdessen einfach eine Methode verwenden:

 public class Konstante {
     private final static int ICH_BIN_EINE_KONSTANTE = 99; // ungefährlich, da nur innerhalb der Klasse der Zugriff möglich ist
 
     public static getDieKonstante() {
         return ICH_BIN_EINE_KONSTANTE;
     }
 }


Ein letztes Beispiel soll aufzeigen, dass mit final lediglich die Zuweisung (das Überschreiben), nicht aber der Gebrauch geschützt wird.

 import java.util.List;
 import java.util.LinkedList;
 
 public class NamensListe {
     private final List interneNamensliste = new LinkedList();
 
     public void neuerName(String name) {
         interneNamensliste.add(name);
     }
 
     public int anzahlNamen() {
         return interneNamensliste.size();
     }
 
     public List gibListe() {
         return interneNamensliste;
     }
 }

Wie man hier schön sieht, kann man auf finalen Member-Variablen, hier die internenNamensliste, Methoden ausführen (wie das Hinzufügen mit interneNamensliste.add(name)). Da die Member-Variable interneNamensliste final ist, kann man ihr keinen anderen Wert zuweisen. Eine zusätzliche Methode

 public void entferneListe() {
     interneNamensliste = null;
 }

ist nicht zulässig und würde beim Kompilieren mit einer Fehlermeldung enden. Richtig riskant ist die Methode gibListe(). Indem die interneNamensliste der Außenwelt (außerhalb der Klasse) verfügbar gemacht wird, kann man von der Außenwelt auch alle Methoden der Liste aufrufen. Damit ist die Liste außerhalb des Einflussbereiches der Klasse. Da hilft es auch nicht, dass die Member-Variable interneNamensliste final ist.

staticBearbeiten

Es können Methoden und Klassenvariablen als static bezeichnet (deklariert) werden.

Statische Methoden und Variablen benötigen keinerlei Instanzen einer Klasse, um aufgerufen zu werden. Ein Beispiel für einen statischen Member ist z.B. die Konstante PI in der Klasse java.lang.Math.

Auch die Methoden dieser Klasse können einfach aufgerufen werden, ohne vorher eine Instanz dieser Klasse anzulegen, z.B. java.lang.Math.max(3,4).

Sofern eine statische Klassenvariable erst zur Laufzeit dynamisch einen Wert erhalten soll, können Sie dies mit einem statischen Block erreichen.

Beispiel:

 public class LoadTimer {
   
   static {
     ladeZeit = System.currentTimeInMillis ();
   }
   private static long ladeZeit;
 }

Es ist dennoch möglich, statische Methoden oder Attribute über ein Objekt aufzurufen, davon wird aber dringend abgeraten. Denn dies führt zu unangenehmen Ergebnissen.

Beispiel:

 public class SuperClass {
  public static void printMessage(){
   System.out.println("Superclass: printMessage");
  }
 }

 public class SubClass extends SuperClass {
  public static void printMessage(){
   System.out.println("Subclass: printMessage");
  }
 }

 public class StrangeEffect {
  public static void main(String[] args){
   SubClass object = new SubClass();
   object.printMessage();
   
   SuperClass castedObject = (SuperClass)object;
   castedObject.printMessage();
  }
 }

Die Ausgabe ist:

Subclass: printMessage
Superclass: printMessage

Erstaunlich ist die zweite Zeile: Obwohl unser object vom Typ SubClass ist, wird die Methode von SuperClass aufgerufen. Offensichtlich funktioniert hier das Überschatten nicht. Das liegt daran, dass statische Methodenaufrufe nicht vom Laufzeittyp abhängen! Konkret bedeutet dies, dass die Entscheidung welche statische Methode nun aufgerufen werden soll, unabhängig davon getroffen wird, welcher Klasse das Exemplar angehört.

Um diesen irreführenden Effekt zu vermeiden, sollte man statische Methoden immer auf Klassen aufrufen und nicht auf Objekten.

strictfpBearbeiten

strictfp kennzeichnet Klassen und Methoden, deren enthaltene Fließkommaoperationen streng auf eine Genauigkeit von 32 bzw. 64 Bit beschränkt sind. Dadurch wird sichergestellt, dass die JVM darauf verzichtet, Fließkommaoperationen intern mit einer höheren Genauigkeit zu berechnen (beispielsweise 40 bzw. 80 Bit) und nach Abschluss der Berechnung wieder auf 32 bzw. 64 Bit zu kürzen. Dies ist wichtig, da es sonst bei verschiedenen JVMs oder verschiedener Hardware zu unterschiedlichen Ergebnissen kommen kann und somit das Programm nicht mehr plattformunabhängig ist.

Ausdrücke, die zur Kompilierzeit konstant sind, sind immer FP-strict.

nativeBearbeiten

native kann nur vor Methoden stehen und bedeutet, dass die Implementierung der betreffenden Methode nicht in Java, sondern einer anderen Programmiersprache geschrieben wurde, und von der virtuellen Maschine über eine Laufzeitbibliothek gelinkt werden muss. Die Syntax der Methode entspricht dann einer abstrakten Methode. Ein Beispiel:

 public native void macheEsNichtInJava ();

Um eine solche Methode zu verwenden muss sie nach dem Compilieren über einen "Java-Pre-Compiler" (javah) gejagt werden, d.h. dieser generiert aus einer "native" - Methode ein entsprechenden C-Header und Rumpf, der dann mit Leben gefüllt werden kann. Diese Rümpfe werden als dll unter Windows bzw. lib unter Linux/Unix compiliert. Diese compilierten Libs müssen dann aber auch zur Laufzeit des Programms zugreifbar sein, andernfalls erhält der Nutzer eine Exception.