Funktionen

  • Deklaration einer Funktion
  • Definition einer Funktion
  • Abbrechen einer Funktion
  • Rekursion

Nach dem aufwendigen Kapitel mit den Schleifen schauen wir uns nun ein einfacheres Thema an. Nicht einfacher, weil es einfacher wäre, sondern weil wir bereits aus den vorherigen Lektionen Wissen mitbringen, dass wir nun in ähnlicher Form wiederfinden werden. Und letztendlich, weil wir ja bereits die ganze Zeit mit einer Funktion namens „main“ arbeiten.

Aufruf einer Funktion

Sehen wir uns den Aufruf einer Funktion direkt anhand eines Beispiels mit sqrt() an:

import static java.lang.Math.*;
 
class TestSqrt {
 
    public static void main(String[] args) {
        double result = sqrt(5);
        System.out.println("Ergebnis: " + result);
    }
 
}

Wir rufen also sqrt() auf, indem wir ein nach dem Funktionsnamen ein paar Klammern schreiben. Zwischen den runden Klammern stehen jene Werte, die an sqrt() zur Verarbeitung weitergegeben werden. sqrt() steht für „square root“ und berechnet die Quadratwurzel einer übergebenen Zahl. Daher ist es intuitiv jenen Wert an die Funktion zu übergeben, dessen Quadratwurzel wir wissen möchten. In diesem Fall ist das also die Zahl 5. Funktionen haben in der Regel eine Rückgabe, denn oft - so wie in diesem Beispiel - sollen ja etwas ausrechnen, wie in der Mathematik. Da der Aufruf einer Funktion mit Rückgabewert wie ein Ausdruck behandelt werden kann, ist natürlich auch die Zuweisung an eine Variable möglich. Wir speichern hier also die Quadratwurzel der Zahl 5 in der Variable result. Die Ausgabe des Programms lautet entsprechend:

Ergebnis: 2.23606797749979

Nehmen wir an, dass wir eine Funktion eine Funktion soll zwei Zahlen addieren - das ließe sich natürlich einfach mit dem +-Operator machen, aber darum geht es ja nicht. In jedem Fall würde man eine solche Funktion wohl so aufrufen:

int result = add( 1, 2 );

und anschließend erwarten, dass result den Wert 3 besitzt. Nun wissen wir, wie wir eine Funkion rufen würden, wenn wir sie schreiben könnten. Und wie das funktioniert schauen wir uns nun an.

Eigene Funktionen definieren

Nun haben wir den Aufruf einer Funktion gesehen. Allerdings sieht das doch anders aus, als die Definition einer Funktion, also wie wir es immer mit main machen. Eine Funktionsdefinition besteht aus dem Funktionskopf (oft auch Signatur genannt):

public static void main(String[] args)

und dem ausgeführten Code zwischen geschweiften Klammern. Der Funktionskopf enthält nur die Information, wie die Funktion verwendet werden kann - also wie sie heißt, welchen Datentyp sie zurück liefert und welche Parameter sie bekommt:

public static <Rückgabedatentyp> <Identifier>(<Parameterliste>)

Was genau die Schlüsselwörter public static machen werden wir an dieser Stelle vernachlässigen und uns an einem späteren Punkt genau ansehen.
Deklarieren wir mal eine Funktion, die zwei Integer-Werte addieren und entsprechend die Summe als Integer zurückliefern soll. Hierfür schreiben wir die Signatur einer solchen Funktion:

public static int add(int left, int right)

Um zu beschreiben, was die Funktion tun soll, öffnen wir nun einen Anweisungsblock mit einer geschweiften Klammer. In diesem Anweisungsblock stehen die Anweisungen, die die Funktion ausführen soll:

public static int add(int left, int right) {
    /* Anweisungen */
}

Die Signatur unserer Funktion sagt aus, dass ein Integerwert zurückgegeben wird. Wann immer ein Wert zurück gegeben wird, muss man klar aussagen, welchen Wert man zurück gibt. Um einen Wert zurückzugeben, verwendet man das Schlüsselwort return, gefolgt von dem gewünschten Wert.

public static int add(int left, int right) {
    /* Anweisungen */
    return 0; 
}

Nun wollen wir aber nicht grundsätzlich 0 zurückliefern. Stattdessen wollen wir die beiden Werte, die wir als Parameter erhalten (left und right) miteinander addieren und das Ergebnis zurückliefern.

public static int add(int left, int right) {
  int summe;
  summe = left + right; 
  return summe;
}

Wir können diese Funktion nun auch von main aus aufrufen:

class TestAdd {
 
    public static void main(String[] args) {
        int result = add(1, 2);
        System.out.println("Ergebnis: " + result);
    }
 
    public static int add(int left, int right) {
        int summe;
        summe = left + right; 
        return summe;
    }
 
}
Ergebnis: 3

Die Parameter left und right sind ganz normale Variablen, wie es summe auch ist. Sie können gelesen und beschrieben werden, der einzige Unterschied ist, dass sie durch den Aufrufer der Funktion bereits Werte zugewiesen bekommen haben. Die Aufgabe dieser Funktion ist es, diese beiden vom Aufrufer zugewiesenen Werte miteinander zu verrechnen.

Wir haben ja bereits gelernt, dass Java mit Expressions arbeitet. left und right sind Expressions vom Datentyp int. left + right ist eine Expression, die ebenfalls vom Datentyp int ist. Auch summe ist eine Expression vom Datentyp int, daher dürfen wir left + right der Variablen summe zuweisen. Die Funktion add liefert ebenfalls einen int zurück, deswegen darf summe per Return zurückgegeben werden. Alle int-Expressions sind miteinander austauschbar, denn aus allen kann ein int ausgelesen werden (Wichtig: Es geht ums Lesen: Der Expression left + right kann nichts zugewiesen werden, left oder right schon.).

Wir sorgen mit summe = left + right; dafür, dass in der Variablen summe der Wert gespeichert wird, den die Expression left + right erzeugt. Und wir speichern den Wert, damit wir den gleichen Wert später wieder auslesen können - hier wird der Wert bei return summe wieder ausgelesen.

Die return-Anweisung benötigt einen Integer, und die Expression left + right liefert ja einen Integer. Wir können uns das Zwischenspeichern in einer extra dafür angelegten Variable also sparen:

public static int add(int left, int right) {
  return left + right; 
}

Das macht den Quelltext kürzer und einfacher. Die Variable summe habe ich lediglich angelegt, um die Gleichartigkeit von lokalen Variablen und Parametern aufzuzeigen.

Alle Parameter werden kopiert, das bedeutet, dass beim Aufruf auch die Zahlen kopiert werden:

class Function {
 
    public static void main(String[] args) {
        int links = 1, rechts = 2;
        int summe = add(links, rechts);         // <--- Aufruf
        System.out.println("Ergebnis: " + summe);
    }
 
    public static int add(int left, int right) {
        int summe;
        summe = left + right; 
        return summe;
    }
 
}

Wie im obigen Beispiel ersichtlich, erfolgt der Aufruf einer Funktion durch die Verwendung des Namens, gefolgt von einem Klammernpaar und einem Semikolon. In den Klammern stehen entsprechend der Signatur erforderliche Paramter. Liefert die Funktion zusätzlich noch einen Wert zurück, muss dieser wie im Beispiel direkt einer Variable zugewiesen werden, ansonsten geht er verloren.
Denken wir wieder in Werten und Expressions: Beim Aufruf der Funktion add werden zwei Expressions vom Typ int angegeben. Diese beiden Expressions werden ausgewertet. links hat den Wert 1, rechts hat den Wert 2. Diese Werte werden jetzt in den Arbeitsbereich der Funktion kopiert. Diese Form des Aufrufs nennt man Call by Value, was soviel heißt wie „Aufruf mit Werten“. Man kann das so verstehen, dass vor dem Aufruf von add der Speicher für die Funktion bereitgestellt wird und dort, wo später die lokale Variable left liegt, wird der Wert der Expression (links) hineinkopiert und dort, wo später die lokale Variable right liegen wird, wird der Wert der Expression rechts hinkopiert.

Ich habe die Variablen hier in der Hauptfunktion main extra deutsch hingeschrieben, damit man sieht, dass die Namen der Variablen unterschiedlich sein dürfen, also die Namen der Variablen beim Aufruf überhaupt nichts mit dem Namen der Parametervariablen zu tun haben. Hier werden die Werte kopiert und existieren damit zweimal. Überschreibt man in der Funktion add nun den Wert für left, dann wirkt sich das nicht auf den Wert von links, die in Speicherbereich von der Funktion main als lokale Variable definiert ist.

Das lässt sich leicht vor Augen führen, wenn wir andere Expressions beim Aufruf auswerten:

int summe = add( 1, 2 );         // <--- Aufruf

1 ist ebenso eine Expression mit dem Datentyp int und dem Wert 1. Die Expression wird ausgewertet und der Wert 1 wird nun wieder in den Speicherbereich für add an die Stelle geschrieben, wo die Funktion später mit der Variablen left zugreift. Würde die Funktion add nun left überschreiben, wird nur die Kopie des Wertes überschrieben. Die 1, die beim Aufruf angegeben wurde, kann man natürlich nicht überschreiben, denn 1 ist eine konstante Zahl. Konstanten kann man - wie der Name schon sagt - nicht verändern.

Abbrechen einer Funktion

Mit der return-Anweisung kann man eine Funktion sofort verlassen. Sie ist damit in gewisser Weise verwandt mit der break-Anweisung für Schleifen. Wir haben im Kapitel über Wächter gesprochen.

Nehmen wir an, dass wir nun eine Funktion schreiben wollen, die 1 bei einer positiven Zahl, -1 bei einer negativen Zahl und 0 bei 0 zurückgeben soll.

public static int sign(int value) {
    int result;
 
    if(value != 0) {
        if(value > 0) result =  1;
        else          result = -1;
    }
    else              result =  0;
 
    return result;
}

Wenn value nicht 0 ist, dann wird geprüft, ob value größer oder kleiner 0 ist. Sonst ist das Ergebnis Null.

Wir wollen die Funktion nun aber anders neu schreiben, wobei wir die Variable result aber einsparen wollen und die Funktion verlassen wollen, sobald wir das Ergebnis kennen. Hierfür installieren wir Wächter. Mit return brechen wir die Funktion sofort ab und geben das Ergebnis zurück. Jeder nachfolgende Code der Funktion wird ignoriert.

public static int sign(int value) {
    if(value == 0)
        return 0;
 
    if(value > 0)
        return 1;
 
    return -1;
}

Wir sehen, dass die Funktion nun keine temporäre Variable mehr benötigt und sogar etwas kürzer ist. Am Schluss wird nicht mehr gefragt, ob value kleiner 0 ist, denn eine andere Möglichkeit bleibt schließlich nicht mehr über, wenn die beiden Wächter value schon die Fälle value gleich 0 und value größer 0 abgefangen haben.

Prozeduren

Eine Funktion hat in der Mathematik die Aufgabe aus diversen Eingabevariablen einen Funktionswert zu bestimmen. Nun kann man in Java aber auch Funktionen schreiben, die nichts zurückgeben. Will man die Tatsache betonen, dass es keine Funktionsrückgabe gibt, spricht man gelegentlich von 'void-Funktionen' oder aus dem Pascal-Sprachgebrauch von „Prozeduren“. Eine Prozedur handelt halt einige Anweisungen entsprechend der Übergabeparameter ab und fertig. Die Unterscheidung wurde in Pascal mit „procedure“ und „function“ vollzogen, in Java wird der Unterschied nicht so offensichtlich festgehalten: man gibt als Rückgabetyp einfach 'void' an.

public static void SayHello() {
    System.out.println("Hello\n");
}

Wie man sieht, fehlt hier auch die return-Anweisung am Ende. Da auch nichts zurückgegeben werden muss, kann man return auch keinen Wert übergeben und so endet die Prozedur, sobald die schließende geschweifte Klammer erreicht wird. Möchte man eine Funktion dennoch vorzeitig verlassen, zum Beispiel weil man nicht mehr als fünfmal hintereinander „Hallo“ sagen möchte, so kann man return auch ohne Parameter aufrufen:

public static void SayHello(int howOften) {
    for(int i = 0; i < howOften; i++) {
        if(i == 5)
            return;     // nach 5mal abbrechen.
        printf( "Hello\n" );
    }
}    

Dieser Code zeigt nur, dass man return jederzeit verwenden kann. An dieser Stelle wäre es schöner vor der Schleife die Variable howOften einmalig zu überprüfen und falls sie größer als 5 ist, sie auf 5 zu korrigeren. Damit kann man sich die Abfrage innerhalb der Schleife wieder sparen. Probiert das doch mal als kleine Übung :-)

Rekursion

Ich werde Dir nun eine Funktion zeigen, die uns im Verlauf des Tutorials noch häufiger begegnen wird. Sie berechnet ein Element der Fibonacci-Folge.

Folgendes Problem: Ein Element der Fibonacci-Folge ist definiert über die natürlichen Zahlen und entspricht der Summe der beiden vorangegangenen Elemente. Eine Besonderheit gilt für die beiden ersten Elemente, da sie natürlich nicht über zwei Vorgänger verfügen. Hier gilt, dass das 0. Element 0 ist und das 1. Element den Wert 1 besitzt.

fib(0) = 0
fib(1) = 1
fib(n) = fib( n-1 ) + fib( n-2 )

Also gilt für die Fibonacci-Folge: 0, 1, 1, 2, 3, 5, 8… usw. Die Reihe sieht zunächst ziemlich langweilig aus, aber Fibonacci wird uns noch auf viele Abenteuer der Programmierung begleiten. Ihre Formulierung ist jedoch etwas besonderes, denn der n.-Wert hängt vom n-1. und dem n-2. Wert ab. Eine solche Funktion wird rekursiv genannt.

Schauen wir uns die Hauptproblem an:

public static int fib(int n) {
  return fib(n - 1) + fib(n - 2);
}

Das kann man so nicht stehen lassen, denn hier würde die Funktion sich bis in alle Ewigkeit selbst aufrufen. Da jeder Funktionsaufruf ein wenig Speicher kostet, wird das Programm irgendwann wegen Speichermangel abstürzen. Wir müssen also beschreiben, wann die Rekursion enden soll. Diese Bedingung nennt man Rekursionsanker und wir werden diesen Anker hier als Wächter implementieren. Denn wir haben ja noch unsere beiden Sonderfälle bei den Indizes 0 und 1. Hierfür positionieren wir einen passenden Wächter:

public static int fib(int n) {
    if(n <= 1)
        return n;
 
    return fib(n - 1) + fib(n - 2);
}

Wird die Funktion fib mit den Werten 0 oder 1 für n gerufen, so wird 0 bzw. 1 zurückgegeben. Das passt also. Schauen wir uns den Aufruf für 2 an, ruft sie sich selbst für die Werte 1 und 0 und addiert die Rückgaben: für fib(2) erhalten wir also 1.

Das ganze als vollständiges Programm:

class Fibonacci {
 
    public static void main(String[] args) {
        int index = 0;
        while( index <= 10 ) {
            System.out.println( "fib( " + index + " ) => " + fib(index));
            index = index + 1;
        }
    }
 
    public static int fib(int n) {
        if(n <= 1)
            return n;
 
        return fib(n - 1) + fib(n - 2);
    }
 
}

Wir bekommen folgende Ausgabe:

fib( 0 ) => 0
fib( 1 ) => 1
fib( 2 ) => 1
fib( 3 ) => 2
fib( 4 ) => 3
fib( 5 ) => 5
fib( 6 ) => 8
fib( 7 ) => 13
fib( 8 ) => 21
fib( 9 ) => 34
fib( 10 ) => 55

Und das entspricht ja - wie gewünscht - genau der Fibonacci-Folge.

So gemächlich die Fibunacci-Folge erstmal aussieht, sie steigt sehr schnell an. Und diese Form der Implementierung sorgt dafür, dass damit ein moderner Rechner relativ schnell an seine Grenzen stößt.

Ziel dieser Lektion

Du solltest nun in der Lage sein, die Signatur einer Funktion von ihrem Code zu unterscheiden.

Die Bedeutung Expressions und Werten sollte Dir weiterhin bewusst sein.

Wir werden in den kommenden Kapiteln noch eine Vielzahl von Funktionen schreiben, so dass Dir der Aufbau von Funktionen mit anderen Parametern und Rückgabeparametern sicherlich bald in Fleisch und Blut übergeht, doch Du solltest den grundsätzlichen Aufbau einer Funktion verstanden haben und wissen, dass Funktionen andere Funktionen rufen können (main() ruft fib()) und sich selbst rufen können (fib() ruft fib()). Wenn Funktionen sich selbst rufen, nennt man dies einen rekursiven Aufruf. Rekursive Funktionen brauchen einen Rekursionsanker, also eine Bedingung, die dafür sorgt, dass sich die Funktion irgendwann aufhört, sich selbst zu rufen.

In der nächsten Lektion werden wir uns genauer mit Kommentaren beschäftigen.