Pengaturcaraan Java dengan ungkapan lambda

Dalam ucaptama teknikal untuk JavaOne 2013, Mark Reinhold, ketua arkitek untuk Java Platform Group di Oracle, menggambarkan ekspresi lambda sebagai satu-satunya peningkatan terbesar untuk model pengaturcaraan Java yang pernah ada . Walaupun terdapat banyak aplikasi untuk ungkapan lambda, artikel ini memfokuskan pada contoh khusus yang sering berlaku dalam aplikasi matematik; iaitu, keperluan menyampaikan fungsi ke algoritma.

Sebagai geek berambut kelabu, saya telah memprogram dalam banyak bahasa selama bertahun-tahun, dan saya telah banyak memprogram di Java sejak versi 1.1. Semasa saya mula bekerja dengan komputer, hampir tidak ada yang mempunyai ijazah dalam sains komputer. Profesional komputer kebanyakannya berasal dari disiplin lain seperti kejuruteraan elektrik, fizik, perniagaan, dan matematik. Dalam kehidupan saya dahulu, saya adalah seorang ahli matematik, dan jadi tidak menghairankan bahawa pandangan awal saya mengenai komputer adalah seperti kalkulator yang boleh diprogramkan raksasa. Saya telah memperluas pandangan saya mengenai komputer selama bertahun-tahun, tetapi saya masih mengalu-alukan peluang untuk mengerjakan aplikasi yang melibatkan beberapa aspek matematik.

Banyak aplikasi dalam matematik memerlukan fungsi diteruskan sebagai parameter kepada algoritma. Contoh dari aljabar kolej dan kalkulus asas termasuk menyelesaikan persamaan atau mengira integral fungsi. Selama lebih dari 15 tahun Java menjadi bahasa pengaturcaraan pilihan saya untuk kebanyakan aplikasi, tetapi itu adalah bahasa pertama yang sering saya gunakan yang tidak membenarkan saya meneruskan fungsi (secara teknikal penunjuk atau merujuk pada fungsi) sebagai parameter dengan cara yang mudah dan mudah. Kekurangan itu akan berubah dengan pelepasan Java 8 yang akan datang.

Kekuatan ungkapan lambda jauh melampaui satu kes penggunaan, tetapi mempelajari pelbagai pelaksanaan contoh yang sama akan membuat anda memahami bagaimana lambda akan memberi manfaat kepada program Java anda. Dalam artikel ini saya akan menggunakan contoh umum untuk membantu menjelaskan masalahnya, kemudian memberikan penyelesaian yang ditulis dalam C ++, Java sebelum ungkapan lambda, dan Java dengan ungkapan lambda. Perhatikan bahawa latar belakang matematik yang kuat tidak diperlukan untuk memahami dan menghayati perkara utama artikel ini.

Belajar mengenai lambdas

Ungkapan lambda, juga dikenal sebagai penutupan, literal fungsi, atau sekadar lambdas, menggambarkan sekumpulan ciri yang ditentukan dalam Permintaan Spesifikasi Java (JSR) 335. Perkenalan yang kurang formal / lebih mudah dibaca untuk ungkapan lambda disediakan di bahagian versi terkini Tutorial Java dan dalam beberapa artikel oleh Brian Goetz, "State of the lambda" dan "State of the lambda: Libraries edisi." Sumber-sumber ini menerangkan sintaks ungkapan lambda dan memberikan contoh kes penggunaan di mana ungkapan lambda berlaku. Untuk lebih lanjut mengenai ungkapan lambda di Java 8, tonton alamat utama teknikal Mark Reinhold untuk JavaOne 2013.

Ungkapan lambda dalam contoh matematik

Contoh yang digunakan dalam artikel ini adalah Peraturan Simpson dari kalkulus asas. Peraturan Simpson, atau lebih khusus Peraturan Simpson Komposit, adalah teknik penyatuan berangka untuk menghitung suatu integral yang pasti. Jangan risau jika anda tidak terbiasa dengan konsep integral yang pasti ; apa yang anda benar-benar perlu fahami adalah bahawa Simpson's Rule adalah algoritma yang mengira nombor nyata berdasarkan empat parameter:

  • Fungsi yang ingin kita satukan.
  • Dua nombor nyata adan byang mewakili titik akhir selang [a,b]pada garis nombor nyata. (Perhatikan bahawa fungsi yang disebutkan di atas harus berterusan pada selang waktu ini.)
  • Bilangan bulat genap nyang menentukan sebilangan subinterval. Dalam melaksanakan Peraturan Simpson kita membahagikan selang [a,b]menjadi nsubinterval.

Untuk mempermudah persembahan, mari fokus pada antara muka pengaturcaraan dan bukan pada perincian pelaksanaan. (Sejujurnya, saya berharap bahawa pendekatan ini akan memungkinkan kita memintas argumen mengenai cara terbaik atau paling berkesan untuk melaksanakan Peraturan Simpson, yang bukan merupakan fokus artikel ini.) Kami akan menggunakan jenis doubleuntuk parameter adan b, dan kami akan menggunakan jenis intuntuk parameter n. Fungsi yang akan disatukan akan mengambil satu parameter jenis doubledan mengembalikan nilai jenis double.

Muat turun Muat turun contoh kod sumber C ++ untuk artikel ini. Dicipta oleh John I. Moore untuk JavaWorld

Parameter fungsi dalam C ++

Untuk memberikan asas perbandingan, mari kita mulakan dengan spesifikasi C ++. Ketika melewati fungsi sebagai parameter di C ++, saya biasanya lebih suka menentukan tanda tangan parameter fungsi menggunakan a typedef. Penyenaraian 1 menunjukkan fail tajuk C ++ bernama simpson.hyang menentukan kedua typedefparameter fungsi dan antara muka pengaturcaraan untuk fungsi C ++ bernama integrate. Fungsi fungsi untuk integrateterkandung dalam file kod sumber C ++ bernama simpson.cpp(tidak ditunjukkan) dan menyediakan pelaksanaan untuk Simpson's Rule.

Penyenaraian 1. Fail header C ++ untuk Simpson's Rule

 #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 

Panggilan integratemudah di C ++. Sebagai contoh mudah, anggap anda mahu menggunakan Peraturan Simpson untuk menghampiri integral fungsi sinus dari 0π ( PI) menggunakan 30subintervals. (Sesiapa yang telah melengkapkan Kalkulus saya seharusnya dapat mengira jawapannya tepat tanpa bantuan kalkulator, menjadikan ini sebagai contoh ujian yang baik untuk integratefungsi tersebut.) Dengan andaian bahawa anda telah memasukkan fail tajuk yang sesuai seperti dan "simpson.h", anda akan dapat untuk memanggil fungsi integrateseperti yang ditunjukkan dalam Penyenaraian 2.

Penyenaraian 2. Panggilan C ++ untuk fungsi mengintegrasikan

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

Itu sahaja yang ada. Dalam C ++ anda melewati fungsi sinus dengan mudah semasa anda melewati tiga parameter lain.

Contoh yang lain

Daripada Peraturan Simpson saya boleh hanya sebagai mudah digunakan Kaedah dua sama ( aka yang dua sama Algoritma) untuk menyelesaikan persamaan dalam bentuk yang f (x) = 0 . Sebenarnya, kod sumber untuk artikel ini merangkumi pelaksanaan sederhana dari Peraturan Simpson dan Kaedah Pembahagian.

Muat turun Muat turun contoh kod sumber Java untuk artikel ini. Dicipta oleh John I. Moore untuk JavaWorld

Java tanpa ungkapan lambda

Sekarang mari kita lihat bagaimana Peraturan Simpson dapat ditentukan di Java. Terlepas dari apakah kami menggunakan ekspresi lambda atau tidak, kami menggunakan antara muka Java yang ditunjukkan dalam Penyenaraian 3 sebagai pengganti C ++ typedefuntuk menentukan tanda tangan parameter fungsi.

Penyenaraian 3. Antara muka Java untuk parameter fungsi

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

Untuk menerapkan Peraturan Simpson di Java, kami membuat kelas bernama Simpsonyang berisi metode integrate, dengan empat parameter yang serupa dengan yang kami lakukan di C ++. Seperti banyak kaedah matematik mandiri (lihat, misalnya, java.lang.Math), kita akan membuat integratekaedah statik. Kaedah integratedinyatakan seperti berikut:

Penyenaraian 4. Tandatangan Java untuk kaedah mengintegrasikan di kelas 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

Ciri lain yang berkaitan di Java 8 adalah sesuatu yang disebut rujukan kaedah , yang memungkinkan kita merujuk kepada kaedah yang ada dengan nama. Rujukan kaedah boleh digunakan sebagai pengganti ungkapan lambda selagi memenuhi kehendak antara muka fungsional. Seperti yang dijelaskan dalam sumber, terdapat beberapa jenis rujukan kaedah, masing-masing dengan sintaks yang sedikit berbeza. Untuk kaedah statik sintaksnya adalah Classname::methodName. Oleh itu, dengan menggunakan metode referensi, kita dapat memanggil integratemetode di Java sesederhana yang kita bisa di C ++. Bandingkan panggilan Java 8 yang ditunjukkan dalam Penyenaraian 10 di bawah dengan panggilan C ++ asal yang ditunjukkan dalam Penyenaraian 2 di atas.

Penyenaraian 10. Menggunakan rujukan kaedah untuk memanggil Simpson.integrate

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