Програмиране на Java с ламбда изрази

В техническия основен адрес за JavaOne 2013, Марк Рейнхолд, главен архитект на Java Platform Group в Oracle, описва ламбда изразите като най-голямото надграждане до програмния модел на Java някога . Въпреки че има много приложения за ламбда изрази, тази статия се фокусира върху конкретен пример, който често се среща в математически приложения; а именно необходимостта от предаване на функция към алгоритъм.

Като сивокос човек съм програмирал на много езици през годините и програмирам широко в Java от версия 1.1. Когато започнах да работя с компютри, почти никой не е завършил компютърни науки. Компютърните професионалисти идват най-вече от други дисциплини като електротехника, физика, бизнес и математика. В моя предишен живот бях математик и затова не би трябвало да е изненадващо, че първоначалният ми възглед за компютър беше този за гигантски програмируем калкулатор. През годините значително разширих вижданията си за компютрите, но все пак приветствам възможността да работя върху приложения, които включват някои аспекти на математиката.

Много приложения в математиката изискват функцията да се предава като параметър на алгоритъм. Примерите от колежната алгебра и основното изчисление включват решаване на уравнение или изчисляване на интеграла на функция. Повече от 15 години Java е избраният от мен език за програмиране за повечето приложения, но това беше първият език, който използвах често, който не ми позволи да предам функция (технически указател или препратка към функция) като параметър по прост, ясен начин. Този недостатък е на път да се промени с предстоящото издание на Java 8.

Силата на ламбда изразите се простира далеч отвъд единичен случай на употреба, но изучаването на различни имплементации на същия пример трябва да ви остави със солидно усещане за това как ламбдите ще бъдат от полза за вашите Java програми. В тази статия ще използвам обикновен пример, за да опиша проблема, след което ще предложа решения, написани на C ++, Java преди ламбда изрази и Java с ламбда изрази. Имайте предвид, че не е необходим силен опит в математиката, за да се разберат и оценят основните точки на тази статия.

Учене за ламбди

Ламбда изрази, известни също като затваряния, функционални литерали или просто ламбда, описват набор от функции, дефинирани в Заявка за спецификация на Java (JSR) 335. По-малко формални / по-четливи въведения в ламбда изразите са предоставени в раздел от последната версия на Урок за Java и в няколко статии от Брайън Гьотц, „Състояние на ламбда“ и „Състояние на ламбда: издание на библиотеки“. Тези ресурси описват синтаксиса на ламбда изразите и предоставят примери за случаи на използване, където са приложими ламбда изрази. За повече информация относно ламбда изразите в Java 8, вижте техническия основен адрес на Марк Рейнхолд за JavaOne 2013.

Ламбда изрази в математически пример

Примерът, използван в тази статия, е Правилото на Симпсън от основното смятане. Правилото на Симпсън, или по-точно Композитното правило на Симпсън, е техника за числена интеграция за приближаване на определен интеграл. Не се притеснявайте, ако не сте запознати с концепцията за определен интеграл ; това, което наистина трябва да разберете, е, че Правилото на Симпсън е алгоритъм, който изчислява реално число въз основа на четири параметъра:

  • Функция, която искаме да интегрираме.
  • Две реални числа aи bкоито представляват крайните точки на интервал [a,b]на реалната числова линия. (Обърнете внимание, че функцията, посочена по-горе, трябва да е непрекъсната в този интервал.)
  • Четно цяло число, nкоето указва редица подинтервали. При прилагането на Правилото на Симпсън ние разделяме интервала [a,b]на nподинтервали.

За да опростим презентацията, нека се съсредоточим върху програмния интерфейс, а не върху подробностите за изпълнението. (Наистина, надявам се, че този подход ще ни позволи да заобиколим аргументите за най-добрия или най-ефективния начин за прилагане на правилото на Симпсън, което не е фокусът на тази статия.) Ще използваме тип doubleза параметри aи bще използваме тип intза параметър n. Функцията, която трябва да се интегрира, ще вземе един параметър от тип doubleи ще върне стойност от тип double.

Изтегляне Изтеглете примера на изходния код на C ++ за тази статия. Създадено от Джон И. Мур за JavaWorld

Функционални параметри в C ++

За да предоставим основа за сравнение, нека започнем със спецификация C ++. Когато предавам функция като параметър в C ++, обикновено предпочитам да посоча сигнатурата на параметъра на функцията, използвайки a typedef. Листинг 1 показва заглавен файл на C ++, simpson.hкойто определя както typedefпараметъра за функцията, така и интерфейса за програмиране за C ++ функция с име integrate. Тялото на функцията за integrateсе съдържа във файл с изходен код на C ++ с име simpson.cpp(не е показано) и осигурява изпълнението на правилото на Simpson.

Листинг 1. Заглавен файл на C ++ за правилото на 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 

Обаждането integrateе лесно в C ++. Като прост пример, да предположим, че сте искали да използвате Правилото на Симпсън, за да апроксимирате интеграла на синусоидната функция от 0до π ( PI), като използвате 30подинтервали. (Всеки, който е завършил Изчисление I, трябва да може да изчисли отговора точно без помощта на калкулатор, което прави това добър тест за integrateфункцията.) Ако приемем, че сте включили правилните заглавни файлове като и "simpson.h", бихте могли да за извикване на функция, integrateкакто е показано в листинг 2.

Листинг 2. Извикване на C ++ за интегриране на функцията

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

Това е всичко. В C ++ вие предавате функцията синус толкова лесно, колкото и другите три параметъра.

Друг пример

Вместо Правилото на Симпсън, бих могъл също толкова лесно да използвам метода на бисекцията ( известен още като алгоритъм на бисекция) за решаване на уравнение на формата f (x) = 0 . Всъщност изходният код на тази статия включва прости реализации както на правилото на Симпсън, така и на метода на бисекцията.

Изтеглете Изтеглете примерите на изходния код на Java за тази статия. Създадено от Джон И. Мур за JavaWorld

Java без ламбда изрази

Сега нека разгледаме как Правилото на Симпсън може да бъде посочено в Java. Независимо дали използваме или не ламбда изрази, ние използваме интерфейса Java, показан в Листинг 3 вместо C ++, за typedefда зададем подписа на параметъра на функцията.

Листинг 3. Java интерфейс за параметъра на функцията

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

За да приложим Правилото на Симпсън в Java, ние създаваме клас с име, Simpsonкойто съдържа метод integrate, с четири параметъра, подобни на това, което направихме в C ++. Както при много самостоятелни математически методи (вижте например java.lang.Math), ще направим integrateстатичен метод. Методът integrateе посочен, както следва:

Листинг 4. Java подпис за метод се интегрира в клас 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

Друга свързана функция в Java 8 е нещо, наречено препратка към метод , което ни позволява да се позоваваме на съществуващ метод по име. Референциите на методите могат да се използват вместо ламбда изрази, стига да отговарят на изискванията на функционалния интерфейс. Както е описано в ресурсите, има няколко различни вида препратки към методи, всеки с малко по-различен синтаксис. За статичните методи синтаксисът е Classname::methodName. Следователно, използвайки препратка към метод, можем да извикаме integrateметода в Java толкова просто, колкото бихме могли в C ++. Сравнете обаждането Java 8, показано в Листинг 10 по-долу, с оригиналното повикване C ++, показано в Листинг 2 по-горе.

Листинг 10. Използване на справка за метод за извикване на Simpson.integrate

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