====== Funktionen ====== * Deklaration einer Funktion * Definition einer Funktion * Abbrechen einer Funktion * Rekursion Nach dem aufwendigen [[java:tutorial:loops|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 () 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 **[[glossary:callbyvalue|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 [[http://de.wikipedia.org/wiki/Fibonacci-Folge|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 [[java:tutorial:comments|nächsten Lektion]] werden wir uns genauer mit Kommentaren beschäftigen.