Java jenerikleri ne zaman <? <T> yerine T> 'yı uzatır ve anahtarlamanın dezavantajı var mı?


205

Aşağıdaki örnek göz önüne alındığında (Hamcrest eşleştiricileri ile JUnit kullanımı):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

Bu, JUnit assertThatyönteminin imzasıyla derlenmez :

public static <T> void assertThat(T actual, Matcher<T> matcher)

Derleyici hata mesajı:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

Ancak, assertThatyöntem imzasını şu şekilde değiştirirsem:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Sonra derleme çalışır.

Üç soru var:

  1. Mevcut sürüm neden tam olarak derlenmiyor? Her ne kadar buradaki kovaryans konularını belirsiz bir şekilde anlasam da, yapmam gerekirse kesinlikle açıklayamadım.
  2. assertThatYöntemi değiştirmenin bir dezavantajı var mı Matcher<? extends T>? Bunu yaparsanız kırılacak başka durumlar var mı?
  3. JUnit'te assertThatyöntemin jenerikleştirilmesinin bir anlamı var mı ? MatcherJUnit şey yapmaz bir tür güvenliği zorlamak için bir girişim gibi herhangi jenerik ve sadece bakışlarla yazılmadı maçlar yöntemini çağırır beri olduğu gibi sınıf, bunu gerektirir görünmüyor Matcheraslında sadece olmaz ve test ne olursa olsun başarısız olur. Güvenli olmayan operasyonlar söz konusu değil (ya da öyle görünüyor).

Referans olarak, JUnit uygulaması şöyledir assertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}

bu bağlantı çok faydalıdır (jenerik, kalıtım ve alt tip): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

Yanıtlar:


145

İlk önce sizi http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html adresine yönlendirmem gerekiyor - inanılmaz bir iş çıkarıyor.

Temel fikir,

<T extends SomeClass>

gerçek parametre SomeClassveya herhangi bir alt türü olduğunda.

Örneğinizde,

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

Bunun expected, uygulayan herhangi bir sınıfı temsil eden Sınıf nesnelerini içerebileceğini söylüyorsunuz Serializable. Sonuç haritanız yalnızca Datesınıf nesnelerini tutabileceğini söylüyor .

Sonucun geçmek zaman, sen ayar Ttam Mapbir Stringetmek Dateeşleşmiyor sınıf nesneleri, Mapbir Stringşey olduğu en etmek Serializable.

Bir şey kontrol etmek - emin istediğiniz vardır Class<Date>değil Date? Bir haritası Stringiçin Class<Date>genel olarak pek de kullanışlı gelmiyor (tutabileceği hepsi Date.classdeğerlerden ziyade örnekleri olarak Date)

Genelleştirmeye gelince assertThat, fikir, yöntemin Matchersonuç türüne uyan a'nın iletilmesini sağlayabileceğidir .


Bu durumda, evet bir sınıf haritası istiyorum. Verdiğim örnek, özel sınıflarımdan ziyade standart JDK sınıflarını kullanmaya yöneliktir, ancak bu durumda sınıf aslında yansıma yoluyla somutlaştırılır ve anahtara dayalı olarak kullanılır. (İstemcinin sunucu sınıflarına sahip olmadığı dağıtılmış bir uygulama, sadece sunucu tarafında çalışmak için hangi sınıfın kullanılacağı).
Yishai

6
Sanırım beynimin nerede sıkıştığı, Date türünde sınıflar içeren bir Harita'nın Serializable türünde sınıflar içeren bir tür Google Haritalar'a neden uymadığına bağlı. Serializable türündeki sınıfların başka sınıflar da olabileceğinden emin olun, ancak kesinlikle Date türünü içerir.
Yishai

Oyuncunun sizin için yapıldığından emin olan matcher.matches () yöntemi umursamıyor, bu yüzden T asla kullanılmadığından neden dahil ettiniz? (yöntem dönüş türü geçersizdir)
Yishai

Ahhh - bu kadar yakın olan iddianın savunmasını okumadığım için aldım. Görünüşe göre sadece uygun bir Matcher'ın geçmesini sağlamak ...
Scott Stanchfield

28

Soruyu cevaplayan herkese teşekkürler, bu benim için bir şeyleri netleştirmeye yardımcı oldu. Sonunda Scott Stanchfield'ın cevabı, onu nasıl anladığımla en yakın hale geldi, ancak onu ilk yazdığında anlamadım, umarım bir başkasının faydası olacak şekilde sorunu yeniden anlatmaya çalışıyorum.

Ben sadece bir genel parametresi vardır ve bu daha kolay anlaşılmasını sağlayacaktır çünkü, Liste açısından soruyu yeniden ifade edeceğim.

Parametreleştirilmiş sınıfın amacı ( örnekte olduğu gibi Liste <Date>veya Harita <K, V>gibi) bir downcast'i zorlamak ve derleyicinin bunun güvenli olduğunu garanti etmesini sağlamak (çalışma zamanı istisnası yok).

Liste örneğini düşünün. Sorumun özü, T türü ve bir Liste alan bir yöntemin neden T'den daha fazla miras zincirinde bir şey listesini kabul etmemesidir.

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

List parametresi, dizelerin bir listesi değil, tarihlerin bir listesi olduğu için derlenmez. Derleme olsaydı, jenerikler çok yararlı olmazdı.

Aynı şey Harita için de geçerlidir. Harita <String, Class<? extends Serializable>>ile aynı şey değildir <String, Class<java.util.Date>>. Bunlar kovaryant değildir, bu yüzden tarih sınıflarını içeren haritadan bir değer almak ve serileştirilebilir öğeler içeren haritaya koymak istersem, bu iyi, ancak şunu söyleyen bir yöntem imzası:

private <T> void genericAdd(T value, List<T> list)

Her ikisini de yapabilmek istiyor:

T x = list.get(0);

ve

list.add(value);

Bu durumda, junit yöntemi bu şeyleri gerçekten önemsemese de, yöntem imzası, elde etmediği kovaryans gerektirir, bu nedenle derlenmez.

İkinci soruda,

Matcher<? extends T>

T bir API olduğunda, API'ların amacı olmayan bir şeyi gerçekten kabul etmenin dezavantajı olacaktır. Amaç, eşleştiricinin gerçek nesneyle eşleşmesini statik olarak sağlamaktır ve Nesneyi bu hesaplamadan hariç tutmanın bir yolu yoktur.

Üçüncü sorunun cevabı, denetlenmeyen işlevsellik açısından hiçbir şeyin kaybedilmeyeceğidir (bu yöntem jenerikleştirilmediyse JUnit API'sında güvenli olmayan bir tipleme olmaz), ancak başka bir şey yapmaya çalışıyorlar - statik olarak iki parametrenin eşleşmesi muhtemeldir.

EDIT (daha fazla tefekkür ve deneyimden sonra):

AssertThat yöntemi imzasıyla ilgili büyük sorunlardan biri, değişken bir T'yi T'nin genel parametresiyle eşitlemeye çalışmaktır. Bu işe yaramaz, çünkü bunlar kovaryant değildir. Yani örneğin bir T olabilir, List<String>ancak derleyicinin çalıştığı bir eşleşmeyi geçirin Matcher<ArrayList<T>>. Bir tür parametresi olmasaydı, işler iyi olurdu, çünkü List ve ArrayList kovaryant, ancak Generics, derleyici ile ilgili olarak ArrayList gerektirdiğinden, açık olduğunu umduğum nedenlerden dolayı bir Listeye tahammül edemez yukarıdan.


Yine de neden mahvetemediğimi anlamıyorum. Neden bir tarih listesini serileştirilebilir bir listeye dönüştüremiyorum?
Thomas Ahle

@ThomasAhle, çünkü Tarihlerin bir listesi olduğunu düşünen referanslar, Dizeleri veya diğer serileştirilebilirleri bulduğunda döküm hatalarına neden olur.
Yishai

Anlıyorum, ama eski referanstan bir şekilde kurtulduğumda, List<Date>tipli bir yöntemden döndüğüm gibi List<Object>? Java tarafından izin verilmese bile, güvenli olmalı.
Thomas Ahle

14

Aşağı kaynar:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

C1 Sınıfı başvurusunun Uzun bir örnek içerebileceğini görebilirsiniz (belirli bir zamanda temel alınan nesne olabilirdi List<Long>), ancak "bilinmeyen" sınıfın Tarih olduğuna dair bir garanti olmadığı için açıkça bir Tarihe verilemiyor. Typsesafe değildir, bu yüzden derleyici buna izin vermez.

Ancak, başka bir nesne eklersek List diyelim (örneğinizde bu nesne Eşleştiricidir), o zaman aşağıdakiler doğru olur:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

... Ancak, Liste türü olursa? T yerine T'yi genişletir ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

Bence değiştirerek Matcher<T> to Matcher<? extends T>, temelde l1 = l2 atamaya benzer senaryoyu tanıtıyorsunuz;

İç içe geçmiş joker karakterlere sahip olmak hala çok kafa karıştırıcıdır, ancak umarım bu, birbirlerine jenerik referansları nasıl atayabileceğinize bakarak jeneriklerin neden anlaşılmasına yardımcı olduğu konusunda mantıklıdır. İşlev çağrısı yaptığınızda derleyici T türünü çıkardığından da kafa karıştırıcıdır (açıkça T olduğunu söylemiyorsunuz).


9

Orijinal kod derleme değil nedeni olduğunu <? extends Serializable>mu değil , "serializable genişleten herhangi bir sınıf," ama "seri hale getirilebilir uzanır bilinmeyen ancak belirli sınıfı." Demek

Örneğin, kod yazıldığı gibi verildiğinde, atamak new TreeMap<String, Long.class>()>için tamamen geçerlidir expected. Derleyici kodun derlenmesine izin verdiyse, assertThat()büyük olasılıkla kırılır çünkü haritada bulduğu Datenesneler yerine nesneler beklerdi Long.


1
Ben tam olarak takip etmiyorum - "anlamına gelmez ... ama ..." derken fark nedir? (örneğin, eski tanıma uyan ancak son tanıma uyan "bilinen ancak özgün olmayan" bir sınıf örneği ne olabilir?)
poundifdef

Evet, bu biraz garip; nasıl daha iyi ifade edeceğinden emin değilim ... "" demek daha mantıklı mı? bilinmeyen bir tür, hiçbir şeyle eşleşen bir tür değil mi? "
erickson

1
Bunu açıklamak için ne yardımcı olabilir "Seri halinde genişletilebilir herhangi bir sınıf" için sadece kullanabilirsiniz<Serializable>
c0der

8

Joker karakterleri anlamamın bir yolu, joker karakterin jenerik referans verilen olası nesnelerin türünü "sahip" olabileceğini değil, uyumlu olduğu diğer jenerik referansların türünü belirtmediğini düşünmektir (bu kafa karıştırıcı gelebilir) ...) Bu nedenle, ilk cevap ifadesinde çok yanıltıcıdır.

Başka bir deyişle, List<? extends Serializable>bu referansı, türün bilinmeyen bir tür olduğu veya Serializable'ın bir alt sınıfı olduğu diğer Listelere atayabileceğiniz anlamına gelir. Bunu TEK LİSTE'nin Serializable alt sınıflarını tutabilmesi açısından düşünmeyin (çünkü bu yanlış anlambilimdir ve Jeneriklerin yanlış anlaşılmasına yol açar).


Bu kesinlikle yardımcı olur, ancak "kafa karıştırıcı gelebilir" yerine "kafa karıştırıcı sesler" gelir. Bir takip olarak, bu açıklamaya göre neden Matcher ile yöntem derleniyor <? extends T>?
Yishai

List <Serializable> olarak tanımlarsak, aynı şeyi yapar mı? 2. paradan bahsediyorum. yani polimorfizm bunun üstesinden gelir mi?
Supun Wijerathne

3

Bunun eski bir soru olduğunu biliyorum, ancak sınırlı joker karakterleri oldukça iyi açıkladığını düşündüğüm bir örneği paylaşmak istiyorum. java.util.Collectionsbu yöntemi sunar:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

Listemiz varsa T, Liste elbette genişleyen tür örnekleri içerebilir T. Liste Hayvanlar içeriyorsa, Liste hem Köpekleri hem de Kedileri (her iki Hayvanı) içerebilir. Köpekler "woofVolume" özelliğine sahiptir ve Kediler "meowVolume" özelliğine sahiptir. Alt sınıflarına özgü bu özelliklere göre sıralamak istesek de T, bu yöntemin bunu yapmasını nasıl bekleyebiliriz? Karşılaştırıcının bir sınırlaması, sadece bir türden ( T) sadece iki şeyi karşılaştırabilmesidir . Yani, sadece bir Comparator<T>yöntem kullanmak bu yöntemi kullanılabilir hale getirir. Ancak, bu yöntemin yaratıcısı, bir şey bir ise T, aynı zamanda üst sınıflarının bir örneği olduğunu kabul etti T. Bu nedenle, o bir Karşılaştırıcısı kullanmamızı verir Tveya herhangi üst sınıfı Tyani ? super T.


1

ya kullanırsan

Map<String, ? extends Class<? extends Serializable>> expected = null;

Evet, yukarıdaki cevabımın yönlendirdiği şey bu.
GreenieMeanie

Hayır, bu duruma en azından denediğim gibi yardımcı olmuyor.
Yishai
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.