Pemrograman Java dengan ekspresi lambda

Dalam alamat keynote teknis untuk JavaOne 2013, Mark Reinhold, kepala arsitek untuk Java Platform Group di Oracle, menggambarkan ekspresi lambda sebagai peningkatan terbesar untuk model pemrograman Java yang pernah ada . Meskipun ada banyak aplikasi untuk ekspresi lambda, artikel ini berfokus pada contoh spesifik yang sering muncul dalam aplikasi matematika; yaitu, kebutuhan untuk meneruskan fungsi ke algoritme.

Sebagai geek berambut abu-abu, saya telah memprogram dalam banyak bahasa selama bertahun-tahun, dan saya telah memprogram secara ekstensif di Java sejak versi 1.1. Ketika saya mulai bekerja dengan komputer, hampir tidak ada yang memiliki gelar di bidang ilmu komputer. Profesional komputer kebanyakan berasal dari disiplin ilmu lain seperti teknik elektro, fisika, bisnis, dan matematika. Dalam kehidupan saya sebelumnya, saya adalah seorang matematikawan, dan karenanya tidak mengherankan bahwa pandangan awal saya tentang komputer adalah kalkulator raksasa yang dapat diprogram. Saya telah memperluas pandangan saya tentang komputer selama bertahun-tahun, tetapi saya masih menyambut baik kesempatan untuk mengerjakan aplikasi yang melibatkan beberapa aspek matematika.

Banyak aplikasi dalam matematika mensyaratkan suatu fungsi dilewatkan sebagai parameter ke suatu algoritma. Contoh dari aljabar perguruan tinggi dan kalkulus dasar termasuk menyelesaikan persamaan atau menghitung integral suatu fungsi. Selama lebih dari 15 tahun Java telah menjadi bahasa pemrograman pilihan saya untuk sebagian besar aplikasi, tetapi itu adalah bahasa pertama yang sering saya gunakan yang tidak memungkinkan saya untuk meneruskan fungsi (secara teknis penunjuk atau referensi ke suatu fungsi) sebagai parameter dengan cara yang sederhana dan lugas. Kekurangan itu akan segera berubah dengan rilis Java 8 yang akan datang.

Kekuatan ekspresi lambda melampaui kasus penggunaan tunggal, tetapi mempelajari berbagai implementasi dari contoh yang sama akan memberi Anda pemahaman yang kuat tentang bagaimana lambda akan bermanfaat bagi program Java Anda. Pada artikel ini saya akan menggunakan contoh umum untuk membantu menjelaskan masalah, kemudian memberikan solusi yang ditulis dalam C ++, Java sebelum ekspresi lambda, dan Java dengan ekspresi lambda. Perhatikan bahwa latar belakang yang kuat dalam matematika tidak diperlukan untuk memahami dan menghargai poin-poin utama artikel ini.

Belajar tentang lambda

Ekspresi lambda, juga dikenal sebagai closure, function literals, atau hanya lambda, mendeskripsikan sekumpulan fitur yang didefinisikan dalam Java Specification Request (JSR) 335. Pengenalan yang kurang formal / lebih mudah dibaca untuk ekspresi lambda disediakan di bagian versi terbaru dari Tutorial Java dan dalam beberapa artikel oleh Brian Goetz, "State of the lambda" dan "State of the lambda: Libraries edition." Sumber daya ini menjelaskan sintaks ekspresi lambda dan memberikan contoh kasus penggunaan di mana ekspresi lambda dapat diterapkan. Untuk informasi selengkapnya tentang ekspresi lambda di Java 8, lihat alamat utama teknis Mark Reinhold untuk JavaOne 2013.

Ekspresi lambda dalam contoh matematika

Contoh yang digunakan di seluruh artikel ini adalah Aturan Simpson dari kalkulus dasar. Aturan Simpson, atau lebih khusus lagi Aturan Simpson Komposit, adalah teknik integrasi numerik untuk mendekati integral tertentu. Jangan khawatir jika Anda tidak terbiasa dengan konsep integral tertentu ; Yang perlu Anda pahami adalah Simpson's Rule adalah algoritme yang menghitung bilangan real berdasarkan empat parameter:

  • Fungsi yang ingin kami integrasikan.
  • Dua bilangan real adan byang mewakili titik akhir interval [a,b]pada garis bilangan real. (Perhatikan bahwa fungsi yang dirujuk di atas harus berlanjut pada interval ini.)
  • Bilangan bulat genap nyang menentukan jumlah subinterval. Dalam menerapkan Aturan Simpson, kami membagi interval [a,b]menjadi nsub -interval .

Untuk menyederhanakan presentasi, mari fokus pada antarmuka pemrograman dan bukan pada detail implementasi. (Sejujurnya, saya berharap pendekatan ini akan membiarkan kita melewati argumen tentang cara terbaik atau paling efisien untuk menerapkan Aturan Simpson, yang bukan fokus artikel ini.) Kita akan menggunakan tipe doubleuntuk parameter adan b, dan kita akan menggunakan tipe intuntuk parameter n. Fungsi yang akan diintegrasikan akan mengambil satu parameter tipe doubledan mengembalikan nilai tipe double.

Unduh Unduh contoh kode sumber C ++ untuk artikel ini. Dibuat oleh John I. Moore untuk JavaWorld

Parameter fungsi di C ++

Untuk memberikan dasar perbandingan, mari kita mulai dengan spesifikasi C ++. Saat meneruskan fungsi sebagai parameter di C ++, saya biasanya lebih suka menentukan tanda tangan parameter fungsi menggunakan a typedef. Kode 1 menunjukkan file header C ++ bernama simpson.hyang menentukan typedefparameter fungsi dan antarmuka pemrograman untuk fungsi C ++ bernama integrate. Badan fungsi untuk integratedimuat dalam file kode sumber C ++ bernama simpson.cpp(tidak ditampilkan) dan menyediakan implementasi untuk Simpson's Rule.

Kode 1. File 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 

Menelepon integratesangatlah mudah di C ++. Sebagai contoh sederhana, misalkan Anda ingin menggunakan Aturan Simpson untuk memperkirakan integral fungsi sinus dari 0ke π ( PI) menggunakan 30subinterval. (Siapa pun yang telah menyelesaikan Kalkulus I harus dapat menghitung jawabannya dengan tepat tanpa bantuan kalkulator, menjadikannya kasus uji yang baik untuk integratefungsi tersebut.) Dengan asumsi bahwa Anda telah menyertakan file header yang tepat seperti dan "simpson.h", Anda akan dapat untuk memanggil fungsi integrateseperti yang ditunjukkan pada Kode 2.

Kode 2. C ++ panggilan untuk mengintegrasikan fungsi

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

Hanya itu yang ada untuk itu. Di C ++ Anda meneruskan fungsi sinus semudah Anda meneruskan tiga parameter lainnya.

Contoh lain

Alih-alih Aturan Simpson, saya dapat dengan mudah menggunakan Metode Biseksi ( alias Algoritma Biseksi) untuk menyelesaikan persamaan bentuk f (x) = 0 . Faktanya, kode sumber untuk artikel ini mencakup implementasi sederhana dari Aturan Simpson dan Metode Biseksi.

Unduh Unduh contoh kode sumber Java untuk artikel ini. Dibuat oleh John I. Moore untuk JavaWorld

Java tanpa ekspresi lambda

Sekarang mari kita lihat bagaimana Aturan Simpson dapat ditentukan di Java. Terlepas dari apakah kita menggunakan ekspresi lambda atau tidak, kita menggunakan antarmuka Java yang ditunjukkan pada Listing 3 sebagai pengganti C ++ typedefuntuk menentukan tanda tangan dari parameter fungsi.

Kode 3. Antarmuka Java untuk parameter fungsi

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

Untuk mengimplementasikan Aturan Simpson di Java, kami membuat kelas bernama Simpsonyang berisi metode integrate, dengan empat parameter serupa dengan yang kami lakukan di C ++. Seperti banyak metode matematika mandiri (lihat, misalnya, java.lang.Math), kami akan membuat integratemetode statis. Metode integrateditentukan sebagai berikut:

Kode 4. Tanda tangan Java untuk metode integrasi 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

Fitur terkait lainnya di Java 8 adalah sesuatu yang disebut referensi metode , yang memungkinkan kita merujuk ke metode yang ada berdasarkan nama. Referensi metode dapat digunakan sebagai pengganti ekspresi lambda selama memenuhi persyaratan antarmuka fungsional. Seperti yang dijelaskan dalam sumber daya, ada beberapa jenis referensi metode, masing-masing dengan sintaks yang sedikit berbeda. Untuk metode statis sintaksnya adalah Classname::methodName. Oleh karena itu, dengan menggunakan referensi metode, kita dapat memanggil integratemetode tersebut di Java sesederhana mungkin di C ++. Bandingkan panggilan Java 8 yang ditunjukkan pada Daftar 10 di bawah dengan panggilan C ++ asli yang ditunjukkan pada Daftar 2 di atas.

Kode 10. Menggunakan referensi metode untuk memanggil Simpson.integrate

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