Java-Programmierung mit Lambda-Ausdrücken

In der technischen Keynote zu JavaOne 2013 beschrieb Mark Reinhold, Chefarchitekt der Java Platform Group bei Oracle, Lambda-Ausdrücke als das größte Upgrade des Java-Programmiermodells aller Zeiten . Obwohl es viele Anwendungen für Lambda-Ausdrücke gibt, konzentriert sich dieser Artikel auf ein bestimmtes Beispiel, das in mathematischen Anwendungen häufig vorkommt. nämlich die Notwendigkeit, eine Funktion an einen Algorithmus zu übergeben.

Als grauhaariger Geek habe ich im Laufe der Jahre in zahlreichen Sprachen programmiert und seit Version 1.1 ausgiebig in Java programmiert. Als ich anfing, mit Computern zu arbeiten, hatte fast niemand einen Abschluss in Informatik. Computerfachleute kamen hauptsächlich aus anderen Disziplinen wie Elektrotechnik, Physik, Wirtschaft und Mathematik. In meinem früheren Leben war ich Mathematiker, und so sollte es keine Überraschung sein, dass meine anfängliche Ansicht eines Computers die eines riesigen programmierbaren Taschenrechners war. Ich habe meine Sicht auf Computer im Laufe der Jahre erheblich erweitert, aber ich begrüße immer noch die Möglichkeit, an Anwendungen zu arbeiten, die einen Aspekt der Mathematik betreffen.

Viele Anwendungen in der Mathematik erfordern, dass eine Funktion als Parameter an einen Algorithmus übergeben wird. Beispiele aus der College-Algebra und der Grundrechnung sind das Lösen einer Gleichung oder das Berechnen des Integrals einer Funktion. Seit über 15 Jahren ist Java für die meisten Anwendungen meine bevorzugte Programmiersprache, aber es war die erste Sprache, die ich häufig verwendete und die es mir nicht erlaubte, eine Funktion (technisch ein Zeiger oder Verweis auf eine Funktion) als zu übergeben Parameter auf einfache und unkomplizierte Weise. Dieses Manko wird sich mit der bevorstehenden Veröffentlichung von Java 8 ändern.

Die Leistungsfähigkeit von Lambda-Ausdrücken geht weit über einen einzelnen Anwendungsfall hinaus. Wenn Sie jedoch verschiedene Implementierungen desselben Beispiels studieren, sollten Sie ein solides Gefühl dafür haben, wie Lambdas Ihren Java-Programmen zugute kommen. In diesem Artikel werde ich ein allgemeines Beispiel verwenden, um das Problem zu beschreiben, und dann Lösungen bereitstellen, die in C ++, Java vor Lambda-Ausdrücken und Java mit Lambda-Ausdrücken geschrieben wurden. Beachten Sie, dass ein starker mathematischer Hintergrund nicht erforderlich ist, um die wichtigsten Punkte dieses Artikels zu verstehen und zu würdigen.

Lambdas lernen

Lambda-Ausdrücke, auch als Closures, Funktionsliterale oder einfach Lambdas bezeichnet, beschreiben eine Reihe von Funktionen, die in Java Specification Request (JSR) 335 definiert sind. Weniger formale / besser lesbare Einführungen in Lambda-Ausdrücke finden Sie in einem Abschnitt der neuesten Version von Java Tutorial und in einigen Artikeln von Brian Goetz, "State of the Lambda" und "State of the Lambda: Libraries Edition". Diese Ressourcen beschreiben die Syntax von Lambda-Ausdrücken und bieten Beispiele für Anwendungsfälle, in denen Lambda-Ausdrücke anwendbar sind. Weitere Informationen zu Lambda-Ausdrücken in Java 8 finden Sie in der technischen Keynote von Mark Reinhold für JavaOne 2013.

Lambda-Ausdrücke in einem mathematischen Beispiel

Das in diesem Artikel verwendete Beispiel ist die Simpsonsche Regel aus der Grundrechnung. Die Simpson-Regel, genauer gesagt die zusammengesetzte Simpson-Regel, ist eine numerische Integrationstechnik zur Approximation eines bestimmten Integrals. Machen Sie sich keine Sorgen, wenn Sie mit dem Konzept eines bestimmten Integrals nicht vertraut sind . Was Sie wirklich verstehen müssen, ist, dass die Simpson-Regel ein Algorithmus ist, der eine reelle Zahl basierend auf vier Parametern berechnet:

  • Eine Funktion, die wir integrieren wollen.
  • Zwei reelle Zahlen aund bdie darstellen , die Endpunkte eines Intervalls [a,b]auf der reellen Zahlengeraden. (Beachten Sie, dass die oben genannte Funktion in diesem Intervall kontinuierlich sein sollte.)
  • Eine gerade Ganzzahl n, die eine Reihe von Teilintervallen angibt. Bei der Implementierung der Simpson-Regel teilen wir das Intervall [a,b]in nTeilintervalle auf.

Um die Präsentation zu vereinfachen, konzentrieren wir uns auf die Programmierschnittstelle und nicht auf die Implementierungsdetails. (Ehrlich gesagt hoffe ich, dass wir mit diesem Ansatz Argumente über die beste oder effizienteste Methode zur Implementierung der Simpson-Regel umgehen können, die nicht im Mittelpunkt dieses Artikels steht.) Wir werden type doublefor parameters aund btype intfor parameter verwenden n. Die zu integrierende Funktion verwendet einen einzelnen Parameter vom Typ doubleund gibt einen Wert vom Typ zurück double.

Download Laden Sie das C ++ - Quellcodebeispiel für diesen Artikel herunter. Erstellt von John I. Moore für JavaWorld

Funktionsparameter in C ++

Beginnen wir mit einer C ++ - Spezifikation, um eine Vergleichsbasis zu schaffen. Wenn ich eine Funktion als Parameter in C ++ übergebe, ziehe ich es normalerweise vor, die Signatur des Funktionsparameters mit a anzugeben typedef. Listing 1 zeigt eine C ++ - Headerdatei mit dem Namen simpson.h, die sowohl den typedefFunktionsparameter als auch die Programmierschnittstelle für eine C ++ - Funktion mit dem Namen angibt integrate. Der Funktionskörper für integrateist in einer C ++ - Quellcodedatei mit dem Namen simpson.cpp(nicht gezeigt) enthalten und stellt die Implementierung für die Simpson-Regel bereit.

Listing 1. C ++ - Headerdatei für Simpsons Regel

 #if !defined(SIMPSON_H) #define SIMPSON_H #include  using namespace std; typedef double DoubleFunction(double x); double integrate(DoubleFunction f, double a, double b, int n) throw(invalid_argument); #endif 

Das Aufrufen integrateist in C ++ unkompliziert. Nehmen wir als einfaches Beispiel an, Sie wollten die Simpsonsche Regel verwenden, um das Integral der Sinusfunktion mithilfe von Teilintervallen von 0π ( PI) zu approximieren 30. (Wer Analysis I abgeschlossen hat , sollte die Antwort genau , ohne die Hilfe eines Rechners berechnen können, so dass dies ein guter Testfall für die integrateFunktion.) Unter der Annahme , dass Sie hatte enthalten die entsprechenden Header - Dateien wie und "simpson.h"würden Sie in der Lage sein , um die Funktion integratewie in Listing 2 gezeigt aufzurufen .

Listing 2. C ++ - Aufruf zur Funktionsintegration

 double result = integrate(sin, 0, M_PI, 30); 

Das ist alles dazu. In C ++ übergeben Sie die Sinusfunktion genauso einfach wie die anderen drei Parameter.

Ein anderes Beispiel

Anstelle von Simpsons Regel hätte ich genauso gut die Bisektionsmethode (auch bekannt als Bisektionsalgorithmus) zum Lösen einer Gleichung der Form f (x) = 0 verwenden können . Tatsächlich enthält der Quellcode für diesen Artikel einfache Implementierungen sowohl der Simpson-Regel als auch der Bisektionsmethode.

Download Laden Sie die Java-Quellcodebeispiele für diesen Artikel herunter. Erstellt von John I. Moore für JavaWorld

Java ohne Lambda-Ausdrücke

Schauen wir uns nun an, wie die Simpson-Regel in Java angegeben werden kann. Unabhängig davon, ob wir Lambda-Ausdrücke verwenden oder nicht, verwenden wir anstelle von C ++ die in Listing 3 gezeigte Java-Schnittstelle, typedefum die Signatur des Funktionsparameters anzugeben.

Listing 3. Java-Schnittstelle für den Funktionsparameter

 public interface DoubleFunction { public double f(double x); } 

Um Simpsons Regel in Java zu implementieren, erstellen wir eine Klasse mit dem Namen Simpson, die eine Methode enthält integrate, mit vier Parametern, die denen in C ++ ähneln. Wie bei vielen in sich geschlossenen mathematischen Methoden (siehe zum Beispiel java.lang.Math) werden wir integrateeine statische Methode erstellen. Die Methode integratewird wie folgt angegeben:

Listing 4. Java-Signatur für die in die Klasse Simpson integrierte Methode

 public static double integrate(DoubleFunction df, double a, double b, int n) 

Everything that we've done thus far in Java is independent of whether or not we will use lambda expressions. The primary difference with lambda expressions is in how we pass parameters (more specifically, how we pass the function parameter) in a call to method integrate. First I'll illustrate how this would be done in versions of Java prior to version 8; i.e., without lambda expressions. As with the C++ example, assume that we want to approximate the integral of the sine function from 0 to π (PI) using 30 subintervals.

Using the Adapter pattern for the sine function

In Java we have an implementation of the sine function available in java.lang.Math, but with versions of Java prior to Java 8, there is no simple, direct way to pass this sine function to the method integrate in class Simpson. One approach is to use the Adapter pattern. In this case we would write a simple adapter class that implements the DoubleFunction interface and adapts it to call the sine function, as shown in Listing 5.

Listing 5. Adapter class for method Math.sin

 import com.softmoore.math.DoubleFunction; public class DoubleFunctionSineAdapter implements DoubleFunction { public double f(double x) { return Math.sin(x); } } 

Using this adapter class we can now call the integrate method of class Simpson as shown in Listing 6.

Listing 6. Using the adapter class to call method Simpson.integrate

 DoubleFunctionSineAdapter sine = new DoubleFunctionSineAdapter(); double result = Simpson.integrate(sine, 0, Math.PI, 30); 

Let's stop a moment and compare what was required to make the call to integrate in C++ versus what was required in earlier versions of Java. With C++, we simply called integrate, passing in the four parameters. With Java, we had to create a new adapter class and then instantiate this class in order to make the call. If we wanted to integrate several functions, we would need to write an adapter class for each of them.

We could shorten the code needed to call integrate slightly from two Java statements to one by creating the new instance of the adapter class within the call to integrate. Using an anonymous class rather than creating a separate adapter class would be another way to slightly reduce the overall effort, as shown in Listing 7.

Listing 7. Using an anonymous class to call method Simpson.integrate

 DoubleFunction sineAdapter = new DoubleFunction() { public double f(double x) { return Math.sin(x); } }; double result = Simpson.integrate(sineAdapter, 0, Math.PI, 30); 

Without lambda expressions, what you see in Listing 7 is about the least amount of code that you could write in Java to call the integrate method, but it is still much more cumbersome than what was required for C++. I am also not that happy with using anonymous classes, although I have used them a lot in the past. I dislike the syntax and have always considered it to be a clumsy but necessary hack in the Java language.

Java with lambda expressions and functional interfaces

Now let's look at how we could use lambda expressions in Java 8 to simplify the call to integrate in Java. Because the interface DoubleFunction requires the implementation of only a single method it is a candidate for lambda expressions. If we know in advance that we are going to use lambda expressions, we can annotate the interface with @FunctionalInterface, a new annotation for Java 8 that says we have a functional interface. Note that this annotation is not required, but it gives us an extra check that everything is consistent, similar to the @Override annotation in earlier versions of Java.

The syntax of a lambda expression is an argument list enclosed in parentheses, an arrow token (->), and a function body. The body can be either a statement block (enclosed in braces) or a single expression. Listing 8 shows a lambda expression that implements the interface DoubleFunction and is then passed to method integrate.

Listing 8. Using a lambda expression to call method Simpson.integrate

 DoubleFunction sine = (double x) -> Math.sin(x); double result = Simpson.integrate(sine, 0, Math.PI, 30); 

Note that we did not have to write the adapter class or create an instance of an anonymous class. Also note that we could have written the above in a single statement by substituting the lambda expression itself, (double x) -> Math.sin(x), for the parameter sine in the second statement above, eliminating the first statement. Now we are getting much closer to the simple syntax that we had in C++. But wait! There's more!

The name of the functional interface is not part of the lambda expression but can be inferred based on the context. The type double for the parameter of the lambda expression can also be inferred from the context. Finally, if there is only one parameter in the lambda expression, then we can omit the parentheses. Thus we can abbreviate the code to call method integrate to a single line of code, as shown in Listing 9.

Listing 9. An alternate format for lambda expression in call to Simpson.integrate

 double result = Simpson.integrate(x -> Math.sin(x), 0, Math.PI, 30); 

But wait! There's even more!

Method references in Java 8

Eine weitere verwandte Funktion in Java 8 ist eine sogenannte Methodenreferenz , mit der wir eine vorhandene Methode mit Namen referenzieren können. Methodenreferenzen können anstelle von Lambda-Ausdrücken verwendet werden, sofern sie den Anforderungen der Funktionsschnittstelle entsprechen. Wie in den Ressourcen beschrieben, gibt es verschiedene Arten von Methodenreferenzen mit jeweils leicht unterschiedlicher Syntax. Für statische Methoden lautet die Syntax Classname::methodName. Daher können wir mithilfe einer Methodenreferenz die integrateMethode in Java so einfach wie in C ++ aufrufen . Vergleichen Sie den in Listing 10 unten gezeigten Java 8-Aufruf mit dem in Listing 2 oben gezeigten ursprünglichen C ++ - Aufruf.

Listing 10. Verwenden einer Methodenreferenz zum Aufrufen von Simpson.integrate

 double result = Simpson.integrate(Math::sin, 0, Math.PI, 30);