Java'da jenerikte silme kavramı nedir?


Yanıtlar:


200

Temel olarak jeneriklerin derleyici hilesi ile Java'da uygulanma şekli. Derlenmiş jenerik kod aslında sadece kullandığı java.lang.Objecthakkında konuşmak her yerde T(veya başka tip parametresi) - ve gerçekten genel bir türü olduğunu derleyici anlatmak için bazı meta var.

Genel bir türe veya yönteme göre bazı kodlar derlediğinizde, derleyici gerçekten ne demek istediğinizi (yani tür argümanının ne Tolduğunu) hesaplar ve doğru şeyi yaptığınızı derlerken doğrular , ancak yayılan kod tekrar konuşur açısından java.lang.Object- derleyici gerektiğinde ekstra dökümler üretir. Yürütme zamanında, a List<String> ve a List<Date>tam olarak aynıdır; ekstra tip bilgisi derleyici tarafından silinmiştir .

Bunu, örneğin, yürütme zamanında tutulan C # ile karşılaştırın; kodun typeof(T)eşdeğeri gibi ifadeleri içermesine izin verir T.class- ikincisinin geçersiz olması dışında. (.NET jenerikleri ve Java jenerikleri arasında daha fazla farklılık vardır, unutmayın) Tip silme, Java jenerikleri ile uğraşırken birçok "tek" uyarı / hata mesajının kaynağıdır.

Diğer kaynaklar:


6
@ Rogerio: Hayır, nesnelerin farklı genel türleri olmaz. Alanlar türlerini biliyorum ama nesneler yok.
Jon Skeet

8
@Rogerio: Kesinlikle - yürütme sırasında yalnızca Object(zayıf yazılan bir senaryoda) olarak List<String>sunulan bir şeyin aslında a ) olup olmadığını öğrenmek son derece kolaydır . Java'da bu mümkün değil - bunun ArrayListorijinal jenerik türünün ne olduğunu değil, olduğunu öğrenebilirsiniz. Bu tür şeyler, örneğin serileştirme / serileştirme durumlarında ortaya çıkabilir. Başka bir örnek, bir kabın genel türünün örneklerini oluşturabilmesi gerektiğidir - bu türü Java'da (as Class<T>) ayrı olarak geçirmeniz gerekir .
Jon Skeet

6
Ben her zaman ya da neredeyse her zaman bir sorun olduğunu iddia etmedim - ama en azından makul sık sık benim deneyimimde bir sorun. Class<T>Sadece Java bu bilgileri tutmaz çünkü bir yapıcı (veya genel yöntem) bir parametre eklemek zorunda olduğum çeşitli yerler vardır . EnumSet.allOfÖrneğin bak - yöntemin genel tür argümanı yeterli olmalıdır; neden "normal" bir argüman belirtmem gerekiyor? Cevap: silme yazın. Bu tür şeyler API'yı kirletir. İlgi alanı dışında, .NET jeneriklerini çok mu kullandınız? (devam ediyor)
Jon Skeet

5
.NET jeneriklerini kullanmadan önce, Java jeneriklerini çeşitli şekillerde garip buldum (ve "arayan tarafından belirtilen" varyans biçimi kesinlikle avantajları olmasına rağmen, joker karakter kullanımı hala bir baş ağrısıdır) - ancak yalnızca .NET jeneriklerini kullandıktan sonra oldu bir süredir Java jenerikleriyle kaç desenin garip veya imkansız hale geldiğini gördüm. Yine Blub paradoksu. Ben .NET jenerikleri de, btw dezavantajları yok demiyorum - ne yazık ki, ifade edilemez çeşitli tip ilişkileri vardır - ama ben çok Java jenerikleri tercih ederim.
Jon Skeet

5
@Rogerio: Orada bir çok şey olabilir yansıması ile yapmak - ama bulmak eğiliminde değildir istiyorum ben şeyler olarak neredeyse sıklıkta bunları yapmak olamaz Java jenerik ile yapmak. Bir alanın tür argümanını neredeyse gerçek bir nesnenin tür argümanını bulmak istediğim kadar bulmak istemiyorum.
Jon Skeet

41

Bir yan not gibi, derleyicinin silme gerçekleştirdiğinde ne yaptığını görmek ilginç bir alıştırmadır - tüm konsepti kavramak biraz daha kolay hale getirir. Derleyiciyi, jenerikleri silip döktüğünüz java dosyalarını çıktılamak için iletebileceğiniz özel bir bayrak vardır. Bir örnek:

javac -XD-printflat -d output_dir SomeFile.java

-printflatDosyaları oluşturur derleyici devredilmiştir alır bayrağıdır. ( -XDKısmen javacaslında sadece derleme yapar çalıştırılabilir kavanoz el teslim söyler javac, ama ben kazmak ...) -d output_dirGerekli çünkü derleyici yeni .java dosyaları koymak için bir yere ihtiyacı var.

Bu, elbette, silmekten daha fazlasını yapar; derleyicinin yaptığı tüm otomatik işler burada yapılır. Örneğin, varsayılan kurucular da eklenir, yeni foreach tarzı fordöngüler düzenli fordöngülere genişletilir , vb. Otomatik olarak gerçekleşen küçük şeyleri görmek güzel.


29

Silme, kelimenin tam anlamıyla kaynak kodda bulunan tür bilgilerinin derlenmiş bayt kodundan silindiği anlamına gelir. Bunu bir kodla anlayalım.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}

Bu kodu derler ve daha sonra bir Java decompiler ile kodalarsanız, böyle bir şey alırsınız. Ayrıştırılan kodun, orijinal kaynak kodunda bulunan tür bilgisi izi içermediğine dikkat edin.

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 

.Class dosyasından tür silme sonra kodu görmek için java decompiler kullanmaya çalıştım, ancak .class dosya hala tür bilgileri var. Denedim jigawotdedim, işe yarıyor.
frank

25

Zaten çok eksiksiz Jon Skeet'in cevabını tamamlamak için, tür silme kavramının önceki Java sürümleriyle uyumluluk ihtiyacından kaynaklandığını fark etmelisiniz . .

Başlangıçta EclipseCon 2007'de sunuldu (artık mevcut değil), uyumluluk şu noktaları içeriyordu:

  • Kaynak uyumluluğu (Güzel olması ...)
  • İkili uyumluluk (Olmalıdır!)
  • Taşıma uyumluluğu
    • Mevcut programlar çalışmaya devam etmelidir
    • Varolan kitaplıklar genel türleri kullanabilmelidir
    • Olmalı!

Orijinal cevap:

Dolayısıyla:

new ArrayList<String>() => new ArrayList()

Daha büyük bir yeniden yapılanma için önermeler vardır . Dil yapılarının sadece sözdizimsel şeker değil, kavramlar olması gereken "Soyut bir kavramı gerçek olarak algıla" olduğunu anlayın.

Ayrıca checkCollection, belirtilen koleksiyonun dinamik olarak türden güvenli bir görünümünü döndüren Java 6 yönteminden de bahsetmeliyim . Yanlış türde bir eleman eklemek için yapılan herhangi bir girişim anında sonuç verir ClassCastException.

Dildeki jenerikler mekanizması derleme zamanı (statik) tip denetimi sağlar, ancak bu mekanizmayı kontrol edilmemiş dökümlerle yenmek mümkündür .

Derleyici, bu tür tüm denetlenmeyen işlemlerle ilgili uyarılar verdiğinden, bu genellikle bir sorun oluşturmaz.

Bununla birlikte, statik tip kontrolünün tek başına yeterli olmadığı zamanlar vardır, örneğin:

  • bir koleksiyon üçüncü taraf bir kitaplığa geçirildiğinde ve kitaplık kodunun yanlış türde bir öğe ekleyerek koleksiyonu bozmaması zorunludur.
  • bir program ClassCastExceptionyanlış yazılmış bir öğenin parametreli bir koleksiyona yerleştirildiğini belirten bir a ile başarısız olur . Ne yazık ki, istisna hatalı eleman yerleştirildikten sonra herhangi bir zamanda ortaya çıkabilir, bu nedenle sorunun gerçek kaynağı hakkında genellikle çok az bilgi sağlar veya hiç bilgi vermez.

Neredeyse dört yıl sonra Temmuz 2012'yi güncelleyin:

Şimdi (2012) " API Geçiş Uyumluluk Kuralları (İmza Testi) " bölümünde ayrıntılı olarak açıklanmıştır

Java programlama dili, türlerle ilgili bazı yardımcı bilgiler dışında, eski ve genel sürümlerin genellikle aynı sınıf dosyaları oluşturmasını sağlayan silme özelliğini kullanarak jenerikleri uygular. Herhangi bir istemci kodunu değiştirmeden veya yeniden derlemeden eski bir sınıf dosyasını genel bir sınıf dosyasıyla değiştirmek mümkün olduğundan ikili uyumluluk bozulmaz.

Genel olmayan eski kodla arayüz oluşturmayı kolaylaştırmak için, parametreli bir türün silinmesini bir tür olarak kullanmak da mümkündür. Böyle bir türe ham tür denir ( Java Dil Belirtimi 3 / 4.8 ). Ham türe izin vermek, kaynak kodu için geriye dönük uyumluluk da sağlar.

Buna göre, java.util.Iteratorsınıfın aşağıdaki sürümleri hem ikili hem de kaynak kodu geriye dönük olarak uyumludur:

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

2
Geriye dönük uyumluluğun tür silme olmadan gerçekleştirilebileceğini, ancak Java programcıları yeni bir koleksiyon kümesi öğrenmeden elde edilemeyeceğini unutmayın. Bu tam olarak .NET'in gittiği yoldur. Başka bir deyişle, önemli olan üçüncü mermidir. (Devamı.)
Jon Skeet

15
Kişisel olarak bunun miyop bir hata olduğunu düşünüyorum - kısa vadeli bir avantaj ve uzun vadeli bir dezavantaj sağladı.
Jon Skeet

8

Zaten tamamlanmış olan Jon Skeet cevabını tamamlıyor ...

Silme yoluyla jenerik ilaçların uygulanmasının bazı can sıkıcı sınırlamalara yol açtığı belirtilmiştir (örn. Hayır new T[42]). Ayrıca, bu şekilde bir şey yapmanın birincil nedeninin bayt kodunda geriye dönük uyumluluk olduğu da belirtilmiştir. Bu da (çoğunlukla) doğrudur. Oluşturulan bytecode -target 1.5, sadece şekersiz döküm -target 1.4'ten biraz farklıdır. Teknik olarak, (büyük hile ile) çalışma zamanında genel tür örneklemelerine erişim elde etmek bile mümkündür (bayt koduyla), bayt kodunda gerçekten bir şey olduğunu kanıtlamak .

Daha ilginç olan nokta ise (silinmemiş olan) silinmeyi kullanarak jenerik ilaçların uygulanmasının, yüksek seviyeli tip sisteminin başarabileceği şeylerde biraz daha esneklik sağlamasıdır. Bunun iyi bir örneği, Scala'nın CLR'ye karşı JVM uygulamasıdır. JVM'de, JVM'nin kendisinin jenerik tipler üzerinde herhangi bir kısıtlama getirmemesi nedeniyle daha yüksek türlerin doğrudan uygulanması mümkündür (çünkü bu "tipler" etkili bir şekilde mevcut değildir). Bu, parametre örneklemeleri hakkında çalışma zamanı bilgisine sahip olan CLR ile çelişir. Bu nedenle CLR'nin kendisi, jeneriklerin nasıl kullanılması gerektiğine dair bazı kavramlara sahip olmalı ve sistemi beklenmedik kurallarla genişletmeye çalışmalıdır. Sonuç olarak, Scala'nın CLR üzerindeki daha yüksek türleri derleyicinin kendisinde öykünen garip bir silme biçimi kullanılarak uygulanır,

Silme, çalışma zamanında yaramaz şeyler yapmak istediğinizde rahatsız edici olabilir, ancak derleyici yazarlarına en fazla esnekliği sunar. Sanırım bu, yakında hiç gitmemesinin bir parçası.


6
Rahatsızlık, yürütme zamanında "yaramaz" şeyler yapmak istediğinizde değildir. O zaman yürütme zamanında mükemmel makul şeyler yapmak istediğinizde. Aslında, silme türü, bir Liste <Dize> Listesine ve sonra da <Tarih> Listesine yalnızca uyarılarla döküm yapmak gibi çok nahoş şeyler yapmanıza izin verir.
Jon Skeet

5

Anladığım kadarıyla ( .NET adamı olarak) JVM jenerik kavramı yoktur, bu nedenle derleyici tip parametrelerini Object ile değiştirir ve sizin için tüm dökümleri gerçekleştirir.

Bu, Java jeneriklerinin sözdizimi şekerinden başka bir şey olmadığı ve referansla iletildiğinde boks / kutudan çıkarma gerektiren değer türleri için herhangi bir performans iyileştirmesi sunmadığı anlamına gelir.


3
Java jenerikleri yine de değer türlerini temsil edemez - List <int> diye bir şey yoktur. Bununla birlikte, Java'da hiç referans kodu yoktur - kesinlikle değere göre geçer (bu değer bir referans olabilir.)
Jon Skeet

2

İyi açıklamalar var. Sadece tür silme işleminin bir kod çözücü ile nasıl çalıştığını göstermek için bir örnek ekliyorum.

Orijinal sınıf,

import java.util.ArrayList;
import java.util.List;


public class S<T> {

    T obj; 

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

Kodun bayt kodundan ayrıştırılması,

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}

2

Neden Generices kullanılır?

Özetle, jenerikler sınıfları, arayüzleri ve yöntemleri tanımlarken tiplerin (sınıflar ve arayüzler) parametre olmasını sağlar. Yöntem bildirimlerinde kullanılan daha bilinen biçimsel parametreler gibi, tip parametreleri de aynı kodu farklı girişlerle yeniden kullanmanız için bir yol sağlar. Fark, biçimsel parametrelere girişlerin değerler, tip parametrelerine girişlerin ise tipler olmasıdır. jenerik kullanılan ode jenerik olmayan kod üzerinde birçok faydası vardır:

  • Derleme zamanında daha güçlü tip kontrolleri.
  • Dökümlerin ortadan kaldırılması.
  • Programcıların genel algoritmalar uygulamalarını sağlama.

Tip Silme Nedir

Derleme zamanında daha sıkı tip kontroller sağlamak ve genel programlamayı desteklemek için Java diline jenerikler tanıtıldı. Jenerikleri uygulamak için Java derleyicisi aşağıdakilere tür silme uygular:

  • Genel türlerdeki tüm tür parametrelerini sınırları ile veya tür parametreleri sınırsızsa Object ile değiştirin. Üretilen bytecode, bu nedenle, sadece sıradan sınıflar, arayüzler ve yöntemler içerir.
  • Tip güvenliğini korumak için gerekirse tip dökümleri yerleştirin.
  • Genişletilmiş jenerik tiplerde polimorfizmi korumak için köprü yöntemleri üretin.

[NB] -Köprü yöntemi nedir? Kısaca, gibi parametreli bir arayüz durumunda Comparable<T>, bu derleyici tarafından ek yöntemlerin eklenmesine neden olabilir; bu ek yöntemlere köprüler denir.

Silme Nasıl Çalışır?

Bir türün silinmesi şu şekilde tanımlanır: tüm tür parametrelerini parametrelenmiş türlerden atın ve herhangi bir tür değişkenini sınırının silinmesiyle veya herhangi bir sınırı yoksa Object ile veya varsa en soldaki sınırın silinmesiyle değiştirin. çoklu sınırlar. İşte bazı örnekler:

  • Silinmesi List<Integer>, List<String>ve List<List<String>>olduğunu List.
  • Silinmesi List<Integer>[]DİR List[].
  • ListHerhangi bir ham tip için benzer şekilde silinme kendisidir.
  • İnt'in silinmesi, her ilkel tipte olduğu gibi, kendisidir.
  • Silme işlemi, Integertip parametreleri olmayan herhangi bir tip için benzerdir.
  • Silinmesi Ttanımındaki asListIS Object, çünkü T herhangi bir bağlandığı.
  • Silinmesi Ttanımındaki maxIS Comparable, çünkü T bağlanmış olanComparable<? super T> .
  • Silinmesi Tnihai tanımı maxise Object, çünkü Tbağlı olan Objectve Comparable<T>ve biz bağlı en soldaki silinmesini alır.

Jenerik kullanırken dikkatli olmanız gerekir

Java'da iki farklı yöntem aynı imzayı kullanamaz. Jenerikler silme ile uygulandığından, iki farklı yöntemin aynı silme ile imzaları olamaz. Bir sınıf, imzaları aynı silme özelliğine sahip iki yöntemi aşırı yükleyemez ve bir sınıf aynı silme özelliğine sahip iki arabirimi uygulayamaz.

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

Bu kodun aşağıdaki gibi çalışmasını istiyoruz:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

Bununla birlikte, bu durumda, her iki yöntemin imzalarının silinmesi aynıdır:

boolean allZero(List)

Bu nedenle, derleme zamanında bir ad çakışması bildirilir. Her iki yönteme aynı adı vermek ve aşırı yükleyerek aralarında ayrım yapmaya çalışmak mümkün değildir, çünkü silindikten sonra bir yöntem çağrısını diğerinden ayırt etmek imkansızdır.

Umarım, Reader keyfine varacaksınız :)

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.