Java'nın somut jeneriklere sahip olmamasını neden umursayayım?


102

Bu, geçenlerde bir röportajda sorduğum bir soru olarak adayın Java diline eklenmesini görmek istediği bir şey olarak ortaya çıktı. Genellikle Java'nın jenerikleri somutlaştırmadığı bir acı olarak tanımlanır , ancak zorlandığında, aday bana orada olsaydı başarabileceği türden şeyleri söyleyemezdi.

Açıktır ki, Java'da ham türlere (ve güvenli olmayan kontrollerde) izin verildiği için, jenerikleri List<Integer>tersine çevirmek ve (örneğin) gerçekten Strings içeren bir sonuç elde etmek mümkündür . Tip bilgisi somutlaştırıldığında bu açıkça imkansız hale getirilebilirdi; ama bundan daha fazlası olmalı !

İnsanlar , gerçekleştirilmiş jenerikler mevcutsa, gerçekten yapmak isteyecekleri şeylerin örneklerini gönderebilirler mi? Demek istediğim, açıkçası Listçalışma zamanında a türünü alabilirsiniz - ama bununla ne yapardınız?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

DÜZENLEME : Yanıtlar temelde Classbir parametre olarak bir parametre olarak geçme ihtiyacı hakkında endişeli göründüğü için buna hızlı bir güncelleme (örneğin EnumSet.noneOf(TimeUnit.class)). Bunun mümkün olmadığı bir yerde daha fazlasını arıyordum . Örneğin:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?

Bu, çalışma zamanında genel bir tür sınıfını alabileceğiniz anlamına mı gelir? (eğer öyleyse, bir örneğim var!)
James B

Bence yeniden adlandırılabilir jeneriklere olan arzunun çoğu jenerikleri öncelikle koleksiyonlarla kullanan ve bu koleksiyonların daha çok diziler gibi davranmasını isteyen kişilerden geliyor.
kdgregory

2
Daha ilginç soru (benim için): Java'da C ++ stil jeneriklerini uygulamak için ne gerekir? Kesinlikle çalışma zamanında yapılabilir görünmektedir, ancak mevcut tüm sınıf yükleyicileri bozacaktır (çünkü findClass()parametrelendirmeyi göz ardı etmek zorunda kalacaktır, ancakdefineClass() olamazdı). Ve bildiğimiz gibi, Geriye dönük uyumluluk en önemli olan Güçler.
kdgregory

1
Aslında Java, çok kısıtlı bir şekilde somut jenerikler sağlar . Bu SO başlığında daha fazla ayrıntı sunuyorum: stackoverflow.com/questions/879855/…
Richard Gomes

JavaOne Keynote Java 9 somutlaştırmaya destekliyor demektir.
Rangi Keen

Yanıtlar:


81

Bu "ihtiyaç" ile karşılaştığım birkaç zamandan beri, nihayetinde bu yapıya indirgeniyor:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Bu, C # 'da çalışır, Tbunun bir varsayılan . Çalışma zamanı türünü bile typeof(T)alabilir ve yapıcıları ile alabilirsiniz Type.GetConstructor().

Ortak Java çözümü Class<T>, argüman olarak geçmek olacaktır .

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(Yapıcı argüman olarak iletilmesi gerekmez, çünkü bir yöntem argümanı da iyidir, yukarıdakiler sadece bir örnektir, ayrıca try-catchkısalık için ihmal edilmiştir)

Diğer tüm genel tip yapılar için, gerçek tip, biraz yansıma yardımı ile kolayca çözülebilir. Aşağıdaki Soru-Cevap, kullanım durumlarını ve olasılıkları göstermektedir:


5
Bu (örneğin) C #? T'nin varsayılan bir kurucuya sahip olduğunu nasıl anlarsınız?
Thomas Jung

4
Evet o yapar. Ve bu sadece basit bir örnek (javabeans ile çalıştığımızı varsayalım). Bütün mesele Java ile size sırasında sınıf alınamıyor olmasıdır çalışma zamanı tarafından T.classya T.getClass(), bu yüzden tüm alanları, yapıcı ve yöntemlerine erişmek olabilir. İnşaatı da imkansız hale getiriyor.
BalusC

3
Bu bana "büyük sorun" olarak gerçekten zayıf görünüyor, özellikle de bu yalnızca yapıcılar / parametreler vb. Etrafındaki bazı çok kırılgan yansımalarla bağlantılı olarak faydalı olacağı için
oxbow_lakes

33
Türü şu şekilde bildirmeniz koşuluyla C # ile derlenir: public class Foo <T> burada T: new (). Bu, geçerli T türlerini parametresiz bir kurucu içerenlerle sınırlayacaktır.
Martin Harris

3
@Colin Aslında java'ya belirli bir türden birden fazla sınıfı veya arabirimi genişletmek için gerekli olduğunu söyleyebilirsiniz. Docs.oracle.com/javase/tutorial/java/generics/bounded.html adresindeki "Çoklu Sınırlar" bölümüne bakın . (Elbette, nesnenin, şablon uzmanlığı kullanılarak C ++ 'da bir şekilde uygulanabilecek bir arayüze soyutlanabilen yöntemleri paylaşmayan belirli bir kümeden biri olacağını söyleyemezsiniz.)
JAB

99

Beni en çok ısıran şey, birden çok jenerik tipte birden çok gönderimden yararlanamamaktır. Aşağıdakiler mümkün değildir ve bunun en iyi çözüm olacağı birçok durum vardır:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }

5
Bunu yapabilmek için şeyleştirmeye kesinlikle gerek yoktur. Yöntem seçimi, derleme zamanı tip bilgisi mevcut olduğunda derleme zamanında yapılır.
Tom Hawtin - tackline

26
@Tom: Bu, tür silme nedeniyle bile derlenmez. Her ikisi de olarak derlenir public void my_method(List input) {}. Bununla birlikte, aynı isme sahip olmayacakları için bu ihtiyaca hiç rastlamadım. Aynı isme sahiplerse, public <T extends Object> void my_method(List<T> input) {}daha iyi bir fikir olup olmadığını sorardım.
BalusC

1
Hm, tamamen aynı sayıda parametre ile aşırı yüklemeyi önleme eğilimindeydim myStringsMethod(List<String> input)ve myIntegersMethod(List<Integer> input)Java'da böyle bir durum için aşırı yükleme mümkün olsa bile , buna benzer bir şeyi tercih ederim .
Fabian Steeg

4
@Fabian: Bu, ayrı bir koda sahip olmanız <algorithm>ve C ++ ' dan elde ettiğiniz türden avantajları önlemeniz gerektiği anlamına gelir .
David Thornley

Kafam karıştı, bu aslında ikinci noktamla aynı, ancak bunun üzerine 2 eksi oy mu aldım? Beni aydınlatmak isteyen var mı?
rsp

34

Tip güvenliği akla geliyor. Parametreleştirilmiş bir türe geri dönüş, yeniden biçimlendirilmiş jenerikler olmadan her zaman güvenli olmayacaktır:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Ayrıca, soyutlamalar daha az sızıntı yapacaktır - en azından tür parametreleri hakkındaki çalışma zamanı bilgileriyle ilgilenenler. Bugün, genel parametrelerden birinin türü hakkında herhangi bir çalışma zamanı bilgisine ihtiyacınız varsa, onu da iletmelisiniz Class. Bu şekilde, harici arayüzünüz uygulamanıza bağlıdır (parametreleriniz için RTTI kullanıp kullanmamanız).


1
Evet - Bunu aşmanın bir yolu var ParametrizedList, çünkü kaynak koleksiyonu kontrol türlerindeki verileri kopyalayan bir tane oluşturuyorum . Biraz benziyor Collections.checkedListama başlamak için bir koleksiyonla tohumlanabilir.
oxbow_lakes

@tackline - birkaç soyutlama daha az sızdırır. Uygulamanızda meta verileri yazmak için erişime ihtiyacınız varsa, dış arabirim size söyleyecektir çünkü istemcilerin size bir sınıf nesnesi göndermesi gerekir.
gustafc

... şeyleştirilmiş jeneriklerle, harici arabirimi değiştirmeden T.class.getAnnotation(MyAnnotation.class)( Tgenel bir tür nerede) gibi şeyler ekleyebileceğiniz anlamına gelir .
gustafc

@gustafc: C ++ şablonlarının size tam bir tür güvenliği sağladığını düşünüyorsanız, şunu okuyun: kdgregory.com/index.php?page=java.generics.cpp
kdgregory

@kdgregory: C ++ 'ın% 100 tip güvenli olduğunu hiç söylemedim - sadece bu silme işlemi tip güvenliğine zarar verir. Sizin de dediğiniz gibi, "C ++, ortaya çıkıyor, C tarzı işaretçi dökümü olarak bilinen kendi tür silme biçimine sahip." Ancak Java yalnızca dinamik yayınlar yapar (yeniden yorumlamaz), bu nedenle şeyleştirme, tüm bunları tür sistemine yerleştirir.
gustafc

26

Kodunuzda genel diziler yaratabileceksiniz.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}

Object'in nesi var? Bir nesne dizisi zaten referans dizisidir. Nesne verileri yığının üzerinde oturmuyor - hepsi yığında.
Ran Biron

19
Güvenlik yazın. İstediğimi Object [] 'e koyabilirim, ancak yalnızca String [] içindeki String'leri koyabilirim.
Turnor

3
Ran: Alay etmeden: Java yerine bir betik dili kullanmayı sevebilirsin, o zaman her yerde türlenmemiş değişkenler esnekliğine sahip olursun!
uçan koyun

2
Diziler, her iki dilde de ortak değişkendir (ve bu nedenle diziler güvenli değildir). String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Her iki dilde de iyi derler. Notsofine çalıştırır.
Martijn

16

Bu eski bir soru, bir sürü cevap var ama bence mevcut cevaplar uygun değil.

"somutlaştırılmış" sadece gerçek anlamına gelir ve genellikle tür silme işleminin tersi anlamına gelir.

Java Generics ile ilgili en büyük sorun:

  • Bu korkunç boks gereksinimi ve ilkellerle referans türleri arasındaki kopukluk. Bu, doğrudan şeyleştirme veya tür silme ile ilgili değildir. C # / Scala bunu düzeltin.
  • Kendi kendine tip yok. JavaFX 8, bu nedenle "kurucuları" kaldırmak zorunda kaldı. Tip silme ile kesinlikle hiçbir ilgisi yok. Scala bunu düzeltir, C # konusunda emin değil.
  • Bildirim tarafı tür farkı yok. C # 4.0 / Scala buna sahip. Tip silme ile kesinlikle hiçbir ilgisi yok.
  • Aşırı yüklenemez void method(List<A> l)ve method(List<B> l). Bu, tür silme nedeniyledir ancak son derece önemsizdir.
  • Çalışma zamanı türü yansıması için destek yok. Bu, tür silme işleminin kalbidir. Derleme zamanında program mantığınızın çoğunu doğrulayan ve kanıtlayan süper gelişmiş derleyicilerden hoşlanıyorsanız, mümkün olduğunca az yansıma kullanmalısınız ve bu tür silme türü sizi rahatsız etmemelidir. Daha düzensiz, betik, dinamik tip programlamayı seviyorsanız ve mantığınızın olabildiğince doğru olduğunu kanıtlayan bir derleyiciyi çok fazla önemsemiyorsanız, o zaman daha iyi yansıtma ve düzeltme tipi silme işlemi yapmak önemlidir.

1
Çoğunlukla serileştirme vakalarında zor buluyorum. Sık sık, sınıf türlerinin serileştirilen genel şeylerin farkına varabilmeyi istersiniz, ancak tür silme nedeniyle kısa süre durursunuz. Böyle bir şey yapmayı zorlaştırıyordeserialize(thingy, List<Integer>.class)
Cogman

3
Bunun en iyi cevap olduğunu düşünüyorum. Özellikle hangi sorunların temelde tür silme işleminden kaynaklandığını ve yalnızca Java dili tasarım sorunlarının neler olduğunu açıklayan bölümler. İnsanlara silme-sızlamaya başladığımı ilk söylediğim şey, Scala ve Haskell'in de tür silme yoluyla çalıştıklarıdır.
aemxdp

13

Serileştirme, ödüllendirme ile daha kolay olacaktır. İstediğimiz şey

deserialize(thingy, List<Integer>.class);

Yapmamız gereken şey

deserialize(thing, new TypeReference<List<Integer>>(){});

çirkin görünüyor ve tuhaf çalışıyor.

Şöyle bir şey söylemenin gerçekten yararlı olacağı durumlar da var

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Bu şeyler çok sık ısırmaz, ancak olduklarında ısırırlar.


Bu. Bunun bin katı. Java'da seriyi kaldırma kodunu her yazmaya çalıştığımda, günü başka bir şeyde çalışmadığım için ağlayarak geçiriyorum
Temel

11

Java Geneircs'e maruz kalmam oldukça sınırlıdır ve diğer yanıtların daha önce bahsettiği noktalardan ayrı olarak, Maurice Naftalin ve Philip Walder'ın Java Generics and Collections kitabında , somutlaştırılmış jeneriklerin yararlı olduğu bir senaryo açıklanmıştır .

Türler yeniden yapılandırılamadığından, Parametreli istisnalara sahip olmak mümkün değildir.

Örneğin aşağıdaki formun beyanı geçerli değildir.

class ParametericException<T> extends Exception // compile error

Bunun nedeni, catch cümlesinin, atılan istisnanın belirli bir türle eşleşip eşleşmediğini kontrol etmesidir. Bu kontrol, örnek testi tarafından gerçekleştirilen kontrol ile aynıdır ve tip yeniden tanımlanamayacağı için yukarıdaki ifade şekli geçersizdir.

Yukarıdaki kod geçerli olsaydı, aşağıdaki şekilde istisna işleme mümkün olabilirdi:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

Kitap ayrıca, Java jeneriklerinin C ++ şablonlarının tanımlanma (genişletme) şekline benzer şekilde tanımlanması durumunda, optimizasyon için daha fazla fırsat sunduğundan daha verimli uygulamaya yol açabileceğinden bahsediyor. Ancak bundan daha fazla açıklama sunmuyor, bu yüzden bilgili kişilerden gelecek herhangi bir açıklama (işaretler) yardımcı olacaktır.


1
Bu geçerli bir nokta, ancak bu kadar parametreleştirilmiş bir istisna sınıfının neden yararlı olacağından emin değilim. Cevabınızı, bunun ne zaman yararlı olabileceğine dair kısa bir örnek içerecek şekilde değiştirebilir misiniz?
oxbow_lakes

@oxbow_lakes: Üzgünüm Java Generics hakkındaki bilgim oldukça sınırlıdır ve onu geliştirmeye çalışıyorum. Şimdi, parametreleştirilmiş istisnanın yararlı olabileceği herhangi bir örnek düşünemiyorum. Bunun hakkında düşünmeye çalışacak. Teşekkürler.
sateesh

İstisna türlerinin çoklu kalıtımının yerine geçebilir.
meriton

Performans iyileştirmesi şu anda bir tür parametresinin Object'ten miras alması gerektiğidir ve ilkel türlerin kutulanmasını gerektirir, bu da bir yürütme ve bellek ek yükü getirir.
meriton

8

Diziler, somutlaştırılmış olsalardı muhtemelen jeneriklerle çok daha iyi oynayacaktı.


2
Elbette, ama yine de sorunları olur. List<String>bir değil List<Object>.
Tom Hawtin - tackline

Katılıyorum - ancak yalnızca ilkeller için (Tamsayı, Uzun vb.). "Normal" Nesne için bu aynıdır. İlkeller parametreleştirilmiş bir tür olamayacağından (çok daha ciddi bir sorun, en azından IMHO), bunu gerçek bir acı olarak görmüyorum.
Ran Biron

3
Dizilerle ilgili sorun onların kovaryansıdır, şeyleştirme ile hiçbir ilgisi yoktur.
Recurse

5

Yineleyici olarak bir jdbc sonuç kümesini sunan bir sarmalayıcım var (bu, bağımlılık enjeksiyonu yoluyla veritabanı kaynaklı işlemleri çok daha kolay bir şekilde test edebileceğim anlamına geliyor).

API Iterator<T>, T'nin yalnızca yapıcıdaki dizeler kullanılarak oluşturulabilen bir tür olduğu gibi görünür . Yineleyici daha sonra sql sorgusundan döndürülen dizelere bakar ve ardından bunu T tipi bir kurucu ile eşleştirmeye çalışır.

Mevcut jeneriklerin uygulanma biçiminde, sonuç kümemden oluşturacağım nesnelerin sınıfını da geçirmem gerekiyor. Doğru anlarsam, jenerikler somutlaştırılmış olsaydı, yapıcılarını almak için T.getClass () 'ı çağırabilirdim ve sonra çok daha düzgün olacak Class.newInstance () sonucunu kullanmak zorunda kalmazdım.

Temel olarak, API'leri yazmayı (sadece bir uygulama yazmanın aksine) daha kolay hale getirdiğini düşünüyorum çünkü nesnelerden çok daha fazlasını çıkarabilirsiniz ve bu nedenle daha az yapılandırma gerekli olacaktır ... reams of config yerine spring veya xstream gibi şeylerde kullanıldığını gördüm.


Ama sınıfta geçmek benim için daha güvenli görünüyor. Her durumda, veritabanı sorgularından yansıtıcı bir şekilde örnek oluşturmak, yine de yeniden düzenleme gibi değişikliklere (hem kodunuzda hem de veritabanında) son derece kırılgandır. Sanırım sınıfı temin etmenin mümkün olmadığı şeyler arıyordum
oxbow_lakes

5

İlkel (değer) türler için kutudan kaçınmak güzel bir şey olabilir. Bu bir şekilde diğerlerinin ortaya çıkardığı dizi şikayetiyle ilgilidir ve bellek kullanımının kısıtlı olduğu durumlarda aslında önemli bir fark yaratabilir.

Parametreli tip üzerinde düşünebilmenin önemli olduğu bir çerçeve yazarken çeşitli problem türleri de vardır. Elbette bu, çalışma zamanında bir sınıf nesnesinin etrafından geçirilerek çözülebilir, ancak bu API'yi gizler ve çerçeve kullanıcısına ek bir yük getirir.


2
Bu, esasen new T[]T'nin ilkel tipte olduğu yerde yapabilmekle ilgilidir !
oxbow_lakes

2

Olağanüstü bir şey elde edeceğinizden değil. Anlaması daha kolay olacak. Tür silme, yeni başlayanlar için zor bir zaman gibi görünür ve sonuçta derleyicinin çalışma şeklini anlamasını gerektirir.

Bence jenerik sade yani, ekstra gereksiz döküm kazandırıyor.


Java'da jenerikler kesinlikle budur . C # ve diğer dillerde bunlar güçlü bir araçtır
Temel

1

Buradaki tüm cevapların gözden kaçırdığı ve benim için sürekli baş ağrısına neden olan bir şey, türler silindiğinden, genel bir arayüzü iki kez miras alamazsınız. İnce taneli arayüzler yapmak istediğinizde bu bir problem olabilir.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

0

Bugün beni yakalayan bir şey var: şeyleştirmeden, genel öğelerin bir değişken listesini kabul eden bir yöntem yazarsanız ... arayanlar, türlerinin güvenli olduğunu DÜŞÜNEBİLİR, ancak yanlışlıkla herhangi bir eski pisliğe girip yönteminizi havaya uçurabilir.

Olması olası görünmüyor mu? ... Elbette, ta ki ... siz veri türünüz olarak Class'ı kullanana kadar. Bu noktada, arayan kişi size mutlu bir şekilde çok sayıda Sınıf nesnesi gönderecektir, ancak basit bir yazım hatası size T'ye uymayan Sınıf nesneleri ve felaket grevleri gönderecektir.

(NB: Burada bir hata yapmış olabilirim, ancak "jenerik değişkenler" etrafında gezinirken, yukarıdakiler tam da beklediğiniz gibi görünüyor. Bunu pratik bir sorun yapan şey, sanırım Sınıf kullanımıdır - arayanlar görünüyor daha az dikkatli olmak için :()


Örneğin, haritalarda anahtar olarak Sınıf nesnelerini kullanan bir paradigma kullanıyorum (basit bir haritadan daha karmaşık - ama kavramsal olarak olan budur).

Örneğin, Java Generics'te harika çalışıyor (önemsiz örnek):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

Örneğin, Java Generics'te yeniden biçimlendirme olmadan, bu HERHANGİ "Sınıf" nesnesini kabul eder. Ve bu, önceki kodun yalnızca küçük bir uzantısı:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Yukarıdaki yöntemler tek bir projede binlerce kez yazılmalıdır - bu nedenle insan hatası olasılığı yüksek olur. Hataları ayıklamak "eğlenceli değil" demek. Şu anda bir alternatif bulmaya çalışıyorum, ancak fazla umut vermeyin.

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.