Di Java 8, apakah lebih baik menggunakan ekspresi metode referensi atau metode mengembalikan implementasi antarmuka fungsional?


11

Java 8 menambahkan konsep antarmuka fungsional , serta berbagai metode baru yang dirancang untuk mengambil antarmuka fungsional. Contoh dari antarmuka ini dapat dibuat secara ringkas menggunakan ekspresi referensi metode (misalnya SomeClass::someMethod) dan ekspresi lambda (misalnya (x, y) -> x + y).

Seorang kolega dan saya memiliki pendapat yang berbeda mengenai kapan sebaiknya menggunakan satu bentuk atau yang lain (di mana "terbaik" dalam hal ini benar-benar bermuara pada "yang paling mudah dibaca" dan "paling sesuai dengan praktik standar", karena pada dasarnya mereka sebaliknya setara). Secara khusus, ini melibatkan kasus di mana yang berikut ini benar:

  • fungsi yang dimaksud tidak digunakan di luar cakupan tunggal
  • memberi contoh nama membantu keterbacaan (sebagai lawan dari misalnya logika cukup sederhana untuk melihat apa yang terjadi sekilas)
  • tidak ada alasan pemrograman lain mengapa satu formulir lebih disukai dari yang lain.

Pendapat saya saat ini tentang masalah ini adalah menambahkan metode pribadi, dan merujuknya dengan referensi metode, adalah pendekatan yang lebih baik. Rasanya seperti ini bagaimana fitur dirancang untuk digunakan, dan tampaknya lebih mudah untuk mengkomunikasikan apa yang terjadi melalui nama metode dan tanda tangan (misalnya "boolean isResultInFuture (hasil hasil)" jelas mengatakan itu mengembalikan boolean). Ini juga membuat metode pribadi lebih dapat digunakan kembali jika peningkatan masa depan ke kelas ingin menggunakan pemeriksaan yang sama, tetapi tidak memerlukan pembungkus antarmuka fungsional.

Preferensi kolega saya adalah memiliki metode yang mengembalikan instance antarmuka (mis. "Predicate resultInFuture ()"). Bagi saya, ini rasanya bukan bagaimana fitur itu dimaksudkan untuk digunakan, terasa agak clunkier, dan sepertinya lebih sulit untuk benar-benar mengomunikasikan maksud melalui penamaan.

Untuk membuat contoh ini konkret, berikut adalah kode yang sama, ditulis dalam gaya yang berbeda:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

Apakah ada dokumentasi atau komentar resmi atau semi-resmi tentang apakah satu pendekatan lebih disukai, lebih sesuai dengan maksud perancang bahasa, atau lebih mudah dibaca? Kecuali sumber resmi, apakah ada alasan yang jelas mengapa seseorang menjadi pendekatan yang lebih baik?


1
Lambdas ditambahkan ke bahasa karena suatu alasan, dan itu tidak membuat Jawa lebih buruk.

@Snowman Tak perlu dikatakan lagi. Karena itu saya menganggap ada beberapa hubungan yang tersirat antara komentar Anda dan pertanyaan saya yang tidak saya mengerti. Apa yang ingin Anda sampaikan?
M. Justin

2
Saya berasumsi bahwa maksudnya adalah bahwa, jika lambdas tidak menjadi peningkatan status quo, mereka tidak akan repot-repot memperkenalkannya.
Robert Harvey

2
@RobertHarvey Tentu. Namun, hingga saat itu, baik lambda dan referensi metode ditambahkan pada saat yang sama, jadi tidak ada status quo sebelumnya. Bahkan tanpa kasus khusus ini, ada kalanya referensi metode masih lebih baik; misalnya, metode publik yang ada (misalnya Object::toString). Jadi pertanyaan saya lebih tentang apakah lebih baik dalam jenis contoh khusus yang saya paparkan di sini, daripada apakah ada contoh di mana satu lebih baik dari yang lain, atau sebaliknya.
M. Justin

Jawaban:


8

Dalam hal pemrograman fungsional, apa yang Anda dan kolega Anda diskusikan adalah gaya bebas poin , lebih khusus pengurangan eta . Pertimbangkan dua tugas berikut:

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

Ini secara operasional setara, dan yang pertama disebut gaya pointful sedangkan yang kedua adalah gaya point-free . Istilah "titik" mengacu pada argumen fungsi bernama (ini berasal dari topologi) yang hilang dalam kasus terakhir.

Lebih umum, untuk semua fungsi F, lambda pembungkus yang mengambil semua argumen yang diberikan dan meneruskannya ke F tidak berubah, kemudian mengembalikan hasil F tidak berubah identik dengan F itu sendiri. Menghapus lambda dan menggunakan F secara langsung (beralih dari gaya pointful ke style point-free di atas) disebut pengurangan eta , sebuah istilah yang berasal dari kalkulus lambda .

Ada ekspresi bebas titik yang tidak dibuat oleh pengurangan eta, contoh paling klasik di antaranya adalah komposisi fungsi . Diberikan fungsi dari tipe A ke tipe B dan fungsi dari tipe B ke tipe C, kita dapat menyusunnya menjadi fungsi dari tipe A ke tipe C:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

Sekarang anggaplah kita memiliki metode Result deriveResult(Foo foo). Karena Predikat adalah Fungsi, kami dapat membuat predikat baru yang pertama kali memanggil deriveResultdan kemudian menguji hasil yang diperoleh:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

Meskipun pelaksanaan dari composepenggunaan gaya yg pd waktunya dengan lambdas, yang digunakan dari compose untuk menentukan isFoosResultInFuturetitik bebas karena tidak ada argumen perlu disebutkan.

Pemrograman titik bebas juga disebut pemrograman diam - diam , dan itu bisa menjadi alat yang ampuh untuk meningkatkan keterbacaan dengan menghapus rincian tanpa tujuan (pun intended) dari definisi fungsi. Java 8 tidak mendukung pemrograman bebas titik hampir secara menyeluruh seperti bahasa yang lebih fungsional seperti Haskell lakukan, tetapi aturan praktis yang baik adalah untuk selalu melakukan pengurangan eta. Tidak perlu menggunakan lambdas yang tidak memiliki perilaku berbeda dari fungsi yang dibungkus.


1
Saya pikir apa yang saya dan rekan saya bicarakan lebih dari dua tugas ini: Predicate<Result> pred = this::isResultInFuture;dan di Predicate<Result> pred = resultInFuture();mana resultInFuture () mengembalikan a Predicate<Result>. Jadi sementara ini adalah penjelasan yang bagus tentang kapan menggunakan referensi metode vs. ekspresi lambda, itu tidak cukup mencakup kasus ketiga ini.
M. Justin

4
@ M.Justin: resultInFuture();Tugas ini identik dengan tugas lambda yang saya sajikan, kecuali dalam kasus saya resultInFuture()metode Anda diuraikan. Sehingga pendekatan itu memiliki dua lapisan tipuan (tidak ada yang melakukan apa-apa) - metode pembangunan lambda dan lambda itu sendiri. Lebih buruk lagi!
Jack

5

Tak satu pun dari jawaban saat ini benar-benar membahas inti dari pertanyaan, yaitu apakah kelas harus memiliki private boolean isResultInFuture(Result result)metode atau private Predicate<Result> resultInFuture()metode. Sementara mereka sebagian besar setara, ada kelebihan dan kekurangan masing-masing.

Pendekatan pertama lebih alami jika Anda memanggil metode secara langsung selain membuat referensi metode. Misalnya, jika Anda menguji metode ini, mungkin akan lebih mudah untuk menggunakan pendekatan pertama. Keuntungan lain untuk pendekatan pertama adalah bahwa metode ini tidak harus peduli tentang bagaimana itu digunakan, memisahkannya dari situs panggilannya. Jika nanti Anda mengubah kelas sehingga Anda memanggil metode secara langsung, decoupling ini akan membuahkan hasil dengan cara yang sangat kecil.

Pendekatan pertama juga lebih baik jika Anda mungkin ingin mengadaptasi metode ini ke antarmuka fungsional yang berbeda atau serikat antarmuka yang berbeda. Misalnya, Anda mungkin menginginkan referensi metode bertipe Predicate<Result> & Serializable, yang pendekatan kedua tidak memberi Anda fleksibilitas untuk mencapainya.

Di sisi lain, pendekatan kedua lebih alami jika Anda berniat untuk melakukan operasi tingkat tinggi pada predikat. Predikat memiliki beberapa metode yang tidak dapat dipanggil secara langsung pada referensi metode. Jika Anda bermaksud menggunakan metode ini, pendekatan kedua lebih unggul.

Pada akhirnya keputusan itu subyektif. Tetapi jika Anda tidak bermaksud memanggil metode pada Predikat, saya akan cenderung memilih pendekatan pertama.


Operasi dengan urutan yang lebih tinggi pada predikat masih tersedia dengan memberikan referensi metode:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack

4

Saya tidak menulis kode Java lagi, tetapi saya menulis dalam bahasa fungsional untuk mencari nafkah, dan juga mendukung tim lain yang sedang belajar pemrograman fungsional. Lambdas memiliki titik manis yang mudah dibaca. Gaya yang diterima secara umum untuk mereka adalah Anda menggunakannya ketika Anda bisa inline mereka, tetapi jika Anda merasa perlu untuk menetapkannya ke variabel untuk digunakan nanti, Anda mungkin harus mendefinisikan sebuah metode.

Ya, orang-orang menetapkan lambdas ke variabel dalam tutorial sepanjang waktu. Itu hanya tutorial untuk membantu memberi Anda pemahaman menyeluruh tentang cara kerjanya. Mereka sebenarnya tidak digunakan seperti itu dalam praktiknya, kecuali dalam keadaan yang relatif jarang (seperti memilih antara dua lambda menggunakan ekspresi if).

Itu berarti jika lambda Anda cukup pendek sehingga mudah dibaca setelah inlining, seperti contoh dari komentar @JimmyJames, maka lakukanlah:

people = sort(people, (a,b) -> b.age() - a.age());

Bandingkan ini dengan keterbacaan saat Anda mencoba menyejajarkan lambda Anda:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

Untuk contoh khusus Anda, saya pribadi akan menandainya pada permintaan tarik dan meminta Anda untuk menariknya ke metode yang disebutkan karena panjangnya. Java adalah bahasa berliku yang mengganggu, jadi mungkin pada waktunya praktik spesifik bahasanya sendiri akan berbeda dari bahasa lain, tetapi sampai saat itu, pertahankan lambda Anda tetap pendek dan sejajar.


2
Re: verbosity - Sementara penambahan fungsional baru tidak, dengan sendirinya mengurangi banyak verbosity, mereka memungkinkan cara untuk melakukannya. Misalnya Anda dapat mendefinisikan: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)dengan implementasi yang jelas dan kemudian Anda dapat menulis kode seperti ini: people = sort(people, (a,b) -> b.age() - a.age());yang merupakan IMO peningkatan besar.
JimmyJames

Itu contoh yang bagus tentang apa yang saya bicarakan, @JimmyJames. Ketika lambdas pendek dan dapat digarisbawahi, mereka merupakan peningkatan besar Ketika mereka begitu lama sehingga Anda ingin memisahkan mereka dan memberi mereka nama, Anda lebih baik hanya mendefinisikan suatu metode. Terima kasih telah membantu saya memahami bagaimana jawaban saya disalahartikan.
Karl Bielefeldt

Saya pikir jawaban Anda baik-baik saja. Tidak yakin mengapa itu akan ditolak.
JimmyJames
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.