Apakah ada manfaat kinerja untuk menggunakan sintaks referensi metode alih-alih sintaks lambda di Java 8?

56

Apakah referensi metode melewatkan overhead bungkus lambda? Mungkinkah mereka di masa depan?

Menurut Tutorial Java tentang Referensi Metode :

Terkadang ... ekspresi lambda tidak melakukan apa-apa selain memanggil metode yang ada. Dalam kasus tersebut, seringkali lebih jelas untuk merujuk pada metode yang ada dengan nama. Referensi metode memungkinkan Anda untuk melakukan ini; mereka adalah ekspresi lambda yang ringkas dan mudah dibaca untuk metode yang sudah memiliki nama.

Saya lebih suka sintaks lambda daripada sintaks referensi metode karena beberapa alasan:

Lambdas lebih jelas

Terlepas dari klaim Oracle, saya menemukan sintaks lambda lebih mudah dibaca daripada referensi metode objek secara langsung karena sintaks referensi metode bersifat ambigu:

Bar::foo

Apakah Anda memanggil metode satu argumen statis pada kelas x dan meneruskannya x?

x -> Bar.foo(x)

Atau apakah Anda memanggil metode instance argumen nol pada x?

x -> x.foo()

Sintaks referensi metode bisa bertahan untuk salah satu. Itu menyembunyikan apa yang sebenarnya dilakukan kode Anda.

Lambdas lebih aman

Jika Anda merujuk Bar :: foo sebagai metode kelas dan Bar kemudian menambahkan metode instance dengan nama yang sama (atau sebaliknya), kode Anda tidak akan lagi dikompilasi.

Anda dapat menggunakan lambdas secara konsisten

Anda dapat membungkus fungsi apa pun dalam lambda - sehingga Anda dapat menggunakan sintaksis yang sama secara konsisten di mana-mana. Sintaks referensi metode tidak akan bekerja pada metode yang mengambil atau mengembalikan array primitif, melempar pengecualian yang diperiksa, atau menggunakan nama metode yang sama digunakan sebagai instance dan metode statis (karena sintaks referensi metode tidak jelas tentang metode yang akan dipanggil) . Mereka tidak berfungsi ketika Anda memiliki metode yang berlebihan dengan jumlah argumen yang sama, tetapi Anda tidak harus tetap melakukannya (lihat item Josh Bloch 41) sehingga kami tidak dapat menentangnya terhadap referensi metode.

Kesimpulan

Jika tidak ada penalti kinerja untuk melakukannya, saya tergoda untuk mematikan peringatan di IDE saya dan menggunakan sintaks lambda secara konsisten tanpa menaburkan referensi metode sesekali ke dalam kode saya.

PS

Baik di sini maupun di sana, tetapi dalam mimpiku, referensi metode objek terlihat lebih seperti ini dan menerapkan invoke-dynamic terhadap metode langsung pada objek tanpa pembungkus lambda:

_.foo()
GlenPeterson
sumber
2
Ini tidak menjawab pertanyaan Anda, tetapi ada alasan lain untuk menggunakan penutupan. Menurut pendapat saya, banyak dari mereka jauh melebihi biaya panggilan fungsi tambahan kecil.
9
Saya pikir Anda khawatir tentang kinerja sebelum waktunya. Menurut pendapat saya kinerja harus menjadi pertimbangan sekunder untuk menggunakan referensi metode; ini semua tentang mengekspresikan niat Anda. Jika Anda perlu melewati fungsi di suatu tempat, mengapa tidak lewat saja alih-alih fungsi lain yang mendelegasikannya? Sepertinya aneh bagi saya; seperti Anda tidak cukup membeli bahwa fungsi adalah hal-hal kelas satu, mereka membutuhkan pendamping anonim untuk memindahkannya. Tetapi untuk menjawab pertanyaan Anda secara langsung, saya tidak akan terkejut jika referensi metode dapat diimplementasikan sebagai panggilan statis.
Doval
7
.map(_.acceptValue()): Jika saya tidak salah, ini sangat mirip dengan sintaks Scala. Mungkin Anda hanya mencoba menggunakan bahasa yang salah.
Giorgio
2
"Sintaks referensi metode tidak akan bekerja pada metode yang mengembalikan kekosongan" - Bagaimana bisa begitu? Aku melewati System.out::printlnuntuk forEach()sepanjang waktu ...?
Lukas Eder
3
"Sintaks referensi metode tidak akan bekerja pada metode yang mengambil atau mengembalikan array primitif, membuang pengecualian yang diperiksa ...." Itu tidak benar. Anda dapat menggunakan referensi metode serta ekspresi lambda untuk kasus-kasus seperti itu, selama antarmuka fungsional yang bersangkutan mengizinkannya.
Stuart Marks

Jawaban:

12

Dalam banyak skenario, saya pikir lambda dan metode-referensi adalah sama. Tetapi lambda akan membungkus target doa dengan tipe antarmuka yang menyatakan.

Sebagai contoh

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

Anda akan melihat konsol menampilkan stacktrace.

Di lambda(), metode pemanggilan target()adalah lambda$lambda$0(InvokeTest.java:20), yang memiliki info saluran yang dapat dilacak. Jelas, itu adalah lambda yang Anda tulis, kompiler menghasilkan metode anonim untuk Anda. Dan kemudian, penelepon dari metode lambda adalah sesuatu seperti InvokeTest$$Lambda$2/1617791695.run(Unknown Source), yaitu invokedynamicpanggilan di JVM, itu berarti panggilan tersebut terkait dengan metode yang dihasilkan .

Dalam methodReference(), pemanggilan metode target()secara langsung InvokeTest$$Lambda$1/758529971.run(Unknown Source), itu berarti panggilan tersebut secara langsung terkait dengan InvokeTest::targetmetode tersebut .

Kesimpulan

Di atas semua itu, bandingkan dengan metode-referensi, menggunakan ekspresi lambda hanya akan menyebabkan satu lagi pemanggilan metode ke metode penghasil dari lambda.

JasonMing
sumber
1
Jawaban di bawah tentang metafactory sedikit lebih baik. Saya menambahkan beberapa komentar berguna yang relevan dengannya (yaitu bagaimana implementasi seperti Oracle / OpenJDK menggunakan ASM untuk menghasilkan objek kelas wrapper yang diperlukan untuk membuat instance lambda / method handle.
Ajax
29

Ini semua tentang metafactory

Pertama, sebagian besar metode referensi tidak perlu diinginkan oleh metafactory lambda, mereka hanya digunakan sebagai metode referensi. Di bawah bagian "Lambda body sugaring" artikel Terjemahan Lambda Expressions ("TLE"):

Semua hal dianggap sama, metode pribadi lebih disukai daripada nonprivat, metode statis lebih disukai daripada metode contoh, yang terbaik adalah jika badan lambda tertarik ke dalam kelas paling dalam di mana ekspresi lambda muncul, tanda tangan harus cocok dengan tanda tangan tubuh lambda, tambahan argumen harus diawali di bagian depan daftar argumen untuk nilai yang ditangkap, dan tidak akan menghapus referensi metode sama sekali. Namun, ada beberapa kasus pengecualian di mana kita mungkin harus menyimpang dari strategi dasar ini.

Ini lebih lanjut disorot lebih lanjut di TLE "The Lambda Metafactory":

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

The implargumen mengidentifikasi metode lambda, baik tubuh lambda desugared atau metode yang disebutkan dalam referensi metode.

Referensi Integer::summetode statis ( ) atau tidak terikat ( Integer::intValue) adalah yang 'paling sederhana' atau yang paling 'nyaman', dalam arti bahwa mereka dapat ditangani secara optimal oleh varian metafactory 'jalur cepat' tanpa keinginan . Keunggulan ini sangat membantu ditunjukkan dalam "varian Metafactory" TLE:

Dengan menghilangkan argumen di mana mereka tidak diperlukan, file kelas menjadi lebih kecil. Dan opsi jalur cepat menurunkan bilah untuk VM untuk menginstruksikan operasi konversi lambda, memungkinkannya untuk diperlakukan sebagai operasi "tinju" dan memfasilitasi optimasi unbox.

Tentu saja, referensi metode penangkapan-contoh ( obj::myMethod) perlu menyediakan turunan terikat sebagai argumen pada pegangan metode untuk pemanggilan, yang dapat berarti kebutuhan untuk menggunakan metode 'jembatan'.

Kesimpulan

Saya tidak begitu yakin apa yang dimaksud dengan 'pembungkus' lambda, tetapi meskipun hasil akhir dari menggunakan lambda atau referensi metode yang ditentukan pengguna adalah sama, cara yang dicapai tampaknya sangat berbeda, dan dapat berbeda di masa depan jika itu tidak terjadi sekarang. Oleh karena itu, saya kira itu lebih mungkin daripada tidak bahwa referensi metode dapat ditangani dengan cara yang lebih optimal oleh metafactory.

hjk
sumber
1
Ini adalah jawaban yang paling relevan, karena sebenarnya menyentuh mesin internal yang diperlukan untuk membuat lambda. Sebagai seseorang yang benar-benar men-debug proses pembuatan lambda (untuk mengetahui cara menghasilkan id yang stabil untuk referensi lambda / metode tertentu sehingga mereka dapat dihapus dari peta), saya menemukan cukup mengejutkan betapa banyak pekerjaan yang dilakukan untuk membuat sebuah contoh dari lambda. Oracle / OpenJDK menggunakan ASM untuk secara dinamis menghasilkan kelas yang, jika perlu, menutup variabel apa pun yang direferensikan dalam lambda Anda (atau kualifikasi contoh referensi metode) ...
Ajax
1
... Selain menutup variabel, khususnya lambda juga membuat metode statis yang disuntikkan ke kelas mendeklarasikan sehingga lambda yang dibuat memiliki sesuatu untuk dipanggil untuk memetakan ke fungsi yang ingin Anda panggil. Referensi metode dengan kualifikasi instance akan menjadikan instance tersebut sebagai parameter untuk metode ini; Saya belum menguji apakah referensi metode :: ini di kelas privat melewatkan penutupan ini, tetapi saya telah menguji bahwa metode statis, pada kenyataannya, melewatkan metode perantara (dan hanya membuat objek agar sesuai dengan target doa di situs panggilan).
Ajax
1
Agaknya metode pribadi juga tidak perlu pengiriman virtual, sedangkan apa pun yang lebih publik perlu menutup instance (melalui metode statis yang dihasilkan) sehingga dapat menggunakan virtual pada instance aktual untuk memastikannya memperhatikan override. TL; DR: Referensi metode menutup lebih sedikit argumen, sehingga lebih sedikit bocor, dan membutuhkan pembuatan kode yang kurang dinamis.
Ajax
3
Komentar spam terakhir ... Generasi kelas dinamis cukup kelas berat. Masih mungkin lebih baik daripada memuat kelas anonim di classloader (karena bagian terberat hanya dilakukan sekali), tetapi Anda akan benar-benar terkejut betapa banyak pekerjaan yang harus dilakukan untuk membuat instance lambda. Akan menarik untuk melihat tolok ukur nyata lambda terhadap referensi metode versus kelas anonim. Perhatikan bahwa dua lambda atau referensi metode yang mereferensikan hal yang sama, tetapi di tempat yang berbeda membuat kelas yang berbeda pada saat runtime (bahkan dalam metode yang sama, tepat setelah satu sama lain).
Ajax
@Ajax Bagaimana dengan yang ini? blog.soat.fr/2015/12/benchmark-java-lambda-vs-classe-anonyme .
Walfrat
0

Ada satu konsekuensi yang cukup serius ketika menggunakan ekspresi lambda yang dapat mempengaruhi kinerja.

Ketika Anda mendeklarasikan ekspresi lambda, Anda membuat penutupan atas cakupan lokal.

Apa artinya ini dan bagaimana pengaruhnya terhadap kinerja?

Yah saya senang Anda bertanya. Ini berarti bahwa masing-masing ekspresi lambda kecil adalah kelas batin anonim kecil dan itu berarti disertai dengan referensi ke semua variabel yang berada dalam cakupan yang sama dari ekspresi lambda.

Ini berarti juga thisreferensi dari instance objek dan semua bidangnya. Bergantung pada waktu pemanggilan lambda yang sebenarnya, ini dapat menyebabkan kebocoran sumber daya yang cukup signifikan karena pengumpul sampah tidak dapat melepaskan referensi ini selama objek yang memegang lambda masih hidup ...

Roland Tepp
sumber
Hal yang sama berlaku untuk referensi metode non-statis.
Ajax
12
Lambda Jawa tidak sepenuhnya tertutup. Mereka juga tidak memiliki kelas batin anonim. Mereka TIDAK membawa referensi ke semua variabel dalam lingkup di mana mereka dideklarasikan. Mereka HANYA membawa referensi ke variabel yang sebenarnya mereka referensi. Ini juga berlaku untuk thisobjek yang bisa dirujuk. Oleh karena itu, lambda tidak membocorkan sumber daya hanya dengan memiliki ruang bagi mereka; hanya berpegang pada objek yang dibutuhkan. Di sisi lain, kelas dalam anonim dapat membocorkan sumber daya, tetapi tidak selalu melakukannya. Lihat kode ini sebagai contoh: a.blmq.us/2mmrL6v
squid314
Jawaban itu salah
Stefan Reich
Terima kasih @StefanReich, Itu sangat membantu!
Roland Tepp