Kapitel 4: Klassenmethoden

Häufig verwendete Algorithmen werden zu so genannten Methoden zusammengefasst, die unter Nennung ihres Namens und ggf. mit Übergabe von aktuellen Parametern aufgerufen werden.

Methoden sind entweder objektbezogen (siehe später) oder klassenbezogen. Klassenbezogene Methoden werden durch das Schlüsselwort static deklariert und können innerhalb oder außerhalb derselben Klasse aufgerufen werden. Die Zugriffsrechte werden über sogenannte Modifizierer geregelt. Mit dem Modifizierer private wird der Zugriff nur von der eigenen Klasse ermöglicht, mit dem Modifizierer public darf von einer beliebigen anderen Klasse zugegriffen werden.

Gibt eine Methode einen Wert zurück, so steht sein Typ vor dem Methodennamen, andernfalls steht dort void. Als Aufrufmechanismus wird in der Programmiersprache Java call-by-value verwendet. D. h., ein im Methodenrumpf benutzter formaler Parameter wird beim Methodenaufruf mit dem Wert des aktuellen Parameters versorgt und innerhalb der Methode kann nur auf den Wert, nicht auf den ursprünglichen Speicherbereich, zugegriffen werden.

Methoden können durch verschiedene Parametersätze überladen werden, d. h. der Name einer Methode wird mehrmals verwendet, wobei sich die formalen Parameter unterscheiden müssen. Die Kombination aus Methodenname und den formalen Parametern bezeichnet man auch als Signatur einer Methode. – Der Rückgabedatentyp zählt nicht zur Signatur.

Eine mit dem Schlüsselwort static versehene Deklaration einer Klassenvariablen führt eine klassenbezogene Variable ein, welche in allen Methoden dieser Klasse sichtbar ist. Auch eine Klassenvariable kann mit unterschiedlichen Modifizierern versehen werden. Mit dem Modifizierer private wird der Zugriff nur von der eigenen Klasse ermöglicht, mit dem Modifier public darf auch von einer beliebigen anderen Klasse zugegriffen werden.

Methodenaufrufe

Die Implementierung von Methoden hat einen ganz entscheidenden Einfluss auf das Design einer Anwendung. Wenn immer Sie eine klar identifizierbare Aufgabe in Ihren Programmcodes erkennen, sollten Sie diese als eigenständige Methode implementieren!

Das Programm Methods.java definiert einige beispielhafte Methoden, um deren Syntax und Nutzung zu zeigen.

 1import de.pakad.udemy.StdIn;
 2import de.pakad.udemy.StdOut;
 3
 4/**
 5 * Demo für statische Methoden in Java. Es werden Asterisk-Zeichen 
 6 * im Terminal ausgegeben.
 7 */
 8public class Methods {
 9
10    /**
11     * Ausgabe einer Eingabeaufforderung. 
12     */
13    private static void prompt() {
14        StdOut.print("Bitte eine positive ganze Zahl: ");
15    }
16
17
18    /**
19     * Eingabe der auszugebenden Anzahl an Asterisk-Zeichen.
20     * 
21     * @return Anzahl Asterisks
22     */
23    private static int input() {
24        prompt();  // Eingabeaufforderung ausgeben
25        return StdIn.readlnInt();
26    }
27    
28
29    /**
30     * Ausgabe von {@code n} Asterisk-Zeichen im
31     * Terminalfenster.
32     * 
33     * @param n Anzahl auszugebender Zeichen
34     */
35    public static void printStars(int n) {
36        for (int i=0; i<n; i++) 
37            StdOut.print("*");
38        StdOut.println();
39    }
40
41    /**
42     * Hauptprogramm.
43     * 
44     * @param args nicht genutzt
45     */
46    public static void main(String[] args) {
47        int n = input();
48        printStars(n);
49    }
50}

Listing 4.1: Methods.java

Methoden strukturieren den Programmcode in kleine Funktionseinheiten, die jeweils eine bestimmte Aufgabe erfüllen. Der Code wird dadurch übersichtlicher und ist einfacher zu warten. Methoden können nicht nur innerhalb eines Programms von verschiedenen Stellen im Code aufgerufen (wiederverwendet) werden, sie können sich untereinander aufrufen. Wenn der jeweilige Zugriffsmodifizierer es erlaubt, kann eine Methode auch von anderen Klassen genutzt werden.

Die Methode printStars() im obigen Beispiel ist öffentlich. Das bedeutet, dass andere Klassen (Programme) darauf zugreifen können.

Unter der Voraussetzung, Methods.java und PrintStars.java befinden sich im gleichen Verzeichnis, kann die Klasse PrintStars auf die öffentliche Methode printStars() von Methods zugreifen und sie aufrufen.

 1/**
 2 * Das Programm zeigt die Wiederverwendung der Methode
 3 * {@link Methods#printStars}.
 4 */
 5public class PrintStars {
 6    /**
 7     * Hauptprogramm. 
 8     * @param args nicht genutzt
 9     */
10    public static void main(String[] args) {
11        Methods.printStars(42);
12    }
13}

Listing 4.2: PrintStars.java

Parameterübergabe via call-by-value

Bereits eingangs habe ich Wertübergabe (call-by-value) dahingehend erläutert, dass eine Methode lediglich den Wert eines Parameters erhält. Wir müssen uns darüber Klarheit verschaffen, was das bedeutet. Hierzu haben wir zwischen primitiven Datentypen und Referenzdatentypen zu differenzieren.

Wertübergabe bei primitiven Datentypen

 1import de.pakad.udemy.StdOut;
 2
 3/**
 4 * {@code SwapImpossible} zeigt, dass Methoden in Java keine
 5 * Werte primitiver Datentypen, die als Argumente übergeben
 6 * werden, verändern kann.
 7 */
 8public class SwapImpossible {
 9
10    /**
11     * Tauschen von Werten primitiver Datentypen geht nicht!
12     * 
13     * @param a erster Wert
14     * @param b zweiter Wert
15     */
16    private static void swap(int a, int b) {
17        StdOut.printf("swap - a: %d, b: %d\n", a , b);
18        int tmp;
19        tmp = a;
20        a = b;
21        b = tmp;
22        StdOut.printf("swap - a: %d, b: %d\n", a , b);
23    }
24
25    /**
26     * Hauptprogramm.
27     * 
28     * @param args nicht genutzt
29     */
30    public static void main(String[] args) {
31        int a = 1, b = 42;
32        StdOut.printf("main - a: %d, b: %d\n", a , b);
33        swap(a, b);
34        StdOut.printf("main - a: %d, b: %d\n", a , b);
35        ;
36    }
37}

Listing 4.3: SwapImpossible.java

Aufgrund der Wertübergabe erhält swap() nur die Werte von x und y, arbeitet also auf einer Kopie. Ein Vertauschen der Variableninhalte von x und y ist daher nicht möglich.

Merke: In Java können keine Methoden implementiert werden, die primitive Übergabeparameter für den Aufrufer modifizieren!

Wertübergabe bei Referenzdatentypen

Bei Referenzdatentypen, wie beispielsweise einem Array sieht das anders aus. Sie erinnern sich: Eine Array-Variable ist eine Referenz auf einen Speicherbereich, in welchem die Daten des jeweiligen Arrays abgelegt sind.

Bei der Wertübergabe wird nicht das Array als solches übergeben, es wird der Wert der Array-Variablen übergeben. Das ist lediglich eine Speicheradresse. Auf üblichen Computersystem sind das 64 Bits. Weil es sich um eine Speicheradresse handelt, bekommt die Methode Zugriff auf den Speicherbereich. Den kann sie dann ändern.

In Bezug auf Referenzdatentypen ist die so realisierte Wertübergabe sowohl Fluch als auch Segen. Auf der einen Seite müssen wir Obacht geben, als Seiteneffekt nicht aus Versehen, ein Referenzobjekt (hier ein Array) zu verändern. Auf der anderen Seite erspart es uns, große Datenmengen „in eine Methode zu kopieren". Auch die Übergabe riesiger Arrays mit einer Million und mehr Datensätzen reduziert sich auf die Übergabe einer Speicheradresse.

 1import de.pakad.udemy.StdOut;
 2
 3/**
 4 * {@code SwapPossible} zeigt, dass Methoden in Java Werte
 5 * in Referenzdatentypen, die als Argumente übergeben
 6 * werden, verändern können.
 7 */
 8public class SwapPossible {
 9    
10    /**
11     * Die Werte in einem Array lassen sich verändern.
12     * 
13     * @param a Array
14     * @param i Index des ersten zu tauschenden Elements
15     * @param j Index des zweiten zu tauschenden Elements
16     */
17    private static void swap(int[] a, int i, int j) {
18        int tmp;
19        StdOut.printf("swap - a[%d]: %d, a[%d]: %d\n", i, a[i] , j, a[j]);
20        tmp = a[i];
21        a[i] = a[j];
22        a[j] = tmp;
23        StdOut.printf("swap - a[%d]: %d, a[%d]: %d\n", i, a[i] , j, a[j]);
24    }
25
26    /**
27     * Hauptprogramm.
28     * 
29     * @param args nicht genutzt
30     */
31    public static void main(String[] args) {
32        int[] a = {1, 2, 3, 4, 5};
33        int i = 0, j = 4;
34
35        StdOut.printf("swap - a[%d]: %d, a[%d]: %d\n", i, a[i] , j, a[j]);
36        swap(a, i, j);
37        StdOut.printf("swap - a[%d]: %d, a[%d]: %d\n", i, a[i] , j, a[j]);
38    }
39}

Listing 4.4: SwapPossible.java

Achtung: Referenzdatentypen sind nicht zwangsläufig veränderlich (mutable). Der Datentyp String ist unveränderlich (immutable). Das ist eine Java-Designentscheidung. Alle nicht veränderlichen Datentypen können, analog zu den primitiven Datentypen, in Methoden nicht modifiziert werden. Zwar wird auch bei Ihnen nur eine Kopie der Speicheradresse an eine Methode übergeben, sodass theoretisch auf den Speicherbereich des Objekts zugegriffen werden könnte, das ist jedoch aufgrund des Designs, wie solche Datentypen konstruiert sind, nicht möglich.

Sichtbarkeit

Innerhalb einer Methode verdecken formale Parameter und lokale Variablen gleichlautende Identifizierer aus der umschließenden Klasse. So kann ein Variablenname mehrmals innerhalb einer Klasse erscheinen, z. B. als Klassenvariable, lokale Variable oder als formaler Parameter.

Soll innerhalb einer Methode auf eine Variable zugegriffen werden, wird diese erst innerhalb der Methode gesucht (lokale Variable oder formaler Parameter). Ist dort die entsprechende Variable nicht vorhanden wird nach einer entsprechenden Klassenvariable gesucht.

 1import de.pakad.udemy.StdOut;
 2/**
 3 * Demonstration von Sichtbarkeiten innerhalb einer Klasse.
 4 */
 5public class Visibility {
 6
 7    public static int V;       // Klassenvariable V
 8
 9    public static void V() {   // Klassenmethode V
10        V = 42;                // referenziert klassenbezogenes V
11    }
12
13    public static int V(int n) {
14        int V = 1;             // lokales V verdeckt klassenbezogenes V
15        for (int i = 1; i <= n; i++)  
16            V *= i;
17        return V;
18    }
19
20    public static void set(int newValue) {
21        V = newValue;          // verändert klassenbezogenes V
22    }
23
24    public static int get() {
25        return V;              // klassenbezogenes V liefern
26    }
27
28    /**
29     * Treiber-Methode, um die Sichtbarkeiten auf den Ebenen Klasse 
30     * (Klassenvariablen) und Methoden (lokale Variablen, Parameter)
31     * zu zeigen.
32     * 
33     * @param args nicht genutzt
34     */
35    public static void main(String[] args) {
36        int V = 5;             // lokale V
37        StdOut.println(V);     // lokales V ausgeben
38        V();                   // klassenbezogenes V auf 42 setzen
39        StdOut.println(get()); // klassenbezogenes V ausgeben
40        StdOut.println(V(V));  // V(5) - lokales V wird übergeben
41        set(21);               // klassenbezogenes V auf 21 setzen
42        StdOut.println(get()); // globales V ausgeben
43    }
44}

Listing 4.5: Visibility.java

Die Regeln zur Sichtbarkeit sind in Java streng aber intuitiv verständlich. Dennoch sollte man sich als Programmierer bemühen, klare Bezeichner für Variablen und Methoden zu benutzen. Das obige Beispielprogramm erfordert viel Konzentration, um es korrekt nachzuvollziehen. – Es ist insoweit ein klassisches Beispiel für schlechten und unübersichtlichen Code, was hier aber beabsichtigt ist, um Ihnen sie Sichtbarkeitsbereiche und deren Abgrenzung demonstrieren zu können.

Fehlerbehandlung

Generell ist jeder Programmierer verpflichtet seine Methoden robust zu implementieren. Darunter versteht man, dass eine Methode mit allen Werten, die ihr übergeben werden können, sinnvolle Resultate liefert.

Man betrachte folgende Klassenmethode faculty(), die alle positiven Zahlen von $1$ bis zum übergebenen Wert aufmultipliziert und das Ergebnis zurückliefert.

public static int faculty(int n) {
    int f = 1;
    for (int i=1;i<=n;i++)
        f = f * i;

    return f;
}  

Falls für n ein Wert kleiner als $0$ übergeben wird, liefert die Methode das (offenbar falsche) Ergebnis $1$ zurück. Falls für $n$ ein zu großer Wert übergeben wird, entsteht ein Integer-Overflow. Korrekt wäre es, wenn die Methode dem Benutzer mitteilen könnte, ob der übergebene Parameter im korrekten Wertebereich liegt. Dies könnte über den Rückgabewert geschehen (z. B. $-1$); besser ist es jedoch, das Ergebnis der Berechnung von der Fehlermeldung zu trennen. Hierzu kann in Java eine Methode eine RuntimeException werfen. Wird diese RuntimeException nicht weiter behandelt, führt sie zu einem Programmabbruch. Dies ist eine recht drastische Maßnahme. Allerdings ist ein Programmabbruch immer noch besser als eine Berechnung mit fehlerhaften Werten, ohne dies dem Benutzer mitzuteilen.

Die Fehlerbehandlung mit einer RuntimeException kann wie folgt aussehen.

public static int faculty(int n) {
    if (n < 0)  throw new RuntimeException("Fehler: Parameter negativ");
    if (n > 12) throw new RuntimeException("Fehler: Parameter zu groß");
    int f = 1;
    for (int i=1;i<=n;i++)
        f = f * i;

    return f;
}  

Bei der Erzeugung einer RuntimeException kann eine Fehlermeldung angegeben werden. Diese wird bei Programmabbruch auf der Konsole ausgegeben. Die erzeugte Ausnahme vom Typ RuntimeException wird dann mit dem Schlüsselwort throw aus der Methode zum Aufrufer der Methode geworfen und die Abarbeitung der Methode endet an dieser Stelle.

Generell sollte eine Methode fehlerhafte Eingaben ablehnen, aber auch der Benutzer der Methode sollte vorher sicherstellen, dass erst gar keine fehlerhaften Werte an eine Methode übergeben werden.

Hinweis: RuntimeException ist eine sehr allgemeine Ausnahme. Java kann verschiedenste Arten von Ausnahmen differenzieren. Mithilfe der objektorientierten Programmierung (dazu später) können wir sogar eigene problemspezifische Ausnahmen definieren.