Java programiranje s lambda izrazima

U glavnoj tehničkoj adresi za JavaOne 2013, Mark Reinhold, glavni arhitekt Java Platform Group u Oracleu, opisao je lambda izraze kao najveću pojedinačnu nadogradnju Java programskog modela ikad . Iako postoji mnogo aplikacija za lambda izraze, ovaj se članak fokusira na određeni primjer koji se često javlja u matematičkim aplikacijama; naime potreba za prosljeđivanjem funkcije algoritmu.

Kao sijedi geek, programirao sam na brojnim jezicima tijekom godina, a intenzivno sam programirao na Javi od verzije 1.1. Kad sam počeo raditi s računalima, gotovo nitko nije diplomirao informatiku. Računalni profesionalci uglavnom su dolazili iz drugih disciplina poput elektrotehnike, fizike, poslovanja i matematike. U svom bivšem životu bio sam matematičar, pa ne bi trebalo čuditi da je moje početno gledište računala bilo stajalište divovskog programabilnog kalkulatora. Tijekom godina znatno sam proširio svoj pogled na računala, ali svejedno pozdravljam priliku da radim na aplikacijama koje uključuju neki aspekt matematike.

Mnoge matematičke primjene zahtijevaju da se funkcija prenese kao parametar algoritmu. Primjeri iz fakultetske algebre i osnovnog računa uključuju rješavanje jednadžbe ili izračunavanje integrala funkcije. Više od 15 godina Java je moj odabrani programski jezik za većinu aplikacija, ali to je bio prvi jezik koji sam često koristio i koji mi nije dopuštao da funkciju (tehnički pokazivač ili referencu na funkciju) prenesem kao parametar na jednostavan, neposredan način. Taj će se nedostatak uskoro promijeniti s nadolazećim izdanjem Jave 8.

Moć lambda izraza širi se daleko više od pojedinačnog slučaja upotrebe, ali proučavanje različitih implementacija istog primjera trebalo bi vam ostaviti čvrst osjećaj kako će lambda biti od koristi vašim Java programima. U ovom članku poslužit ću se uobičajenim primjerom koji će pomoći u opisivanju problema, a zatim pružiti rješenja napisana na C ++, Java prije lambda izraza i Java s lambda izrazima. Imajte na umu da jaka matematika nije potrebna da bi se razumjele i uvažile glavne točke ovog članka.

Učenje o lambdama

Lambda izrazi, poznati i kao zatvaranja, funkcijski literali ili jednostavno lambda, opisuju skup značajki definiranih u Zahtjevu za specifikaciju Java (JSR) 335. Manje formalni / čitljiviji uvodi u lambda izraze nalaze se u odjeljku najnovije verzije Java Tutorial i u nekoliko članaka Briana Goetza, "Stanje lambde" i "Stanje lambde: izdanje knjižnica". Ovi izvori opisuju sintaksu lambda izraza i pružaju primjere slučajeva upotrebe u kojima su lambda izrazi primjenjivi. Za više informacija o lambda izrazima u Javi 8, pogledajte tehničku uvodnu adresu Marka Reinholda za JavaOne 2013.

Lambda izrazi u matematičkom primjeru

Primjer korišten u ovom članku je Simpsonovo pravilo iz osnovnog računa. Simpsonovo pravilo, ili točnije Složeno Simpsonovo pravilo, numerička je tehnika integracije za približavanje određenog integrala. Ne brinite ako vam nije poznat pojam određenog integrala ; ono što stvarno trebate razumjeti je da je Simpsonovo pravilo algoritam koji izračunava stvarni broj na temelju četiri parametra:

  • Funkcija koju želimo integrirati.
  • Dva stvarna broja ai bkoja predstavljaju krajnje točke intervala [a,b]na liniji stvarnog broja. (Imajte na umu da bi gore navedena funkcija trebala biti kontinuirana u ovom intervalu.)
  • Paran cijeli broj nkoji navodi broj podintervala. U provedbi Simpsonovog pravila interval dijelimo [a,b]na npodintervale.

Da bismo pojednostavili prezentaciju, usredotočimo se na programsko sučelje, a ne na detalje implementacije. (Iskreno, nadam se da će nam ovaj pristup neka premosnica argumenata o najboljem ili najučinkovitiji način provedbe simpsonova, što nije u fokusu ovog članka.) Mi ćemo koristiti tip doubleza parametre ai b, a mi ćemo koristiti tip intza parametar n. Funkcija koja će se integrirati uzet će jedan parametar tipa doublei vratiti vrijednost tipa double.

Preuzimanje Preuzmite primjer izvornog koda C ++ za ovaj članak. Stvorio John I. Moore za JavaWorld

Parametri funkcije u C ++

Da bismo pružili osnovu za usporedbu, krenimo sa C ++ specifikacijom. Kada prosljeđujem funkciju kao parametar u C ++-u, obično preferiram navođenje potpisa parametra funkcije pomoću a typedef. Popis 1 prikazuje datoteku zaglavlja C ++ imenovanu simpson.hkoja specificira i typedefparametar funkcije i programsko sučelje za imenovanu funkciju C ++ integrate. Tijelo funkcije za integratesadrži datoteku izvornog koda C ++ pod nazivom simpson.cpp(nije prikazano) i pruža implementaciju Simpsonovih pravila.

Popis 1. Datoteka zaglavlja C ++ za Simpsonovo pravilo

 #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 

integrateU C ++-u je pozivanje jednostavno. Kao jednostavan primjer, pretpostavimo da ste željeli upotrijebiti Simpsonovo pravilo za aproksimaciju integrala sinusne funkcije od 0do π ( PI) pomoću 30subintervala. (Svatko tko je dovršio Calculus I trebao bi moći izračunati odgovor točno bez pomoći kalkulatora, što je ovo dobar test za integratefunkciju.) Pod pretpostavkom da ste uključili odgovarajuće datoteke zaglavlja kao što su i "simpson.h", moći ćete za pozivanje funkcije integratekao što je prikazano na popisu 2.

Popis 2. C ++ poziv za integriranje funkcije

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

To je sve. U C ++-u sinusnu funkciju prenosite jednako lako kao i ostala tri parametra.

Još jedan primjer

Umjesto simpsonova sam mogao imati jednako lako koristiti bisekcija Metoda ( aka na bisekcija algoritam) za rješavanjem jednadžbe oblika f (x) = 0 . Zapravo, izvorni kôd ovog članka uključuje jednostavne implementacije i Simpsonovog pravila i metode bisekcije.

Preuzimanje Preuzmite Primjere izvornog koda Java za ovaj članak. Stvorio John I. Moore za JavaWorld

Java bez lambda izraza

Sada pogledajmo kako se Simpsonovo pravilo može navesti u Javi. Bez obzira koristimo li lambda izraze ili ne, koristimo Java sučelje prikazano u Popisu 3 umjesto C ++ typedefda odredimo potpis parametra funkcije.

Popis 3. Java sučelje za parametar funkcije

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

Da bismo implementirali Simpsonovo pravilo u Javi, kreirali smo klasu Simpsonkoja sadrži metodu integrate, s četiri parametra slična onima koje smo radili u C ++-u. Kao i kod mnogih samostalnih matematičkih metoda (vidi, na primjer, java.lang.Math), izradit ćemo integratestatičku metodu. Metoda integrateje određena kako slijedi:

Popis 4. Java potpis za metodu integrira u klasi Simpson

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

Sve što smo do sada radili na Javi neovisno je o tome hoćemo li koristiti lambda izraze ili ne. Primarna razlika s lambda izrazima je u tome kako prosljeđujemo parametre (točnije, kako prenosimo parametar funkcije) u pozivu na metodu integrate. Prvo ću ilustrirati kako bi se to radilo u verzijama Jave prije verzije 8; tj. bez lambda izraza. Kao i na primjeru C ++, pretpostavimo da želimo aproksimirati integral sinusne funkcije od 0do π ( PI) pomoću 30subintervala.

Korištenje uzorka adaptera za sinusnu funkciju

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

Još jedna srodna značajka u Javi 8 nešto je što se naziva referenca na metodu , što nam omogućuje da se postojećom metodom uputimo imenom. Upute na metode mogu se koristiti umjesto lambda izraza sve dok zadovoljavaju zahtjeve funkcionalnog sučelja. Kao što je opisano u resursima, postoji nekoliko različitih vrsta referenci metoda, svaka s malo drugačijom sintaksom. Za statičke metode sintaksa je Classname::methodName. Stoga, koristeći referencu na metodu, metodu možemo nazvati integrateu Javi najjednostavnije kao što bismo mogli u C ++. Usporedite poziv Java 8 prikazan u donjem popisu 10 s originalnim pozivom C ++ prikazan u gornjem popisu 2.

Popis 10. Korištenje reference metode za pozivanje Simpson.integrate

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