Programmation Java avec des expressions lambda

Dans le discours d'ouverture technique de JavaOne 2013, Mark Reinhold, architecte en chef du Java Platform Group chez Oracle, a décrit les expressions lambda comme la plus grande mise à niveau jamais réalisée du modèle de programmation Java . Bien qu'il existe de nombreuses applications pour les expressions lambda, cet article se concentre sur un exemple spécifique qui se produit fréquemment dans les applications mathématiques; à savoir, la nécessité de passer une fonction à un algorithme.

En tant que geek aux cheveux gris, j'ai programmé dans de nombreux langages au fil des ans, et j'ai beaucoup programmé en Java depuis la version 1.1. Quand j'ai commencé à travailler avec des ordinateurs, presque personne n'avait de diplôme en informatique. Les professionnels de l'informatique venaient principalement d'autres disciplines telles que l'électrotechnique, la physique, les affaires et les mathématiques. Dans ma propre vie antérieure, j'étais mathématicien, et il n'est donc pas surprenant que ma vision initiale d'un ordinateur soit celle d'une calculatrice programmable géante. J'ai considérablement élargi ma vision des ordinateurs au fil des ans, mais je suis toujours ravi de pouvoir travailler sur des applications qui impliquent certains aspects des mathématiques.

De nombreuses applications en mathématiques exigent qu'une fonction soit passée en paramètre à un algorithme. Des exemples de l'algèbre universitaire et du calcul de base comprennent la résolution d'une équation ou le calcul de l'intégrale d'une fonction. Depuis plus de 15 ans, Java a été mon langage de programmation de choix pour la plupart des applications, mais c'était le premier langage que j'utilisais fréquemment et qui ne me permettait pas de passer une fonction (techniquement un pointeur ou une référence à une fonction) en tant que paramètre d'une manière simple et directe. Cette lacune est sur le point de changer avec la prochaine version de Java 8.

La puissance des expressions lambda va bien au-delà d'un seul cas d'utilisation, mais l'étude de diverses implémentations du même exemple devrait vous laisser une idée précise de la manière dont les lambdas bénéficieront à vos programmes Java. Dans cet article, je vais utiliser un exemple courant pour aider à décrire le problème, puis fournir des solutions écrites en C ++, Java avant les expressions lambda et Java avec des expressions lambda. Notez qu'une solide expérience en mathématiques n'est pas nécessaire pour comprendre et apprécier les principaux points de cet article.

En savoir plus sur les lambdas

Les expressions lambda, également appelées fermetures, littéraux de fonction ou simplement lambdas, décrivent un ensemble de fonctionnalités définies dans Java Specification Request (JSR) 335. Des introductions moins formelles / plus lisibles aux expressions lambda sont fournies dans une section de la dernière version du Tutoriel Java et dans quelques articles de Brian Goetz, "State of the lambda" et "State of the lambda: Libraries edition". Ces ressources décrivent la syntaxe des expressions lambda et fournissent des exemples de cas d'utilisation où les expressions lambda sont applicables. Pour en savoir plus sur les expressions lambda dans Java 8, regardez le discours d'ouverture technique de Mark Reinhold pour JavaOne 2013.

Expressions lambda dans un exemple mathématique

L'exemple utilisé tout au long de cet article est la règle de Simpson du calcul de base. La règle de Simpson, ou plus spécifiquement la règle de Simpson composite, est une technique d'intégration numérique pour approcher une intégrale définie. Ne vous inquiétez pas si vous n'êtes pas familier avec le concept d'une intégrale définie ; ce que vous devez vraiment comprendre, c'est que la règle de Simpson est un algorithme qui calcule un nombre réel basé sur quatre paramètres:

  • Une fonction que nous voulons intégrer.
  • Deux nombres réels aet bqui représentent les extrémités d'un intervalle [a,b]sur la droite numérique réelle. (Notez que la fonction mentionnée ci-dessus doit être continue sur cet intervalle.)
  • Un entier pair nqui spécifie un certain nombre de sous-intervalles. En mettant en œuvre la règle de Simpson, nous divisons l'intervalle [a,b]en nsous-intervalles.

Pour simplifier la présentation, concentrons-nous sur l'interface de programmation et non sur les détails d'implémentation. (En vérité, j'espère que cette approche nous permettra de contourner les arguments sur la manière la meilleure ou la plus efficace d'implémenter la règle de Simpson, qui n'est pas l'objet de cet article.) Nous utiliserons le type doublepour les paramètres aet b, et nous utiliserons le type intpour le paramètre n. La fonction à intégrer prendra un seul paramètre de type doubleet un retour une valeur de type double.

Télécharger Téléchargez l'exemple de code source C ++ pour cet article. Créé par John I. Moore pour JavaWorld

Paramètres de fonction en C ++

Pour fournir une base de comparaison, commençons par une spécification C ++. Lors du passage d'une fonction en tant que paramètre en C ++, je préfère généralement spécifier la signature du paramètre de fonction à l'aide d'un typedef. Le listing 1 montre un fichier d'en-tête C ++ nommé simpson.hqui spécifie à la fois le typedefpour le paramètre de fonction et l'interface de programmation pour une fonction C ++ nommée integrate. Le corps de la fonction pour integrateest contenu dans un fichier de code source C ++ nommé simpson.cpp(non illustré) et fournit l'implémentation de la règle de Simpson.

Liste 1. Fichier d'en-tête C ++ pour la règle de 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 

L'appel integrateest simple en C ++. À titre d'exemple simple, supposons que vous vouliez utiliser la règle de Simpson pour approximer l'intégrale de la fonction sinus de 0à π ( PI) en utilisant des 30sous-intervalles. (Quiconque a terminé Calculus, je devrais être en mesure de calculer la réponse exactement sans l'aide d'une calculatrice, ce qui en fait un bon cas de test pour la integratefonction.) En supposant que vous ayez inclus les fichiers d'en-tête appropriés tels que et "simpson.h", vous seriez en mesure pour appeler la fonction integratecomme indiqué dans la liste 2.

Listing 2. Appel C ++ à l'intégration de fonction

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

C'est tout ce qu'on peut en dire. En C ++, vous passez la fonction sinus aussi facilement que vous passez les trois autres paramètres.

Un autre exemple

Au lieu de la règle de Simpson, j'aurais pu tout aussi facilement utiliser la méthode de bisection ( alias l'algorithme de bisection) pour résoudre une équation de la forme f (x) = 0 . En fait, le code source de cet article comprend des implémentations simples de la règle de Simpson et de la méthode de bisection.

Télécharger Téléchargez les exemples de code source Java pour cet article. Créé par John I. Moore pour JavaWorld

Java sans expressions lambda

Voyons maintenant comment la règle de Simpson pourrait être spécifiée en Java. Que nous utilisions ou non des expressions lambda, nous utilisons l'interface Java présentée dans le Listing 3 à la place du C ++ typedefpour spécifier la signature du paramètre de fonction.

Listing 3. Interface Java pour le paramètre de fonction

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

Pour implémenter la règle de Simpson en Java, nous créons une classe nommée Simpsonqui contient une méthode,, integrateavec quatre paramètres similaires à ce que nous avons fait en C ++. Comme pour beaucoup de méthodes mathématiques autonomes (voir, par exemple, java.lang.Math), nous allons créer integrateune méthode statique. La méthode integrateest spécifiée comme suit:

Listing 4. Signature Java pour la méthode intégrée dans la classe Simpson

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

Tout ce que nous avons fait jusqu'à présent en Java est indépendant de l'utilisation ou non d'expressions lambda. La principale différence avec les expressions lambda réside dans la façon dont nous transmettons les paramètres (plus précisément, la façon dont nous transmettons le paramètre de fonction) dans un appel à une méthode integrate. Je vais d'abord illustrer comment cela serait fait dans les versions de Java antérieures à la version 8; c'est-à-dire sans expressions lambda. Comme avec l'exemple C ++, supposons que nous voulons approcher l'intégrale de la fonction sinus de 0à π ( PI) en utilisant des 30sous-intervalles.

Utilisation du modèle d'adaptateur pour la fonction sinus

En Java, nous avons une implémentation de la fonction sinus disponible dans java.lang.Math, mais avec les versions de Java antérieures à Java 8, il n'y a pas de moyen simple et direct de transmettre cette fonction sinus à la méthode integratede la classe Simpson. Une approche consiste à utiliser le modèle d'adaptateur. Dans ce cas, nous écririons une classe d'adaptateur simple qui implémente l' DoubleFunctioninterface et l'adapte pour appeler la fonction sinus , comme indiqué dans le Listing 5.

Listing 5. Classe d'adaptateur pour la méthode Math.sin

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

En utilisant cette classe d'adaptateur, nous pouvons maintenant appeler la integrateméthode de la classe Simpsoncomme indiqué dans le Listing 6.

Listing 6. Utilisation de la classe d'adaptateur pour appeler la méthode 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

Une autre fonctionnalité connexe de Java 8 est ce qu'on appelle une référence de méthode , qui nous permet de faire référence à une méthode existante par son nom. Les références de méthode peuvent être utilisées à la place des expressions lambda tant qu'elles satisfont aux exigences de l'interface fonctionnelle. Comme décrit dans les ressources, il existe plusieurs types de références de méthodes, chacune avec une syntaxe légèrement différente. Pour les méthodes statiques, la syntaxe est Classname::methodName. Par conséquent, en utilisant une référence de méthode, nous pouvons appeler la integrateméthode en Java aussi simplement que nous le pourrions en C ++. Comparez l'appel Java 8 présenté dans le listing 10 ci-dessous avec l'appel C ++ d'origine montré dans le listing 2 ci-dessus.

Listing 10. Utilisation d'une référence de méthode pour appeler Simpson.integrate

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