Zaten soyut sınıflarımız varken neden Java 8'deki arayüzlere varsayılan ve statik yöntemler eklendi?


99

Java 8'de, arabirimler uygulanmış yöntemler, statik yöntemler ve "varsayılan" yöntemler (uygulama sınıflarının geçersiz kılması gerekmeyen) içerebilir.

Benim (muhtemelen saf) görüşüme göre, bu gibi arayüzleri ihlal etmeye gerek yoktu. Arayüzler her zaman yapmanız gereken bir sözleşme olmuştur ve bu çok basit ve saf bir konsepttir. Şimdi birkaç şeyin karışımı. Bence:

  1. statik yöntemler arayüzlere ait değildir. Onlar faydalı sınıflara aittir.
  2. “varsayılan” yöntemlere hiçbir zaman arayüzlerde izin verilmemeliydi. Bu amaç için her zaman soyut bir sınıf kullanabilirsiniz.

Kısacası:

Java 8'den Önce:

  • Statik ve varsayılan yöntemler sağlamak için soyut ve normal sınıfları kullanabilirsiniz. Arayüzlerin rolü açıktır.
  • Arayüzdeki tüm yöntemler sınıfları uygulayarak geçersiz kılınmalıdır.
  • Tüm uygulamaları değiştirmeden bir arayüze yeni bir yöntem ekleyemezsiniz, ancak bu aslında iyi bir şey.

Java 8'den sonra:

  • Bir arabirim ile soyut bir sınıf arasında (çoklu kalıtım dışında) hemen hemen hiçbir fark yoktur. Aslında normal bir sınıfı bir arayüzle taklit edebilirsiniz.
  • Uygulamaları programlarken, programcılar varsayılan yöntemleri geçersiz kılmayı unutabilirler.
  • Bir sınıf aynı imzayla varsayılan bir yönteme sahip iki veya daha fazla arabirim uygulamaya çalışırsa, derleme hatası oluşur.
  • Bir arabirime varsayılan bir yöntem ekleyerek, her uygulayıcı sınıf bu davranışı otomatik olarak devralır. Bu sınıfların bazıları akılda bu yeni işlevsellik ile tasarlanmamış olabilir ve bu sorunlara neden olabilir. Örneğin, birisi default void foo()bir arayüze yeni bir varsayılan yöntem eklerse , aynı imzayla özel bir yöntem uygulayan ve uygulayan Ixsınıf derlenmez.CxIxfoo

Bu gibi büyük değişikliklerin temel nedenleri nelerdir ve (varsa) hangi yeni yararları ekler?


30
Bonus soru: Neden sınıflar için çoklu kalıtım getirmediler?

2
statik yöntemler arayüzlere ait değildir. Onlar faydalı sınıflara aittir. Hayır, onlar @Deprecatedkategoriye ait! Statik yöntemler, Java'da cehalet ve tembellik nedeniyle en çok suistimal edilen yapılardan biridir. Birçok statik yöntem genellikle yetersiz programlayıcı anlamına gelir, eşleşmeyi birkaç büyüklük sırasına göre artırır ve neden kötü bir fikir olduklarını fark ettiğinizde ünite testi ve refaktöre kabus olur!

11
@JarrodRoberson "Neden kötü bir fikir olduklarını fark ettiğinizde ünite testine ve refactor'a bir kabus musunuz!" Hakkında biraz daha ipucu verebilir misiniz (bağlantılar harika olurdu)? Bunu hiç düşünmedim ve bu konuda daha fazla bilgi edinmek istiyorum.
Sulu

11
@Chris Çoklu devlet mirası, özellikle bellek tahsisi ( klasik elmas problemi ) ile bir sürü soruna neden olur . Bununla birlikte, çoklu davranış mirası, sadece arayüz tarafından belirlenmiş olan sözleşmeyi karşılayan uygulamaya bağlıdır (arayüz bildirdiği ve gerektirdiği diğer yöntemleri çağırabilir). İnce, ama çok ilginç bir ayrım.
ssube

1
java8 şimdi u sadece varsayılan yöntem olarak ekleyebilir önce mevcut arabirimine yöntem eklemek istiyorum, hantal veya neredeyse mümkün değildi.
VdeX

Yanıtlar:


59

Varsayılan yöntemler için iyi motive edici bir örnek, şu anda sahip olduğunuz Java standart kütüphanesindedir.

list.sort(ordering);

onun yerine

Collections.sort(list, ordering);

Bunun birden fazla aynı uygulaması olmadan yapabileceklerini sanmıyorum List.sort.


19
C # bu sorunun Uzatma Yöntemleri ile aşar.
Robert Harvey,

5
ve bağlantılı bir listenin bir O (1) fazladan boşluk ve O (n log n) zaman birleştirme noktası kullanmasına olanak tanır, çünkü bağlantılı listeler yerinde birleştirilebilir, java 7'de harici bir diziye dökülür ve sonra bunu sırala
cırcır ucube

5
Goetz'in sorunu açıkladığı bu kağıdı buldum . Bu yüzden şimdilik bu cevabı çözüm olarak işaretleyeceğim.
Mister Smith,

1
@RobertHarvey: İki yüz milyon maddelik Liste <Byte> Yaratın, IEnumerable<Byte>.Appendonlara katılmak için kullanın ve ardından çağrı yapın Count, sonra uzatma yöntemlerinin sorunu nasıl çözdüğünü söyleyin. Eğer CountIsKnownve Countüyeleri olsaydı, kurucu koleksiyonlar olsaydı IEnumerable<T>, geri dönüş Appendreklam CountIsKnownyapabilirdi, ancak bu mümkün olmayan yöntemler yoktu.
supercat

6
@supercat: Neden bahsettiğin hakkında en ufak bir fikrim yok.
Robert Harvey

48

Doğru cevabı aslında Java Dokümantasyonunda bulunur :

[d] efault yöntemleri, kütüphanelerinizin arayüzlerine yeni işlevler eklemenizi ve bu arayüzlerin daha eski sürümleri için yazılmış kodlarla ikili uyumluluk sağlamanızı sağlar.

Bu, Java'da uzun zamandır devam eden bir acı kaynağı olmuştur, çünkü arayüzler halka açıldıktan sonra gelişmeleri imkansızdı. (Dokümantasyondaki içerik, bir yorumda link verdiğiniz kağıtla ilgilidir: Sanal uzatma yöntemleri ile arayüz gelişimi .) Ayrıca, yeni özelliklerin hızlı bir şekilde benimsenmesi (örn. Lambdalar ve yeni akış API'leri) ancak mevcut koleksiyonlar arayüzler ve varsayılan uygulamalar sunar. İkili uyumluluğun kırılması veya yeni API'lerin tanıtılması, Java 8'in en önemli özelliklerinin ortak kullanımda bulunmasından birkaç yıl önce geçeceği anlamına gelir.

Arayüzlerde statik yöntemlere izin vermenin nedeni, belgelerle tekrar ortaya çıkar: [t] kütüphanelerinizde yardımcı yöntemleri düzenlemenizi kolaylaştırır; Bir arayüze özgü statik yöntemleri, ayrı bir sınıftan ziyade aynı arayüzde tutabilirsiniz. Başka bir deyişle, java.util.Collectionsşimdi (nihayet) gibi statik fayda sınıfları genel olarak (elbette her zaman değil ) bir anti-patern olarak düşünülebilir . Tahminime göre, bu uzantıya destek eklemenin, sanal uzatma yöntemleri uygulandığında önemsiz olduğu, aksi takdirde muhtemelen gerçekleşmeyeceği yönündedir.

Benzer bir kayda göre, bu yeni özelliklerin nasıl faydalı olabileceğinin bir örneği, yakın zamanda beni rahatsız eden bir sınıfı göz önünde bulundurmaktır java.util.UUID. UUID tip 1, 2 veya 5 için gerçekten destek sağlamaz ve bunu yapmak için kolayca değiştirilemez. Ayrıca, geçersiz kılınamayan önceden tanımlanmış rastgele bir jeneratör ile sıkıştı. Desteklenmeyen UUID türleri için kod uygulamak, bir arabirimden ziyade üçüncü taraf bir API'ye doğrudan bağımlılık veya dönüşüm kodunun sürdürülmesi ve bununla birlikte ek atık toplama masrafı gerektirir. Statik yöntemlerle, UUIDeksik parçaların gerçek üçüncü taraf uygulamalarına izin vererek, bunun yerine bir arayüz olarak tanımlanabilirdi. (Eğer UUIDaslında bir arayüz olarak tanımlanmış olsaydı, muhtemelen bir çeşit toparlanmış olurduk.UuidUtil Çok korkunç olabilecek statik yöntemlerle sınıf.

[T] 'nin burada bir arayüz ile soyut sınıf arasında hemen hemen hiçbir fark olmadığını söylemek doğru değil , çünkü soyut sınıflar arayüzler yapamazken devlete sahip olabilir (yani alanları bildirir). Bu nedenle çoklu mirasa veya hatta mixin tarzı mirasa eşdeğer değildir. Uygun karışımlar (Groovy 2.3'ün özellikleri gibi ) duruma erişebilir. (Groovy ayrıca statik uzatma yöntemlerini de destekler.)

Ayrıca , Bence Doval'ın örneğini takip etmek de iyi bir fikir değil . Bir arabirimin bir sözleşme tanımlaması gerekiyordu, ancak sözleşmeyi yürürlüğe koymaması gerekiyordu. (Yine de Java'da değil.) Bir uygulamanın doğru şekilde doğrulanması bir test paketinin veya başka bir aracın sorumluluğundadır. Sözleşmelerin tanımlanması ek açıklamalar ile yapılabilir ve OVal iyi bir örnektir, ancak arayüzlerde tanımlanan kısıtlamaları destekleyip desteklemediğini bilmiyorum. Böyle bir sistem, şu anda mevcut olmasa bile uygulanabilir. (Stratejiler javac, açıklama işlemcisi aracılığıyla derleme zamanı özelleştirmesini içerirAPI ve çalışma zamanı bytecode üretimi.) İdeal olarak, sözleşmeler derleme zamanında uygulanır ve bir test paketi kullanılarak en kötü durumda, ancak benim anladığım kadarıyla çalışma zamanı uygulamasının kaşlarını çattığını. Java'da sözleşme programlamasına yardımcı olabilecek diğer ilginç bir araç da Checker Framework'tür .


1
Ayrıca (yani benim son paragrafta üzerinde takip için arayüzlerinde sözleşmeleri zorlamaz ), bu işaret değer defaultyöntemleri geçersiz kılamaz equals, hashCodeve toString. Bu izin verilmeyen neden bir çok bilgilendirici maliyet / fayda analizi burada bulunabilir: mail.openjdk.java.net/pipermail/lambda-dev/2013-March/...
ngreen

Java'nın yalnızca tek bir sanal equalsve tek bir hashCodeyönteme sahip olması çok kötü, çünkü koleksiyonların test etmesi gerekebilecek iki farklı eşitlik türü vardır ve birden çok arayüzü uygulayacak öğeler birbiriyle çelişen sözleşmelere tabi tutulabilir. hashMapAnahtar olarak değişmeyecek listeleri kullanabilmek faydalıdır, ancak koleksiyonları hashMapmevcut durumdan ziyade eşdeğerliğe dayanarak eşleştirilen eşyalarda saklamanın yararlı olacağı zamanlar da vardır (eşdeğerlik eşleşme durumu ve değişkenlik anlamına gelir ). .
supercat

Peki, Java tür karşılaştırıcı ve karşılaştırılabilir arayüzleri ile bir geçici çözüm vardır. Ama bunlar çok çirkin, sanırım.
ngreen

Bu arayüzler sadece belirli toplama türleri için desteklenir ve kendi sorun teşkil: Bir karşılaştırıcı olabilir kendisi potansiyel olarak (devlet encapsulate örneğin her dizenin başında karakterlerin yapılandırılabilenleri, görmezden olabilecek bir uzman dize karşılaştırıcı bu durumda numarası yoksayılacak karakterler, sırayla sıralanan herhangi bir koleksiyonun durumunun bir parçası haline gelebilecek olan karşılaştırıcının durumunun bir parçası olacaktır), ancak iki karşılaştırıcıdan eşdeğer olup olmadıklarını sormak için tanımlanmış bir mekanizma yoktur.
supercat

Oh evet, karşılaştırıcıların acısını alıyorum. Basit olması gereken bir ağaç yapısı üzerinde çalışıyorum ama karşılaştırıcıyı doğru yapmak çok zor değil. Muhtemelen sorunu ortadan kaldırmak için özel bir ağaç sınıfı yazacağım.
ngreen

44

Çünkü sadece bir sınıfa miras alabilirsiniz. Uygulaması soyut bir temel sınıfa ihtiyacınız olacak kadar karmaşık olan iki arayüzünüz varsa, bu iki arayüz pratikte birbirini dışlar.

Alternatif, bu soyut temel sınıfları statik yöntem koleksiyonuna dönüştürmek ve tüm alanları argümana dönüştürmektir. Bu, arabirimin herhangi bir uygulayıcısının statik yöntemleri çağırmasını ve işlevselliğini elde etmesini sağlar, ancak bu zaten çok ayrıntılı olan bir dilde çok fazla bir kazandır.


Arayüzlerde uygulamalar sağlayabilmenin nedenini motive eden bir örnek olarak, bu Yığın arayüzünü göz önünde bulundurun:

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

    T pop() throws EmptyException;
 }

Birisi arayüzü uyguladığında pop, yığın boşsa bir istisna atacağını garanti etmenin bir yolu yoktur . Bu kuralı popiki yönteme ayırarak uygulayabiliriz : public finalsözleşmeyi uygulayan bir protected abstractyöntem ve fiili patlamayı yapan bir yöntem.

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();
        }
    }
 }

Tüm uygulamaların sözleşmeye saygı göstermesini sağlamakla kalmıyor, aynı zamanda yığının boş olup olmadığını ve istisnayı atıp atmadıklarını kontrol etmelerini de önlüyoruz. Arayüzü soyut bir sınıfa dönüştürmek zorunda kalmamız dışında, büyük bir kazanç! Kalıtımsal bir dilde, bu büyük bir esneklik kaybıdır. Sizin arabirimlerinizi karşılıklı olarak özel yapar. Yalnızca arayüz yöntemlerine dayanan uygulamalar sağlayabilmek sorunu çözecektir.

Java 8'in arabirimlere yöntem ekleme yaklaşımının son yöntemlerin mi yoksa korumalı soyut yöntemlerin mi eklendiğinden emin değilim, ancak D dilinin izin verdiğini ve Sözleşme ile Tasarım için yerel destek sağladığını biliyorum . popKesin olduğu için bu teknikte tehlike yoktur , bu nedenle uygulayıcı sınıf onu geçersiz kılamaz.

Geçersiz kılınabilen yöntemlerin varsayılan uygulamalarına gelince, Java API'lerine eklenen varsayılan uygulamaların yalnızca eklendikleri arabirimin sözleşmesine dayandığını varsayıyorum, bu nedenle arabirimi doğru şekilde uygulayan herhangi bir sınıf varsayılan uygulamalarla doğru şekilde davranır.

Dahası,

Bir arabirim ile soyut bir sınıf arasında (çoklu kalıtım dışında) hemen hemen hiçbir fark yoktur. Aslında normal bir sınıfı bir arayüzle taklit edebilirsiniz.

Arabirimdeki alanları bildiremediğiniz için bu doğru değildir. Arayüze yazdığınız hiçbir yöntem, uygulama detaylarına güvenemez.


Arayüzlerdeki statik yöntemlerin lehine bir örnek olarak, Java API'sindeki Koleksiyonlar gibi yardımcı sınıfları düşünün . Bu sınıf sadece var çünkü bu statik yöntemler kendi arayüzlerinde bildirilemiyor. Arayüzde Collections.unmodifiableListbildirildiği kadar iyi olabilirdi Listve bulmak daha kolay olurdu.


4
Karşı Argüman: Statik metotlar, eğer doğru bir şekilde yazılırlarsa, kendi kendine yetiyorlarsa, ayrı bir statik sınıfta, sınıf adına göre toplanabilecekleri ve kategorize edilebilecekleri daha anlamlı ve bir arayüzde daha az anlam ifade ediyorlar. Bu, statik yöntemleri nesnede tutmak veya yan etkilere neden olmak gibi istismarları davet ederek statik yöntemleri dengelenemez hale getirir.
Robert Harvey,

3
@RobertHarvey Statik yönteminiz bir sınıftaysa eşit derecede aptalca şeyler yapmanıza engel olan şey nedir? Ayrıca, arayüzdeki yöntemin hiç bir durum gerektirmeyebilir. Bir sözleşmeyi yürürlüğe koymaya çalışıyor olabilirsiniz. StackArayüzünüz olduğunu ve popboş bir yığınla çağrıldığında bir istisna atıldığından emin olmak istediğinizi varsayalım . Verilen soyut yöntemler boolean isEmpty()ve protected T pop_impl()uygulayabilirsiniz final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }Bu, TÜM uygulayıcılar için sözleşmeyi zorlar.
Doval

Bir dakika ne? Bir yığın itin ve Pop yöntemleri vardır değil olacaktı static.
Robert Harvey,

@RobertHarvey Yorumlardaki karakter sınırı için olmasaydı daha net olurdu, ancak statik yöntemler yerine arayüzde varsayılan uygulamalar için durum hazırlıyordum.
Doval

8
Varsayılan arabirim yöntemlerinin, standart kütüphaneyi, ona göre mevcut kodu uyarlamaya gerek kalmadan genişletmek için tanıtılmış bir kesmek olduğunu düşünüyorum.
Giorgio

2

Belki de amaç, bağımlılık yoluyla statik bilgi veya işlevsellik enjekte etme ihtiyacını değiştirerek karma sınıflar oluşturma becerisini sağlamaktı .

Bu fikir, arayüzlere uygulanan işlevsellik eklemek için C # 'daki uzatma yöntemlerini nasıl kullanabileceğinizi gösteriyor.


1
Uzantı yöntemleri, arabirimlere işlevsellik katmaz. Uzatma yöntemleri sadece uygun list.sort(ordering);formu kullanarak bir sınıfta statik yöntemleri çağırmak için sözdizimsel bir şekerlemedir .
Robert Harvey,

IEnumerableC # 'daki arayüze bakarsanız, bu arayüze eklenti yöntemlerinin uygulanmasının (gibi LINQ to Objects), uygulayan her sınıf için nasıl işlevsellik eklediğini görebilirsiniz IEnumerable. İşlevsellik ekleyerek demek istedim.
rae 1

2
Uzatma yöntemleri ile ilgili en güzel şey bu; işlevselliği bir sınıf veya arayüze bağladığınız illüzyonunu verir . Sadece bir sınıfa gerçek yöntemler ekleyerek karıştırmayın; sınıf yöntemlerinin bir nesnenin özel üyelerine erişimi vardır, uzantı yöntemleri yoktur (çünkü bunlar gerçekten statik yöntemleri çağırmanın başka bir yoludur).
Robert Harvey,

2
Tam da bu yüzden Java arayüzünde statik veya varsayılan metotlara sahip olmakla ilgili bir ilişki görüyorum; uygulama, sınıfın kendisinde değil, arayüzde bulunanlara dayanmaktadır.
rae 1

1

defaultYöntemlerde gördüğüm iki ana amaç (bazı kullanım durumlarında her iki amaca hizmet eder):

  1. Sözdizimi şekeri. Bir yardımcı sınıf bu amaca hizmet edebilir, ancak örnek yöntemleri daha iyidir.
  2. Mevcut bir arayüzün genişletilmesi. Uygulama genel fakat bazen yetersizdir.

Hemen hemen ikinci amaç olsaydı, bunu yeni bir arayüzde göremezdiniz Predicate. Tüm @FunctionalInterfaceaçıklamalı arayüzlerin, bir lambdanın uygulayabilmesi için tam olarak bir soyut metoda sahip olması gerekir. Eklenen defaultgibi yöntemler and, or, negatesadece yarar vardır ve bunları geçersiz kılmak gerekiyordu. Ancak, bazen statik yöntemler daha iyi yapardı .

Mevcut arabirimlerin genişletilmesine gelince - orada bile olsa, bazı yeni yöntemler sadece sözdizimi şekeridir. Yöntemleri Collectiongibi stream, forEach, removeIf- temelde, geçersiz kılmak gerekmez sadece yarar var. Ve sonra gibi yöntemler var spliterator. Varsayılan uygulama yetersizdir, ancak hey, en azından kod derler. Arayüzünüz zaten yayınlanmış ve yaygın olarak kullanılmışsa, yalnızca başvurunuz.


staticMetotlara gelince , sanırım diğerleri de oldukça iyi örtüyor: Arayüzün kendi yardımcı sınıfı olmasını sağlıyor. Belki CollectionsJava'nın geleceğinden kurtulabiliriz ? Set.empty()sallanırdı.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.