जावा जेनेरिक प्रकार मिटाना: कब और क्या होता है?


238

मैंने ओरेकल की वेबसाइट पर जावा के प्रकार के क्षरण के बारे में पढ़ा ।

टाइप इरेज़र कब होता है? संकलन के समय या रनटाइम पर? जब कक्षा भरी हुई है? जब क्लास को तत्काल किया जाता है?

बहुत सारी साइटें (ऊपर उल्लिखित आधिकारिक ट्यूटोरियल सहित) कहती हैं कि संकलन समय पर प्रकार का क्षरण होता है। यदि संकलित समय पर प्रकार की जानकारी पूरी तरह से हटा दी जाती है, तो जेडीके एक प्रकार की जानकारी या गलत प्रकार की जानकारी के साथ उपयोग किए जाने वाले तरीके को कैसे अनुकूलता देता है?

निम्न उदाहरण पर विचार करें: कहो वर्ग Aएक विधि है, empty(Box<? extends Number> b)। हम संकलित करते हैं A.javaऔर वर्ग फ़ाइल प्राप्त करते हैं A.class

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

अब हम एक और वर्ग बनाते हैं Bजो emptyगैर-पैरामीटर किए गए तर्क (कच्चे प्रकार) के साथ विधि को लागू करता है empty(new Box()):। अगर हम क्लासपाथ में संकलित B.javaकरते हैं A.class, तो जेवैक एक चेतावनी जुटाने के लिए पर्याप्त स्मार्ट है। तो A.class इसमें कुछ प्रकार की जानकारी संग्रहीत है।

public class B {
    public static void invoke() {
        // java: unchecked method invocation:
        //  method empty in class A is applied to given types
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        // java: unchecked conversion
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        A.empty(new Box());
    }
}

मेरा अनुमान है कि जब वर्ग लोड किया जाता है तो प्रकार का क्षरण होता है, लेकिन यह सिर्फ एक अनुमान है। तो यह कब होता है?


2
इस प्रश्न का एक और "सामान्य" संस्करण: stackoverflow.com/questions/313584/…
Ciro Santilli 病

@afryingpan: मेरे उत्तर में उल्लिखित लेख में विस्तार से बताया गया है कि कैसे और कब प्रकार का क्षरण होता है। यह भी बताता है कि टाइप जानकारी कब रखी जाती है। दूसरे शब्दों में: व्यापक मान्यता के विपरीत, जावा में संशोधित जेनरिक उपलब्ध है। देखें: rgomes.info/using-typetokens-to-retrieve-generic-parameters
रिचर्ड गोम्स

जवाबों:


240

जेनेरिक के उपयोग पर टाइप इरेज़र लागू होता है । क्लास फ़ाइल में निश्चित रूप से मेटाडेटा है, यह कहने के लिए कि कोई विधि / प्रकार है या नहीं सामान्य है और क्या अड़चन हैं आदि। लेकिन जब जेनरिक का उपयोग किया जाता है , तो वे संकलन-समय जाँच और निष्पादन-समय में बदल जाते हैं। तो यह कोड:

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

में संकलित है

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

निष्पादन समय पर यह पता लगाने का कोई तरीका नहीं है कि T=Stringसूची ऑब्जेक्ट के लिए - वह जानकारी चली गई है।

... लेकिन List<T>इंटरफ़ेस अभी भी खुद को सामान्य होने के रूप में विज्ञापित करता है।

संपादित करें: बस स्पष्ट करने के लिए, कंपाइलर चर के बारे में जानकारी को बनाए रखता है List<String>- लेकिन आप अभी भी यह नहीं जान सकते कि T=Stringसूची के लिए क्या है।


6
नहीं, यहां तक ​​कि एक सामान्य प्रकार के उपयोग में भी मेटाडेटा रनटाइम पर उपलब्ध हो सकता है। एक स्थानीय चर प्रतिबिंब के माध्यम से सुलभ नहीं है, लेकिन "सूची <स्ट्रिंग> एल" के रूप में घोषित विधि पैरामीटर के लिए, रनटाइम पर टाइप मेटाडेटा होगा, जो प्रतिबिंब एपीआई के माध्यम से उपलब्ध है। हां, "टाइप इरेज़र" उतना सरल नहीं है जितना कि बहुत से लोग सोचते हैं ...
Rogério

4
@Rogerio: जैसा कि मैंने आपकी टिप्पणी का उत्तर दिया है, मेरा मानना ​​है कि आप एक चर के प्रकार को प्राप्त करने और किसी वस्तु के प्रकार को प्राप्त करने में सक्षम होने के बीच भ्रमित हो रहे हैं । ऑब्जेक्ट अपने प्रकार के तर्क को नहीं जानता है, भले ही फ़ील्ड करता है।
जॉन स्कीट

बेशक, सिर्फ वस्तु को देखकर आप यह नहीं जान सकते हैं कि यह एक सूची <स्ट्रिंग> है। लेकिन वस्तुएं कहीं से भी प्रकट नहीं होती हैं। उन्हें स्थानीय रूप से बनाया जाता है, एक विधि आह्वान तर्क के रूप में पारित किया जाता है, एक विधि कॉल से वापसी मान के रूप में लौटा जाता है, या किसी वस्तु के क्षेत्र से पढ़ा जाता है ... इन सभी मामलों में आप रनटाइम पर जान सकते हैं कि सामान्य प्रकार क्या है, या तो संक्षेप में या जावा प्रतिबिंब एपीआई का उपयोग करके।
रोजेरियो

13
@Rogerio: आप कैसे जानते हैं कि वस्तु कहां से आई है? यदि आपके पास टाइप का कोई पैरामीटर है तो आप List<? extends InputStream>कैसे जान सकते हैं कि यह किस प्रकार का था जब इसे बनाया गया था? यहां तक ​​कि अगर आप यह पता लगा सकते हैं कि संदर्भ किस क्षेत्र में संग्रहीत किया गया है, तो आपको क्यों होना चाहिए? आपको निष्पादन के समय ऑब्जेक्ट के बारे में बाकी जानकारी प्राप्त करने में सक्षम होना चाहिए, लेकिन इसके सामान्य प्रकार के तर्क नहीं? आपको लगता है कि यह छोटी सी बात है जो डेवलपर्स को वास्तव में प्रभावित नहीं करती है, प्रकार को मिटाने की कोशिश कर रही है - जबकि मुझे लगता है कि यह कुछ मामलों में बहुत महत्वपूर्ण समस्या है।
जॉन स्कीट

लेकिन टाइप इरेज़र एक छोटी सी चीज़ है जो वास्तव में डेवलपर्स को प्रभावित नहीं करती है! बेशक, मैं दूसरों के लिए नहीं बोल सकता, लेकिन मेरे अनुभव में यह कभी बड़ी बात नहीं थी। मैं वास्तव में अपने जावा मॉकिंग एपीआई (जेमॉकिट) के डिजाइन में रनटाइम प्रकार की जानकारी का लाभ उठाता हूं; विडंबना यह है कि .NET mocking API को C # में उपलब्ध सामान्य प्रकार की प्रणाली का कम लाभ लगता है।
रोजेरियो

99

संकलन समय पर जेनरिक को समझने के लिए संकलक जिम्मेदार है। कंपाइलर जेनेरिक कक्षाओं की इस "समझ" को दूर फेंकने के लिए भी जिम्मेदार है, एक प्रक्रिया में हम टाइप इरैचर कहते हैं । सभी संकलन समय पर होता है।

नोट: जावा डेवलपर्स के बहुमत के विश्वासों के विपरीत, बहुत सीमित तरीके से होने के बावजूद, संकलन-समय प्रकार की जानकारी रखना और रनटाइम पर इस जानकारी को पुनर्प्राप्त करना संभव है। दूसरे शब्दों में: जावा बहुत सीमित तरीके से संशोधित जेनरिक प्रदान करता है

प्रकार के क्षरण के संबंध में

ध्यान दें कि, संकलन-समय पर, कंपाइलर के पास पूर्ण प्रकार की जानकारी उपलब्ध होती है, लेकिन बाइट कोड उत्पन्न होने पर यह जानकारी सामान्य रूप से हटा दी जाती है, इस प्रक्रिया को टाइप इरेज़र । संगतता मुद्दों के कारण यह इस तरह से किया जाता है: भाषा डिजाइनरों का इरादा प्लेटफ़ॉर्म के संस्करणों के बीच पूर्ण स्रोत कोड संगतता और पूर्ण बाइट कोड संगतता प्रदान कर रहा था। यदि इसे अलग तरीके से लागू किया गया था, तो आपको प्लेटफ़ॉर्म के नए संस्करणों में माइग्रेट करते समय अपने विरासत अनुप्रयोगों को फिर से प्राप्त करना होगा। जिस तरह से यह किया गया था, सभी विधि हस्ताक्षर संरक्षित हैं (स्रोत कोड संगतता) और आपको किसी भी चीज़ (बाइनरी संगतता) को फिर से इकट्ठा करने की आवश्यकता नहीं है।

जावा में संशोधित जेनरिक के बारे में

यदि आपको संकलन-समय प्रकार की जानकारी रखने की आवश्यकता है, तो आपको अनाम कक्षाएं नियोजित करने की आवश्यकता है। मुद्दा यह है: अनाम कक्षाओं के बहुत ही विशेष मामले में, रनटाइम पर पूर्ण संकलन-समय की जानकारी प्राप्त करना संभव है, जो दूसरे शब्दों में अर्थ है: पुनरीक्षित जेनरिक। इसका मतलब यह है कि संकलक प्रकार की जानकारी को फेंक नहीं देता है जब अनाम कक्षाएं शामिल होती हैं; यह जानकारी उत्पन्न बाइनरी कोड में रखी गई है और रनटाइम सिस्टम आपको इस जानकारी को पुनः प्राप्त करने की अनुमति देता है।

मैंने इस विषय पर एक लेख लिखा है:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

ऊपर दिए गए लेख में वर्णित तकनीक के बारे में एक नोट यह है कि तकनीक डेवलपर्स के बहुमत के लिए अस्पष्ट है। इसके बावजूद यह काम करता है और अच्छी तरह से काम करता है, अधिकांश डेवलपर्स तकनीक के साथ भ्रमित या असहज महसूस करते हैं। यदि आपके पास जनता के लिए अपना कोड जारी करने के लिए एक साझा कोड आधार या योजना है, तो मैं उपरोक्त तकनीक की सिफारिश नहीं करता हूं। दूसरी ओर, यदि आप अपने कोड के एकमात्र उपयोगकर्ता हैं, तो आप इस तकनीक को देने वाली शक्ति का लाभ उठा सकते हैं।

नमूना कोड

ऊपर के लेख में नमूना कोड के लिंक हैं।


1
@ will824: मैंने बड़े पैमाने पर उत्तर में सुधार किया है और मैंने कुछ परीक्षण मामलों के लिए लिंक जोड़ा है। चीयर्स :)
रिचर्ड गम्स

1
वास्तव में, वे द्विआधारी और स्रोत संगतता दोनों को बनाए नहीं रखते थे: oracle.com/technetwork/java/javase/compatibility-137462.html मैं उनके इरादे के बारे में और कहां पढ़ सकता हूं? डॉक्स का कहना है कि यह टाइप इरेज़र का उपयोग करता है, लेकिन ऐसा क्यों नहीं कहता।
Dzmitry Lazerka

@ रीचर्ड वास्तव में, उत्कृष्ट लेख! आप यह भी जोड़ सकते हैं कि स्थानीय कक्षाएं भी काम करती हैं और दोनों मामलों में (अनाम और स्थानीय कक्षाएं), वांछित प्रकार के तर्क के बारे में जानकारी केवल प्रत्यक्ष पहुंच के मामले में रखी जाती है new Box<String>() {};, अप्रत्यक्ष पहुंच के मामले में नहीं void foo(T) {...new Box<T>() {};...}क्योंकि संकलक टाइप जानकारी नहीं रखता है संलग्नक विधि घोषणा।
यान-गेल गुहेन्यूक

मैंने अपने लेख की एक टूटी हुई कड़ी तय कर दी है। मैं अपने जीवन को धीरे-धीरे समाप्त कर रहा हूं और अपने डेटा को पुनः प्राप्त कर रहा हूं। :-)
रिचर्ड गोम्स

33

यदि आपके पास एक फ़ील्ड है जो एक सामान्य प्रकार है, तो इसके प्रकार के मापदंडों को कक्षा में संकलित किया जाता है।

यदि आपके पास कोई ऐसा तरीका है जो सामान्य प्रकार लेता है या रिटर्न करता है, तो उन प्रकार के मापदंडों को कक्षा में संकलित किया जाता है।

यह जानकारी वह है जो संकलक आपको यह बताने के लिए उपयोग करता है कि आप विधि Box<String>को पास नहीं कर सकते empty(Box<T extends Number>)

एपीआई जटिल है, लेकिन आप प्रतिबिंब एपीआई के माध्यम से इस प्रकार की जानकारी का निरीक्षण कर सकते हैं getGenericParameterTypes, जैसे कि getGenericReturnType, और, और खेतों के लिए getGenericType

यदि आपके पास एक ऐसा कोड है जो एक सामान्य प्रकार का उपयोग करता है, तो संकलक प्रकारों की जांच करने के लिए आवश्यकतानुसार (कॉलर में) कास्ट सम्मिलित करता है। सामान्य वस्तुएं स्वयं कच्चे प्रकार की होती हैं; पैरामीटर प्रकार "मिटा" है। इसलिए, जब आप एक बनाते हैं, तो ऑब्जेक्ट में वर्ग के new Box<Integer>()बारे में कोई जानकारी नहीं होती है ।IntegerBox

एंजेलिका लैंगर के अक्सर पूछे जाने वाले प्रश्न जावा जेनरिक के लिए सबसे अच्छा संदर्भ है।


2
वास्तव में, यह औपचारिक सामान्य प्रकार के क्षेत्र और विधियाँ हैं जिन्हें कक्षा में संकलित किया जाता है, अर्थात, "T"। वास्तविक प्रकार के सामान्य प्रकार को प्राप्त करने के लिए , आपको "अनाम-क्लास ट्रिक" का उपयोग करना होगा ।
यान-गेल गुहेन्यूक

13

जावा लैंग्वेज में जेनरिक इस विषय पर एक बहुत अच्छा मार्गदर्शक है।

जावा कंपाइलर द्वारा जेनरिक को इरेज़र नामक फ्रंट-एंड रूपांतरण के रूप में कार्यान्वित किया जाता है। आप (लगभग) इसे स्रोत-से-स्रोत अनुवाद के रूप में सोच सकते हैं, जिससे सामान्य संस्करण loophole()को गैर-सामान्य संस्करण में बदल दिया जाता है।

तो, यह संकलन समय पर है। JVM को कभी पता नहीं चलेगा कि ArrayListआपने कौन सा प्रयोग किया।

मैं श्री स्कीट के उत्तर की भी सिफारिश करूंगा कि जावा में जेनेरिक में विलोपन की अवधारणा क्या है?


6

संकलन समय पर प्रकार का क्षरण होता है। किस प्रकार के क्षरण का मतलब है कि यह सामान्य प्रकार के बारे में भूल जाएगा, हर प्रकार के बारे में नहीं। इसके अलावा, अभी भी सामान्य प्रकार के बारे में मेटाडेटा होगा। उदाहरण के लिए

Box<String> b = new Box<String>();
String x = b.getDefault();

में परिवर्तित हो जाता है

Box b = new Box();
String x = (String) b.getDefault();

संकलन के समय। आपको चेतावनी नहीं मिल सकती है क्योंकि संकलक को पता है कि किस प्रकार का सामान्य है, लेकिन इसके विपरीत, क्योंकि यह पर्याप्त नहीं जानता है इसलिए यह प्रकार की सुरक्षा की गारंटी नहीं दे सकता है।

इसके अतिरिक्त, कंपाइलर एक मेथड कॉल पर पैरामीटर्स के बारे में जानकारी टाइप करता रहता है, जिसे आप रिफ्लेक्शन के माध्यम से प्राप्त कर सकते हैं।

यह मार्गदर्शिका सबसे अच्छा विषय है।


6

"टाइप इरेज़र" शब्द वास्तव में जावा की जेनेरिक की समस्या का सही वर्णन नहीं है। टाइप इरेज़र एक खराब चीज़ नहीं है, वास्तव में यह प्रदर्शन के लिए बहुत आवश्यक है और अक्सर इसका इस्तेमाल C ++, Haskell, D जैसी कई भाषाओं में किया जाता है।

इससे पहले कि आप घृणा करें, कृपया विकी से टाइप इरेज़र की सही परिभाषा को याद करें

टाइप इरेज़र क्या है?

टाइप एरेस्सर लोड-टाइम प्रक्रिया को संदर्भित करता है जिसके द्वारा एक कार्यक्रम से स्पष्ट प्रकार एनोटेशन को हटा दिया जाता है, इससे पहले कि इसे रन-टाइम के दौरान निष्पादित किया जाता है

टाइप इरेज़र का अर्थ है डिज़ाइन टाइम पर बनाए गए टाइप टैग्स को फेंकना या कंपाइल टाइम पर इनफ़ॉर्म टाइप टैग्स जैसे कि बाइनरी कोड में संकलित प्रोग्राम में किसी भी प्रकार के टैग्स का न होना। और यह बाइनरी कोड को संकलित करने वाली प्रत्येक प्रोग्रामिंग भाषा के लिए कुछ मामलों को छोड़कर जहां आपको रनटाइम टैग की आवश्यकता होती है। उदाहरण के लिए इन अपवादों में सभी अस्तित्वगत प्रकार (जावा संदर्भ प्रकार जो उप-प्रकार हैं, कई भाषाओं में किसी भी प्रकार, संघ प्रकार) शामिल हैं। प्रकार के क्षरण का कारण यह है कि कार्यक्रम एक ऐसी भाषा में परिवर्तित हो जाते हैं जो कुछ प्रकार की uni- टाइप (द्विआधारी भाषा केवल बिट्स की अनुमति देती है) के रूप में प्रकार केवल अमूर्त हैं और इसके मूल्यों और उन्हें संभालने के लिए उपयुक्त शब्दार्थ के लिए एक संरचना का दावा करते हैं।

तो यह बदले में, एक सामान्य प्राकृतिक चीज है।

जावा की समस्या अलग है और इसका कारण यह है कि यह कैसे पुनरावृत्ति करता है।

जावा के बारे में अक्सर दिए गए बयानों में संशोधन नहीं किया गया है, यह भी गलत है।

जावा पुनरावृत्ति करता है, लेकिन पिछड़े संगतता के कारण गलत तरीके से।

क्या है संशोधन?

हमारे से विकी से

सुधार वह प्रक्रिया है जिसके द्वारा किसी कंप्यूटर प्रोग्राम के बारे में एक सार विचार एक स्पष्ट डेटा मॉडल या प्रोग्रामिंग भाषा में बनाई गई अन्य ऑब्जेक्ट में बदल जाता है।

विशेषज्ञता से अभिप्राय अभिप्रेत है कुछ अमूर्त (पैरामीट्रिक टाइप) को कुछ ठोस (ठोस प्रकार) में परिवर्तित करना।

हम एक सरल उदाहरण द्वारा इसका वर्णन करते हैं:

परिभाषा के साथ एक ArrayList:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

एक अमूर्त है, विस्तार से एक प्रकार का कंस्ट्रक्टर, जिसे कंक्रीट प्रकार के साथ विशेषीकृत होने पर "पुनःप्राप्त" किया जाता है, कहते हैं इंटेगर:

ArrayList<Integer>
{
    Integer[] elems;
}

कहाँ पे ArrayList<Integer> वास्तव में एक प्रकार है।

लेकिन यह वास्तव में जावा क्या बात नहीं है !!! इसके बजाय, वे अपनी सीमाओं के साथ लगातार अमूर्त प्रकारों को मानते हैं, अर्थात विशेषज्ञता के लिए पारित मापदंडों से स्वतंत्र एक ही ठोस प्रकार का उत्पादन:

ArrayList
{
    Object[] elems;
}

जो यहाँ अंतर्निहित बाध्य ऑब्जेक्ट ( ArrayList<T extends Object>== ArrayList<T>) के साथ है।

इसके बावजूद कि यह सामान्य सरणियों को अनुपयोगी बनाता है और कच्चे प्रकारों के लिए कुछ अजीब त्रुटियों का कारण बनता है:

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

यह बहुत सारी अस्पष्टताओं का कारण बनता है

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

समान फ़ंक्शन का संदर्भ दें:

void function(ArrayList list)

और इसलिए जेनेरिक विधि अधिभार जावा में उपयोग नहीं किया जा सकता है।


2

मैंने एंड्रॉइड में टाइप इरेज़र के साथ सामना किया है। उत्पादन में हम minify के विकल्प के साथ gradle का उपयोग करते हैं। लघुकरण के बाद मुझे घातक अपवाद मिला है। मैंने अपनी वस्तु की विरासत श्रृंखला दिखाने के लिए सरल कार्य किया है:

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

और इस फ़ंक्शन के दो परिणाम हैं:

छोटा कोड नहीं:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

न्यूनतम कोड:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

तो, न्यूनतम कोड में वास्तविक पैराट्राइज्ड वर्गों को बिना किसी प्रकार की जानकारी के कच्ची कक्षाओं के साथ बदल दिया जाता है। मेरी परियोजना के लिए एक समाधान के रूप में, मैंने सभी प्रतिबिंब कॉल को हटा दिया और फ़ंक्शन तर्कों में पारित स्पष्ट परम प्रकारों के साथ उन्हें उत्तर दिया।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.