Mengapa metode default dan statis ditambahkan ke antarmuka di Java 8 ketika kita sudah memiliki kelas abstrak?

99

Di Java 8, antarmuka dapat berisi metode yang diimplementasikan, metode statis, dan apa yang disebut metode "default" (yang tidak perlu ditimpa oleh kelas implementasi).

Dalam pandangan saya (mungkin naif), tidak perlu melanggar antarmuka seperti ini. Antarmuka selalu menjadi kontrak yang harus Anda penuhi, dan ini adalah konsep yang sangat sederhana dan murni. Sekarang ini adalah campuran dari beberapa hal. Menurutku:

  1. metode statis bukan milik antarmuka. Mereka termasuk kelas utilitas.
  2. Metode "default" seharusnya tidak diizinkan di antarmuka sama sekali. Anda selalu dapat menggunakan kelas abstrak untuk tujuan ini.

Pendeknya:

Sebelum Java 8:

  • Anda bisa menggunakan kelas abstrak dan reguler untuk menyediakan metode statis dan default. Peran antarmuka jelas.
  • Semua metode dalam antarmuka harus diganti dengan menerapkan kelas.
  • Anda tidak dapat menambahkan metode baru di antarmuka tanpa memodifikasi semua implementasi, tetapi ini sebenarnya adalah hal yang baik.

Setelah Java 8:

  • Hampir tidak ada perbedaan antara antarmuka dan kelas abstrak (selain multiple inheritance). Bahkan Anda bisa meniru kelas reguler dengan antarmuka.
  • Saat memprogram implementasi, pemrogram mungkin lupa untuk mengganti metode default.
  • Ada kesalahan kompilasi jika kelas mencoba menerapkan dua atau lebih antarmuka yang memiliki metode default dengan tanda tangan yang sama.
  • Dengan menambahkan metode default ke antarmuka, setiap kelas pelaksana secara otomatis mewarisi perilaku ini. Beberapa dari kelas-kelas ini mungkin belum dirancang dengan fungsionalitas baru itu dalam pikiran, dan ini dapat menyebabkan masalah. Misalnya, jika seseorang menambahkan metode default baru default void foo()ke antarmuka Ix, maka kelas yang Cxmengimplementasikan Ixdan memiliki foometode pribadi dengan tanda tangan yang sama tidak dapat dikompilasi.

Apa alasan utama untuk perubahan besar seperti itu, dan apa manfaat baru (jika ada) yang mereka tambahkan?

Tuan Smith
sumber
30
Pertanyaan bonus: Mengapa mereka tidak memperkenalkan banyak pewarisan untuk kelas saja?
2
metode statis bukan milik antarmuka. Mereka termasuk kelas utilitas. Tidak, mereka termasuk dalam @Deprecatedkategori! metode statis adalah salah satu konstruksi paling disalahgunakan di Jawa, karena ketidaktahuan dan kemalasan. Banyak metode statis biasanya berarti programmer tidak kompeten, meningkatkan kopling dengan beberapa urutan besarnya dan merupakan mimpi buruk untuk unit test dan refactor ketika Anda menyadari mengapa mereka adalah ide yang buruk!
11
@JarrodRoberson Bisakah Anda memberikan beberapa petunjuk lagi (tautan akan bagus) tentang "adalah mimpi buruk bagi unit test dan refactor ketika Anda menyadari mengapa itu adalah ide yang buruk!"? Saya tidak pernah memikirkan itu dan saya ingin tahu lebih banyak tentang itu.
berair
11
@ Chris Multiple inheritance of state menyebabkan banyak masalah, terutama dengan alokasi memori ( masalah berlian klasik ). Namun, pewarisan berganda perilaku, hanya tergantung pada pertemuan implementasi kontrak yang telah ditetapkan oleh antarmuka (antarmuka dapat memanggil metode lain yang menyatakan dan membutuhkan). Perbedaan yang halus, tetapi sangat menarik.
ssube
1
Anda ingin menambahkan metode ke antarmuka yang ada, rumit atau hampir tidak mungkin sebelum java8 sekarang Anda bisa menambahkannya sebagai metode default.
VdeX

Jawaban:

59

Contoh motivasi yang baik untuk metode default adalah di perpustakaan standar Java, di mana Anda sekarang miliki

list.sort(ordering);

dari pada

Collections.sort(list, ordering);

Saya tidak berpikir mereka bisa melakukan itu jika tanpa lebih dari satu implementasi yang identik List.sort.

soru
sumber
19
C # mengatasi masalah ini dengan Metode Ekstensi.
Robert Harvey
5
dan memungkinkan daftar yang ditautkan untuk menggunakan O (1) ruang ekstra dan O (n log n) penggabungan waktu, karena daftar tertaut dapat digabungkan di tempatnya, di java 7 ia dibuang ke array eksternal dan kemudian mengurutkannya
ratchet freak
5
Saya menemukan makalah ini di mana Goetz menjelaskan masalahnya. Jadi saya akan menandai jawaban ini sebagai solusi untuk saat ini.
Tuan Smith
1
@RobertHarvey: Buat Daftar dua ratus juta item <Byte>, gunakan IEnumerable<Byte>.Appenduntuk bergabung dengan mereka, lalu panggil Count, lalu ceritakan bagaimana metode ekstensi menyelesaikan masalah. Jika CountIsKnowndan Countadalah anggota IEnumerable<T>, pengembalian dari Appenddapat beriklan CountIsKnownjika koleksi konstituen melakukannya, tetapi tanpa metode seperti itu tidak mungkin.
supercat
6
@ supercat: Saya tidak tahu apa yang Anda bicarakan.
Robert Harvey
48

Jawaban yang benar sebenarnya ditemukan dalam Dokumentasi Java , yang menyatakan:

[d] metode efektif memungkinkan Anda untuk menambahkan fungsionalitas baru ke antarmuka perpustakaan Anda dan memastikan kompatibilitas biner dengan kode yang ditulis untuk versi yang lebih lama dari antarmuka tersebut.

Ini telah lama menjadi sumber rasa sakit di Jawa, karena antarmuka cenderung tidak mungkin untuk berevolusi setelah dipublikasikan. (Konten dalam dokumentasi terkait dengan makalah yang Anda tautkan dalam komentar: Evolusi antarmuka melalui metode ekstensi virtual .) Selain itu, adopsi cepat fitur baru (misalnya lambda dan API aliran baru) hanya dapat dilakukan dengan memperluas antarmuka koleksi yang ada dan menyediakan implementasi standar. Melanggar kompatibilitas biner atau memperkenalkan API baru akan berarti bahwa beberapa tahun akan berlalu sebelum fitur-fitur terpenting Java 8 akan digunakan secara umum.

Alasan untuk mengizinkan metode statis di antarmuka sekali lagi diungkapkan oleh dokumentasi: [t] itu membuatnya lebih mudah bagi Anda untuk mengatur metode pembantu di perpustakaan Anda; Anda dapat menyimpan metode statis khusus untuk antarmuka di antarmuka yang sama daripada di kelas yang terpisah. Dengan kata lain, kelas utilitas statis seperti java.util.Collectionssekarang dapat (akhirnya) dianggap sebagai anti-pola, secara umum (tentu saja tidak selalu ). Dugaan saya adalah menambahkan dukungan untuk perilaku ini sepele setelah metode ekstensi virtual diterapkan, jika tidak, mungkin tidak akan dilakukan.

Pada catatan yang sama, contoh bagaimana fitur baru ini dapat bermanfaat adalah dengan mempertimbangkan satu kelas yang baru - baru ini mengganggu saya java.util.UUID,. Itu tidak benar-benar memberikan dukungan untuk UUID tipe 1, 2, atau 5, dan tidak dapat dengan mudah dimodifikasi untuk melakukannya. Itu juga macet dengan generator acak yang sudah ditentukan sebelumnya yang tidak bisa diganti. Menerapkan kode untuk jenis UUID yang tidak didukung membutuhkan ketergantungan langsung pada API pihak ketiga dan bukan antarmuka, atau pemeliharaan kode konversi dan biaya pengumpulan sampah tambahan. Dengan metode statis, UUIDbisa saja didefinisikan sebagai antarmuka, yang memungkinkan implementasi pihak ketiga nyata dari bagian yang hilang. (Jika UUIDawalnya didefinisikan sebagai antarmuka, kita mungkin akan memiliki semacam kikukUuidUtil kelas dengan metode statis, yang akan mengerikan juga.) Banyak API inti Java mengalami degradasi karena gagal mendasarkan diri pada antarmuka, tetapi pada Java 8 jumlah alasan untuk perilaku buruk ini telah berkurang.

Tidak benar untuk mengatakan bahwa [t] di sini sebenarnya tidak ada perbedaan antara antarmuka dan kelas abstrak , karena kelas abstrak dapat memiliki status (yaitu, menyatakan bidang) sedangkan antarmuka tidak dapat. Oleh karena itu tidak setara dengan pewarisan berganda atau bahkan warisan gaya campuran. Mixin yang tepat (seperti sifat Groovy 2.3 ) memiliki akses ke negara. (Groovy juga mendukung metode ekstensi statis.)

Ini juga bukan ide yang baik untuk mengikuti contoh Doval , menurut pendapat saya. Antarmuka seharusnya mendefinisikan kontrak, tetapi tidak seharusnya menegakkan kontrak. (Lagipula tidak di Jawa.) Verifikasi yang tepat atas suatu implementasi adalah tanggung jawab test suite atau alat lain. Menentukan kontrak dapat dilakukan dengan anotasi, dan OVal adalah contoh yang baik, tapi saya tidak tahu apakah itu mendukung batasan yang didefinisikan pada antarmuka. Sistem seperti itu layak, bahkan jika saat ini belum ada. (Strategi mencakup penyesuaian waktu kompilasi javacmelalui prosesor anotasiAPI dan pembuatan bytecode run-time.) Idealnya, kontrak akan diberlakukan pada waktu kompilasi, dan kasus terburuk menggunakan suite uji, tetapi pemahaman saya adalah bahwa penerapan runtime tidak disukai. Alat lain yang menarik yang mungkin membantu pemrograman kontrak di Jawa adalah Kerangka Pemeriksa .

ngreen
sumber
1
Untuk tindak lanjut lebih lanjut pada paragraf terakhir saya (yaitu jangan memaksakan kontrak dalam antarmuka ), ada baiknya menunjukkan bahwa defaultmetode tidak dapat menimpa equals, hashCodedan toString. Analisis biaya / manfaat yang sangat informatif tentang mengapa hal ini dilarang dapat ditemukan di sini: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…
ngreen
Sayang sekali bahwa Java hanya memiliki satu virtual equalsdan satu hashCodemetode karena ada dua jenis persamaan yang mungkin perlu diuji oleh koleksi, dan item yang akan mengimplementasikan banyak antarmuka mungkin terjebak dengan persyaratan kontrak yang saling bertentangan. Mampu menggunakan daftar yang tidak akan berubah sebagai hashMapkunci sangat membantu, tetapi ada juga saat-saat ketika akan sangat membantu untuk menyimpan koleksi di hashMapmana yang cocok dengan hal-hal yang didasarkan pada kesetaraan daripada keadaan saat ini [ekivalensi menyiratkan keadaan yang cocok dan tidak dapat diubah ] .
supercat
Nah, Java semacam memiliki solusi dengan Comparator dan antarmuka yang sebanding. Tapi itu agak jelek, kurasa.
ngreen
Mereka interface hanya didukung untuk jenis koleksi tertentu, dan mereka menimbulkan masalah mereka sendiri: pembanding mungkin itu sendiri berpotensi merangkum negara (misalnya string pembanding khusus mungkin mengabaikan sejumlah dikonfigurasi karakter pada awal setiap string, dalam hal jumlah karakter untuk diabaikan akan menjadi bagian dari keadaan pembanding) yang pada gilirannya akan menjadi bagian dari keadaan koleksi apa pun yang disortir olehnya, tetapi tidak ada mekanisme yang pasti untuk menanyakan dua pembanding jika mereka setara.
supercat
Oh ya, saya mendapat sakit dari pembanding. Saya sedang mengerjakan struktur pohon yang seharusnya sederhana tetapi bukan karena sangat sulit untuk mendapatkan pembanding dengan benar. Saya mungkin akan menulis kelas pohon kustom hanya untuk membuat masalah hilang.
ngreen
44

Karena Anda hanya dapat mewarisi satu kelas. Jika Anda memiliki dua antarmuka yang implementasinya cukup kompleks sehingga Anda memerlukan kelas dasar abstrak, kedua antarmuka tersebut saling eksklusif dalam praktiknya.

Alternatifnya adalah mengubah kelas-kelas dasar abstrak menjadi kumpulan metode statis dan mengubah semua bidang menjadi argumen. Itu akan memungkinkan setiap implementator antarmuka untuk memanggil metode statis dan mendapatkan fungsionalitas, tetapi itu adalah banyak sekali dalam bahasa yang sudah terlalu verbose.


Sebagai contoh yang memotivasi mengapa bisa menyediakan implementasi dalam antarmuka bisa bermanfaat, pertimbangkan antarmuka Stack ini:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

Tidak ada cara untuk menjamin bahwa ketika seseorang mengimplementasikan antarmuka, popakan mengeluarkan pengecualian jika tumpukan kosong. Kita dapat menegakkan aturan ini dengan memisahkan popmenjadi dua metode: public finalmetode yang menegakkan kontrak dan protected abstractmetode yang melakukan popping yang sebenarnya.

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

Kami tidak hanya memastikan bahwa semua implementasi mematuhi kontrak, kami juga membebaskan mereka dari keharusan memeriksa apakah tumpukan kosong dan melempar pengecualian. Ini adalah kemenangan besar! ... kecuali fakta bahwa kami harus mengubah antarmuka menjadi kelas abstrak. Dalam bahasa dengan pewarisan tunggal, itu kehilangan fleksibilitas yang besar. Itu membuat calon antarmuka Anda saling eksklusif. Mampu memberikan implementasi yang hanya mengandalkan metode antarmuka sendiri akan menyelesaikan masalah.

Saya tidak yakin apakah pendekatan Java 8 untuk menambahkan metode ke antarmuka memungkinkan menambahkan metode akhir atau metode abstrak yang dilindungi, tapi saya tahu bahasa D memungkinkan dan menyediakan dukungan asli untuk Desain berdasarkan Kontrak . Tidak ada bahaya dalam teknik ini karena popbersifat final, sehingga tidak ada kelas pelaksana yang bisa menimpanya.

Adapun implementasi standar dari metode yang dapat ditimpa, saya menganggap implementasi standar apa pun yang ditambahkan ke Java API hanya bergantung pada kontrak antarmuka yang ditambahkan, sehingga setiap kelas yang mengimplementasikan antarmuka dengan benar juga akan berperilaku benar dengan implementasi default.

Bahkan,

Hampir tidak ada perbedaan antara antarmuka dan kelas abstrak (selain multiple inheritance). Bahkan Anda bisa meniru kelas reguler dengan antarmuka.

Ini tidak sepenuhnya benar karena Anda tidak dapat mendeklarasikan bidang dalam antarmuka. Metode apa pun yang Anda tulis di antarmuka tidak dapat mengandalkan detail implementasi apa pun.


Sebagai contoh yang mendukung metode statis dalam antarmuka, pertimbangkan kelas utilitas seperti Koleksi di API Java. Kelas itu hanya ada karena metode statis tidak dapat dideklarasikan di antarmuka masing-masing. Collections.unmodifiableListbisa saja dinyatakan di Listantarmuka, dan itu akan lebih mudah ditemukan.

Doval
sumber
4
Counter-argumen: karena metode statis, jika ditulis dengan benar, mandiri, mereka lebih masuk akal dalam kelas statis terpisah di mana mereka dapat dikumpulkan dan dikategorikan berdasarkan nama kelas, dan kurang masuk akal dalam antarmuka, di mana mereka pada dasarnya adalah kenyamanan yang mengundang penyalahgunaan seperti memegang keadaan statis di objek atau menyebabkan efek samping, membuat metode statis tidak dapat diuji.
Robert Harvey
3
@RobertHarvey Apa yang menghentikan Anda melakukan hal-hal yang sama konyolnya jika metode statis Anda ada di kelas? Selain itu, metode di antarmuka mungkin tidak memerlukan status apa pun sama sekali. Anda mungkin hanya mencoba untuk menegakkan kontrak. Misalkan Anda memiliki Stackantarmuka dan Anda ingin memastikan bahwa ketika popdipanggil dengan tumpukan kosong, pengecualian dilemparkan. Diberikan metode abstrak boolean isEmpty()dan protected T pop_impl(), Anda dapat menerapkan final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Ini memberlakukan kontrak pada SEMUA pelaksana.
Doval
Tunggu apa? Metode push dan Pop pada stack tidak akan terjadi static.
Robert Harvey
@ RobertTarvey Saya akan lebih jelas jika bukan karena batas karakter pada komentar, tapi saya membuat kasus untuk implementasi standar dalam sebuah antarmuka, bukan metode statis.
Doval
8
Saya pikir metode antarmuka standar lebih merupakan peretasan yang telah diperkenalkan untuk dapat memperluas perpustakaan standar tanpa perlu mengadaptasi kode yang ada berdasarkan itu.
Giorgio
2

Mungkin maksudnya adalah untuk menyediakan kemampuan untuk membuat kelas mixin dengan mengganti kebutuhan untuk menyuntikkan informasi atau fungsi statis melalui ketergantungan.

Gagasan ini tampaknya terkait dengan bagaimana Anda dapat menggunakan metode ekstensi dalam C # untuk menambahkan fungsionalitas yang diimplementasikan ke antarmuka.

rae1
sumber
1
Metode ekstensi tidak menambah fungsionalitas ke antarmuka. Metode ekstensi hanyalah gula sintaksis untuk memanggil metode statis di kelas menggunakan list.sort(ordering);formulir yang mudah .
Robert Harvey
Jika Anda melihat IEnumerableantarmuka dalam C #, Anda dapat melihat bagaimana menerapkan metode ekstensi ke antarmuka itu (seperti LINQ to Objectshalnya) menambahkan fungsionalitas untuk setiap kelas yang mengimplementasikan IEnumerable. Itulah yang saya maksud dengan menambahkan fungsionalitas.
rae1
2
Itulah hal hebat tentang metode penyuluhan; mereka memberikan ilusi bahwa Anda berlari pada fungsionalitas ke kelas atau antarmuka. Hanya saja, jangan bingung dengan menambahkan metode aktual ke kelas; metode kelas memiliki akses ke anggota pribadi dari suatu objek, metode ekstensi tidak (karena mereka benar-benar hanya cara lain memanggil metode statis).
Robert Harvey
2
Tepat, dan itulah sebabnya saya melihat beberapa hubungan memiliki metode statis atau standar dalam sebuah antarmuka di Java; implementasi didasarkan pada apa yang tersedia untuk antarmuka bukan kelas itu sendiri.
rae1
1

Dua tujuan utama yang saya lihat dalam defaultmetode (beberapa kasus penggunaan melayani kedua tujuan):

  1. Gula sintaksis. Kelas utilitas dapat melayani tujuan itu, tetapi metode instance lebih baik.
  2. Perpanjangan antarmuka yang ada. Implementasinya generik tetapi terkadang tidak efisien.

Jika itu hanya tentang tujuan kedua, Anda tidak akan melihatnya di antarmuka baru seperti Predicate. Semua @FunctionalInterfaceantarmuka beranotasi wajib memiliki tepat satu metode abstrak agar lambda dapat mengimplementasikannya. Ditambahkan defaultmetode seperti and, or, negatehanya utilitas, dan Anda tidak seharusnya menimpa mereka. Namun, terkadang metode statis akan lebih baik .

Adapun ekstensi dari antarmuka yang ada - bahkan di sana, beberapa metode baru hanya gula sintaks. Metode Collectionseperti stream, forEach, removeIf- pada dasarnya, itu hanya utilitas Anda tidak perlu menimpa. Dan kemudian ada metode seperti spliterator. Implementasi defaultnya adalah suboptimal, tapi hei, setidaknya kompilasi kode. Gunakan hanya ini jika antarmuka Anda sudah diterbitkan dan digunakan secara luas.


Adapun staticmetode, saya kira yang lain cukup baik: Ini memungkinkan antarmuka untuk menjadi kelas utilitas sendiri. Mungkin kita bisa menyingkirkan Collectionsmasa depan Jawa? Set.empty()akan bergoyang.

Vlasec
sumber