Programmazione Java con espressioni lambda

Nel discorso chiave tecnico per JavaOne 2013, Mark Reinhold, capo architetto del Java Platform Group presso Oracle, ha descritto le espressioni lambda come il più grande aggiornamento mai realizzato al modello di programmazione Java . Sebbene esistano molte applicazioni per le espressioni lambda, questo articolo si concentra su un esempio specifico che si verifica frequentemente nelle applicazioni matematiche; vale a dire, la necessità di passare una funzione a un algoritmo.

Come un geek dai capelli grigi, ho programmato in numerose lingue nel corso degli anni e ho programmato ampiamente in Java dalla versione 1.1. Quando ho iniziato a lavorare con i computer, quasi nessuno aveva una laurea in informatica. I professionisti del computer provenivano principalmente da altre discipline come ingegneria elettrica, fisica, economia e matematica. Nella mia vita precedente ero un matematico, quindi non dovrebbe sorprendere che la mia visione iniziale di un computer fosse quella di una gigantesca calcolatrice programmabile. Ho ampliato notevolmente la mia visione dei computer nel corso degli anni, ma accolgo comunque l'opportunità di lavorare su applicazioni che coinvolgono alcuni aspetti della matematica.

Molte applicazioni in matematica richiedono che una funzione venga passata come parametro a un algoritmo. Esempi di algebra universitaria e calcolo di base includono la risoluzione di un'equazione o il calcolo dell'integrale di una funzione. Per oltre 15 anni Java è stato il mio linguaggio di programmazione preferito per la maggior parte delle applicazioni, ma è stato il primo linguaggio che ho usato frequentemente che non mi ha permesso di passare una funzione (tecnicamente un puntatore o un riferimento a una funzione) come parametro in modo semplice e diretto. Questa lacuna sta per cambiare con l'imminente rilascio di Java 8.

La potenza delle espressioni lambda si estende ben oltre un singolo caso d'uso, ma lo studio di varie implementazioni dello stesso esempio dovrebbe lasciarti con un solido senso di come le lambda beneficeranno i tuoi programmi Java. In questo articolo userò un esempio comune per descrivere il problema, quindi fornirò soluzioni scritte in C ++, Java prima delle espressioni lambda e Java con espressioni lambda. Nota che non è richiesta una solida preparazione in matematica per comprendere e apprezzare i punti principali di questo articolo.

Imparare a conoscere i lambda

Le espressioni lambda, note anche come chiusure, letterali di funzione o semplicemente lambda, descrivono un insieme di funzionalità definite in JSR (Java Specification Request) 335. In una sezione dell'ultima versione del Java Tutorial e in un paio di articoli di Brian Goetz, "State of the lambda" e "State of the lambda: Libraries edition". Queste risorse descrivono la sintassi delle espressioni lambda e forniscono esempi di casi d'uso in cui sono applicabili espressioni lambda. Per ulteriori informazioni sulle espressioni lambda in Java 8, guarda il discorso tecnico di Mark Reinhold per JavaOne 2013.

Espressioni lambda in un esempio matematico

L'esempio utilizzato in questo articolo è la regola di Simpson dal calcolo di base. La regola di Simpson, o più specificamente la regola di Simpson composita, è una tecnica di integrazione numerica per approssimare un integrale definito. Non preoccuparti se non hai familiarità con il concetto di integrale definito ; quello che devi veramente capire è che la regola di Simpson è un algoritmo che calcola un numero reale basato su quattro parametri:

  • Una funzione che vogliamo integrare.
  • Due numeri reali ae bche rappresentano i punti finali di un intervallo [a,b]sulla linea dei numeri reali. (Notare che la funzione di cui sopra dovrebbe essere continua su questo intervallo.)
  • Un numero intero pari nche specifica un numero di sottointervalli. Nell'implementazione della regola di Simpson, dividiamo l'intervallo [a,b]in nsottointervalli.

Per semplificare la presentazione, concentriamoci sull'interfaccia di programmazione e non sui dettagli di implementazione. (Sinceramente, spero che questo approccio ci permetta di ignorare gli argomenti sul modo migliore o più efficiente per implementare la regola di Simpson, che non è il focus di questo articolo.) Useremo il tipo doubleper i parametri ae b, e useremo il tipo intper il parametro n. La funzione da integrare prenderà un singolo parametro di tipo doublee restituirà un valore di tipo double.

Scarica Scarica l'esempio di codice sorgente C ++ per questo articolo. Creato da John I. Moore per JavaWorld

Parametri di funzione in C ++

Per fornire una base per il confronto, iniziamo con una specifica C ++. Quando si passa una funzione come parametro in C ++, di solito preferisco specificare la firma del parametro della funzione utilizzando un file typedef. Il Listato 1 mostra un file di intestazione C ++ denominato simpson.hche specifica sia il typedefparametro per la funzione che l'interfaccia di programmazione per una funzione C ++ denominata integrate. Il corpo della funzione per integrateè contenuto in un file di codice sorgente C ++ denominato simpson.cpp(non mostrato) e fornisce l'implementazione per la regola di Simpson.

Listato 1. File di intestazione C ++ per la regola di Simpson

 #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 

La chiamata integrateè semplice in C ++. Come semplice esempio, supponiamo di voler usare la regola di Simpson per approssimare l'integrale della funzione seno da 0a π ( PI) usando 30sottointervalli. (Chiunque abbia completato Calculus dovrei essere in grado di calcolare la risposta esattamente senza l'aiuto di una calcolatrice, rendendolo un buon test case per la integratefunzione.) Supponendo che tu abbia incluso i file di intestazione corretti come e "simpson.h", saresti in grado per chiamare la funzione integratecome mostrato nel Listato 2.

Listato 2. Chiamata in C ++ alla funzione integrate

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

È tutto quello che c'è da fare. In C ++ si passa la funzione sine con la stessa facilità con cui si passano gli altri tre parametri.

Un altro esempio

Invece della regola di Simpson avrei potuto usare altrettanto facilmente il metodo di bisezione ( noto anche come algoritmo di bisezione) per risolvere un'equazione della forma f (x) = 0 . In effetti, il codice sorgente di questo articolo include semplici implementazioni sia della regola di Simpson che del metodo di bisezione.

Scarica Scarica gli esempi di codice sorgente Java per questo articolo. Creato da John I. Moore per JavaWorld

Java senza espressioni lambda

Ora diamo un'occhiata a come potrebbe essere specificata la regola di Simpson in Java. Indipendentemente dal fatto che stiamo usando o meno espressioni lambda, usiamo l'interfaccia Java mostrata nel Listato 3 al posto del C ++ typedefper specificare la firma del parametro della funzione.

Listato 3. Interfaccia Java per il parametro della funzione

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

Per implementare la regola di Simpson in Java creiamo una classe denominata Simpsonche contiene un metodo integrate, con quattro parametri simili a quello che abbiamo fatto in C ++. Come con molti metodi matematici autonomi (vedi, ad esempio, java.lang.Math), creeremo integrateun metodo statico. Il metodo integrateè specificato come segue:

Listato 4. Firma Java per il metodo integrato nella classe Simpson

 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

Un'altra caratteristica correlata in Java 8 è qualcosa chiamato riferimento al metodo , che ci consente di fare riferimento a un metodo esistente per nome. I riferimenti ai metodi possono essere utilizzati al posto delle espressioni lambda purché soddisfino i requisiti dell'interfaccia funzionale. Come descritto nelle risorse, esistono diversi tipi di riferimenti al metodo, ciascuno con una sintassi leggermente diversa. Per i metodi statici la sintassi è Classname::methodName. Pertanto, utilizzando un riferimento al metodo, possiamo chiamare il integratemetodo in Java nel modo più semplice possibile in C ++. Confronta la chiamata Java 8 mostrata nel Listato 10 sotto con la chiamata C ++ originale mostrata nel Listato 2 sopra.

Listato 10. Utilizzo di un metodo di riferimento per chiamare Simpson.integrate

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