java - स्पष्ट अनाम आंतरिक वर्ग के बजाय लैम्ब्डा का उपयोग करते समय विभिन्न सामान्य व्यवहार




generics lambda (2)

tldr:

  1. javac में एक बग है जो javac -एम्बेडेड इनर क्लासेस के लिए गलत एन्कोडिंग विधि को रिकॉर्ड करता है। परिणामस्वरूप, वास्तविक एन्कोडिंग विधि पर चर को उन आंतरिक वर्गों द्वारा हल नहीं किया जा सकता है।
  2. यकीनन java.lang.reflect API कार्यान्वयन में बग के दो सेट हैं:
    • कुछ तरीकों को अपवाद के रूप में प्रलेखित किया जाता है जब कोई भी प्रकार का सामना नहीं किया जाता है, लेकिन वे कभी नहीं करते हैं। इसके बजाय, वे अशक्त संदर्भों को प्रचारित करने की अनुमति देते हैं।
    • विभिन्न Type::toString() वर्तमान में एक प्रकार का समाधान नहीं किया जा सकता है जब NullPointerException फेंक या प्रचारित करता है।

इसका उत्तर जेनेरिक हस्ताक्षरों के साथ करना है जो आमतौर पर क्लास फ़ाइलों में उत्सर्जित होते हैं जो जेनरिक का उपयोग करते हैं।

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

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

TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};

... इस Signature में शामिल हैं:

LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;

इस से, java.lang.reflection API आपके (अनाम) वर्ग के बारे में सामान्य सुपरस्क्रिप्ट की जानकारी पार्स कर सकता है।

लेकिन हम पहले से ही जानते हैं कि यह ठीक काम करता है जब TypeToken को ठोस प्रकारों के साथ पैरामीटरित किया जाता है। आइए एक अधिक प्रासंगिक उदाहरण देखें, जहां इसके प्रकार पैरामीटर में एक प्रकार का चर शामिल है:

static <F> void test() {
    TypeToken sup = new TypeToken<F[]>() {};
}

यहाँ, हमें निम्नलिखित हस्ताक्षर मिलते हैं:

LTypeToken<[TF;>;

समझ में आता है, है ना? अब, आइए देखें कि java.lang.reflect एपीआई इन हस्ताक्षरों से जेनेरिक सुपरटाइप जानकारी कैसे निकाल सकते हैं। अगर हम Class::getGenericSuperclass() में पियर करते हैं Class::getGenericSuperclass() , तो हम देखते हैं कि सबसे पहली चीज जो है वह है getGenericInfo() । यदि हमें पहले इस विधि में नहीं बुलाया गया है, तो एक ClassRepository हो जाता है:

private ClassRepository getGenericInfo() {
    ClassRepository genericInfo = this.genericInfo;
    if (genericInfo == null) {
        String signature = getGenericSignature0();
        if (signature == null) {
            genericInfo = ClassRepository.NONE;
        } else {
            // !!!  RELEVANT LINE HERE:  !!!
            genericInfo = ClassRepository.make(signature, getFactory());
        }
        this.genericInfo = genericInfo;
    }
    return (genericInfo != ClassRepository.NONE) ? genericInfo : null;
}

यहाँ महत्वपूर्ण टुकड़ा getFactory() लिए कॉल है, जो करने के लिए फैलता है:

CoreReflectionFactory.make(this, ClassScope.make(this))

ClassScope वह बिट है जिसकी हम परवाह करते हैं: यह प्रकार चर के लिए एक रिज़ॉल्यूशन गुंजाइश प्रदान करता है। एक प्रकार के चर नाम को देखते हुए, गुंजाइश एक मिलान प्रकार चर के लिए खोज की जाती है। यदि कोई नहीं पाया जाता है, तो 'बाहरी' या एनक्लोजिंग स्कोप खोजा जाता है:

public TypeVariable<?> lookup(String name) {
    TypeVariable<?>[] tas = getRecvr().getTypeParameters();
    for (TypeVariable<?> tv : tas) {
        if (tv.getName().equals(name)) {return tv;}
    }
    return getEnclosingScope().lookup(name);
}

और, अंत में, यह सब करने के लिए महत्वपूर्ण ( ClassScope ):

protected Scope computeEnclosingScope() {
    Class<?> receiver = getRecvr();

    Method m = receiver.getEnclosingMethod();
    if (m != null)
        // Receiver is a local or anonymous class enclosed in a method.
        return MethodScope.make(m);

    // ...
}

यदि एक प्रकार का चर (उदाहरण के लिए, F ) वर्ग पर ही नहीं पाया जाता है (जैसे, अनाम TypeToken<F[]> ), तो अगला चरण TypeToken<F[]> विधि की खोज करना है । यदि हम असंतुष्ट अनाम वर्ग को देखते हैं, तो हम इस विशेषता को देखते हैं:

EnclosingMethod: LambdaTest.test()V

इस विशेषता की उपस्थिति का मतलब है कि computeEnclosingScope जेनेरिक विधि static <F> void test() लिए एक MethodScope उत्पादन करेगा। चूंकि test प्रकार W घोषित करता है, हम इसे तब पाते हैं जब हम एनक्लोजिंग गुंजाइश खोजते हैं।

तो, यह एक मेमने के अंदर काम क्यों नहीं करता है?

इसका उत्तर देने के लिए, हमें यह समझना चाहिए कि लैम्ब्डा कैसे संकलित होती है। लैम्ब्डा का शरीर एक सिंथेटिक स्थैतिक विधि में स्थानांतरित हो जाता है । उस बिंदु पर जहां हम अपने invokedynamic घोषणा करते हैं, एक invokedynamic निर्देश उत्सर्जित हो जाता है, जो TypeToken कार्यान्वयन वर्ग का कारण बनता है जो पहली बार उस निर्देश को हिट करता है।

इस उदाहरण में, लैम्ब्डा बॉडी के लिए उत्पन्न स्टैटिक विधि कुछ इस तरह दिखती है (यदि विघटित होती है):

private static /* synthetic */ Object lambda$test$0() {
    return new LambdaTest$1();
}

... जहां LambdaTest$1 आपका अनाम वर्ग है। आइए हम असंतुष्ट हों और हमारी विशेषताओं का निरीक्षण करें:

Signature: LTypeToken<TW;>;
EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;

उस मामले की तरह, जहां हमने एक लंबोदर के बाहर एक अनाम प्रकार को तत्काल किया, हस्ताक्षर में प्रकार चर W लेकिन EnclosingMethod सिंथेटिक विधि को संदर्भित करता है

सिंथेटिक विधि lambda$test$0() प्रकार चर W घोषणा नहीं करता है। इसके अलावा, lambda$test$0() test() द्वारा संलग्न नहीं है, इसलिए W की घोषणा इसके अंदर दिखाई नहीं देती है। आपके अनाम वर्ग का एक सुपर-टाइप है जिसमें एक प्रकार का वैरिएबल है जो आपकी कक्षा के बारे में नहीं जानता है क्योंकि यह दायरे से बाहर है।

जब हम getGenericSuperclass() , तो LambdaTest$1 लिए गुंजाइश पदानुक्रम में W शामिल नहीं है, इसलिए पार्सर इसे हल नहीं कर सकता है। कोड कैसे लिखा जाता है, इस अनसुलझे प्रकार के चर के परिणामस्वरूप सामान्य सुपरपेप के पैरामीटर में null जाते हैं।

ध्यान दें कि, आपके लैम्ब्डा ने एक प्रकार को तुरंत बदल दिया था जो किसी भी प्रकार के चर (जैसे, TypeToken<String> ) को संदर्भित नहीं करता था तब आप इस समस्या में नहीं चलेंगे।

निष्कर्ष

(i) javac में एक बग है। जावा वर्चुअल मशीन विशिष्टता §4.7.7 (" §4.7.7 एट्रीब्यूट") में कहा गया है:

यह सुनिश्चित करने के लिए एक जावा कंपाइलर की जिम्मेदारी है कि method_index माध्यम से पहचाना जाने वाला तरीका वास्तव में क्लास का निकटतम method_index से method_index वाला तरीका है जिसमें यह EnclosingMethod विशेषता है। (जोर मेरा)

वर्तमान में, लैम्ब्डा राइटर अपने कोर्स को चलाने के बाद जेवैक एन्क्लोजिंग विधि निर्धारित करता है, और परिणामस्वरूप, एन्क्लोसिंगमेथोड विशेषता एक ऐसी विधि को संदर्भित करता है जो कभी भी लेक्सिकल दायरे में मौजूद नहीं थी। यदि EnclosingMethod ने वास्तविक रूप से संलग्न विधि की सूचना दी, तो उस विधि पर चर प्रकार को lambda- एम्बेडेड क्लासेस द्वारा हल किया जा सकता है, और आपका कोड अपेक्षित परिणाम देगा।

यकीनन यह भी एक बग है कि हस्ताक्षर पार्सर / रेफ़र चुपचाप एक null प्रकार के तर्क को एक ParameterizedType (जो कि, @ टॉम-ह्वाटिन-टैकललाइन बिंदु के रूप में) के रूप में प्रचारित करने की अनुमति देता है, में स्टर्लिंग toString() जैसे एनपीई फेंकने toString() जैसे सहायक प्रभाव होते हैं।

EnclosingMethod मुद्दे के लिए मेरी बग रिपोर्ट अब ऑनलाइन है।

(ii) java.lang.reflect और इसके सहायक API में यकीनन कई बग हैं।

विधि ParameterizedType::getActualTypeArguments() को ParameterizedType::getActualTypeArguments() को फेंकने के रूप में प्रलेखित किया जाता है, जब "कोई भी वास्तविक प्रकार का तर्क एक गैर-मौजूद प्रकार की घोषणा को संदर्भित करता है"। यह विवरण यकीनन उस मामले को कवर करता है जहां एक प्रकार का चर दायरे में नहीं है। GenericArrayType::getGenericComponentType() को एक समान अपवाद फेंकना चाहिए जब "अंतर्निहित सरणी का प्रकार एक गैर-मौजूद प्रकार की घोषणा को संदर्भित करता है"। वर्तमान में, न तो किसी भी परिस्थिति में TypeNotPresentException को फेंकना प्रतीत होता है।

मैं यह भी तर्क दूंगा कि विभिन्न Type::toString ओवरराइड ओवरराइड को केवल एनपीई या किसी अन्य अपवाद को फेंकने के बजाय किसी भी अनसुलझे प्रकार के विहित नाम में भरना चाहिए।

मैंने इन प्रतिबिंब संबंधी मुद्दों के लिए एक बग रिपोर्ट प्रस्तुत की है, और सार्वजनिक रूप से दिखाई देने के बाद मैं लिंक को पोस्ट करूंगा।

समाधान?

यदि आपको संलग्न करने की विधि द्वारा घोषित एक प्रकार के चर का संदर्भ देने में सक्षम होने की आवश्यकता है, तो आप ऐसा नहीं कर सकते; आपको लंबे अनाम प्रकार के सिंटैक्स पर वापस आना होगा। हालांकि, लैम्ब्डा संस्करण को ज्यादातर अन्य मामलों में काम करना चाहिए। आपको संलग्न वर्ग द्वारा घोषित प्रकार चर भी संदर्भित करने में सक्षम होना चाहिए। उदाहरण के लिए, ये हमेशा काम करना चाहिए:

class Test<X> {
    void test() {
        Supplier<TypeToken<X>> s1 = () -> new TypeToken<X>() {};
        Supplier<TypeToken<String>> s2 = () -> new TypeToken<String>() {};
        Supplier<TypeToken<List<String>>> s3 = () -> new TypeToken<List<String>>() {};
    }
}

दुर्भाग्य से, यह देखते हुए कि यह बग स्पष्ट रूप से अस्तित्व में है क्योंकि लैम्ब्डा को पहली बार पेश किया गया था, और यह सबसे हालिया एलटीएस रिलीज में तय नहीं किया गया है, आपको यह तय करने के लंबे समय बाद तक अपने ग्राहकों की जेडडीके में बग अवशेषों को मानना ​​पड़ सकता है, यह मान लिया जाता है बिल्कुल तय।

प्रसंग

मैं एक ऐसी परियोजना पर काम कर रहा हूं जो सामान्य प्रकारों पर निर्भर है। इसके प्रमुख घटकों में से एक तथाकथित TypeToken , जो रनटाइम के दौरान सामान्य प्रकार का प्रतिनिधित्व करने और उन पर कुछ उपयोगिता कार्यों को लागू करने का एक तरीका प्रदान करता है। जावा के टाइप एरासुर से बचने के लिए, मैं स्वचालित रूप से उत्पन्न उपवर्ग बनाने के लिए घुंघराले कोष्ठक संकेतन ( {} ) का उपयोग कर रहा हूं क्योंकि यह प्रकार को पुन: प्रयोज्य बनाता है।

TypeToken मूल रूप से क्या करता है

यह TypeToken का दृढ़ता से सरलीकृत संस्करण है जो मूल कार्यान्वयन की तुलना में अधिक उदार है। हालांकि, मैं इस दृष्टिकोण का उपयोग कर रहा हूं ताकि मैं यह सुनिश्चित कर सकूं कि वास्तविक समस्या उन उपयोगिता कार्यों में से एक में झूठ नहीं है।

public class TypeToken<T> {

    private final Type type;
    private final Class<T> rawType;

    private final int hashCode;


    /* ==== Constructor ==== */

    @SuppressWarnings("unchecked")
    protected TypeToken() {
        ParameterizedType paramType = (ParameterizedType) this.getClass().getGenericSuperclass();
        this.type = paramType.getActualTypeArguments()[0];

        // ...
    } 

जब यह काम करता है

मूल रूप से, यह कार्यान्वयन लगभग हर स्थिति में पूरी तरह से काम करता है। इसे ज्यादातर प्रकारों को संभालने में कोई समस्या नहीं है। निम्नलिखित उदाहरण पूरी तरह से काम करते हैं:

TypeToken<List<String>> token = new TypeToken<List<String>>() {};
TypeToken<List<? extends CharSequence>> token = new TypeToken<List<? extends CharSequence>>() {};

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

<T> void test() {
    TypeToken<T[]> token = new TypeToken<T[]>() {};
}

इस स्थिति में, type एक GenericArrayType जो अपने घटक प्रकार के रूप में एक GenericArrayType रखता है। यह पूरी तरह से ठीक है।

लंबोदर का उपयोग करते समय अजीब स्थिति

हालाँकि, जब आप TypeToken अंदर TypeToken को इनिशियलाइज़ करते हैं, तो चीजें बदलने लगती हैं। (प्रकार चर ऊपर test समारोह से आता है)

Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};

इस स्थिति में, type अभी भी एक GenericArrayType , लेकिन यह इसके घटक प्रकार के रूप में null

लेकिन अगर आप एक अनाम आंतरिक वर्ग बना रहे हैं, तो चीजें फिर से बदलने लगती हैं:

Supplier<TypeToken<T[]>> sup = new Supplier<TypeToken<T[]>>() {
        @Override
        public TypeToken<T[]> get() {
            return new TypeToken<T[]>() {};
        }
    };

इस स्थिति में, घटक प्रकार फिर से सही मान (TypeVariable) रखता है

परिणामी प्रश्न

  1. लैम्ब्डा-उदाहरण में टाइप-वेरिएबल का क्या होता है? प्रकार का अनुमान सामान्य प्रकार का सम्मान क्यों नहीं करता है?
  2. स्पष्ट रूप से घोषित और अंतर्निहित घोषित उदाहरण के बीच क्या अंतर है? क्या प्रकार का अनुमान एकमात्र अंतर है?
  3. बॉयलरप्लेट स्पष्ट घोषणा का उपयोग किए बिना मैं इसे कैसे ठीक कर सकता हूं? यह यूनिट टेस्टिंग में विशेष रूप से महत्वपूर्ण हो जाता है क्योंकि मैं यह जांचना चाहता हूं कि कंस्ट्रक्टर अपवाद छोड़ता है या नहीं।

इसे थोड़ा स्पष्ट करने के लिए: यह प्रोग्राम के लिए "प्रासंगिक" समस्या नहीं है क्योंकि मैं गैर-रिज़ॉल्वेबल प्रकारों की अनुमति नहीं देता, लेकिन यह अभी भी एक दिलचस्प घटना है जिसे मैं समझना चाहता हूं।

मेरा शोध

अपडेट १

इस बीच, मैंने इस विषय पर कुछ शोध किया है। जावा लैंग्वेज स्पेसिफिकेशन .1215.12.2.2 में मुझे एक अभिव्यक्ति मिली है जिसके साथ कुछ करना हो सकता है - "प्रयोज्यता के अनुकूल ", अपवाद के रूप में "स्पष्ट रूप से टाइप किए गए लंबो एक्सप्रेशन" का उल्लेख। जाहिर है, यह गलत अध्याय है, लेकिन अभिव्यक्ति अन्य प्रकारों में उपयोग की जाती है, जिसमें अध्याय प्रकार के अनुमान शामिल हैं।

लेकिन ईमानदार होने के लिए: मुझे वास्तव में अभी तक यह पता नहीं चला है कि उन सभी ऑपरेटरों को क्या पसंद है := या Fi0 मतलब है कि इसे विस्तार से समझना वास्तव में कठिन है। मुझे खुशी होगी अगर कोई इसे थोड़ा स्पष्ट कर सकता है और यदि यह अजीब व्यवहार का स्पष्टीकरण हो सकता है।

अपडेट २

मैंने फिर से उस दृष्टिकोण के बारे में सोचा है और इस निष्कर्ष पर पहुंचा हूं, कि यदि कंपाइलर टाइप को हटा देगा क्योंकि यह "प्रयोज्यता के अनुकूल नहीं है", तो यह सबसे उदार प्रकार के बजाय घटक प्रकार को null करने के लिए सेट करने के लिए उचित नहीं है , वस्तु। मैं एक भी कारण नहीं सोच सकता कि भाषा डिजाइनरों ने ऐसा करने का फैसला क्यों किया।

अपडेट ३

मैंने अभी जावा के नवीनतम संस्करण के साथ एक ही कोड को 8u191 किया है (मैंने पहले 8u191 उपयोग किया था)। मेरे अफसोस के लिए, यह कुछ भी नहीं बदला है, हालांकि जावा के प्रकार के अनुमान में सुधार किया गया है ...

अद्यतन ४

मैंने कुछ दिनों पहले ऑफिशियल जावा बग डेटाबेस / ट्रैकर में प्रवेश का अनुरोध किया है और यह अभी स्वीकार कर लिया गया है। चूंकि मेरी रिपोर्ट की समीक्षा करने वाले डेवलपर्स ने प्राथमिकता P4 को बग को सौंपा था, इसलिए इसे ठीक होने तक कुछ समय लग सकता है। आप रिपोर्ट here पा सकते हैं।

टॉम हॉल्टिन के लिए एक विशाल चिल्लाहट - यह उल्लेख करने के लिए समझौता करता है कि यह जावा एसई में ही एक आवश्यक बग हो सकता है। हालांकि, माइक स्ट्रोबेल की एक रिपोर्ट संभवतः उनके प्रभावशाली पृष्ठभूमि ज्ञान के कारण मेरी तुलना में अधिक विस्तृत होगी। हालाँकि, जब मैंने रिपोर्ट लिखी, स्ट्रोबेल का जवाब अभी तक उपलब्ध नहीं था।


मुझे युक्ति का प्रासंगिक हिस्सा नहीं मिला है, लेकिन यहाँ एक आंशिक उत्तर है।

वहाँ निश्चित रूप से एक null घटक प्रकार के साथ null । स्पष्ट होने के लिए, यह TypeToken.type ऊपर की कास्ट से GenericArrayType ( GenericArrayType !) विधि getGenericComponentType साथ है। एपीआई डॉक्स स्पष्ट रूप से उल्लेख नहीं करते हैं कि null रिटर्न वैध है या नहीं। हालाँकि, toString विधि NullPointerException फेंकती है, इसलिए निश्चित रूप से एक बग (जावा के कम से कम यादृच्छिक संस्करण में मैं उपयोग कर रहा हूं)।

मेरे पास bugs.java.com खाता नहीं है, इसलिए यह रिपोर्ट नहीं कर सकता। कोई चाहिए।

आइए उत्पन्न की गई फ़ाइलों पर एक नज़र डालें।

javap -private YourClass

यह एक लिस्टिंग का उत्पादन करना चाहिए जिसमें कुछ शामिल हैं:

static <T> void test();
private static TypeToken lambda$test$0();

ध्यान दें कि हमारी स्पष्ट test पद्धति में इसका प्रकार पैरामीटर है, लेकिन सिंथेटिक लैम्बडा विधि नहीं है। आप कुछ इस तरह की उम्मीद कर सकते हैं:

static <T> void test();
private static <T> TypeToken<T[]> lambda$test$0(); /*** DOES NOT HAPPEN ***/
             // ^ name copied from `test`
                          // ^^^ `Object[]` would not make sense

ऐसा क्यों नहीं होता है संभवत: क्योंकि यह एक संदर्भ में एक विधि प्रकार पैरामीटर होगा जहां एक प्रकार के प्रकार के पैरामीटर की आवश्यकता होती है, और वे आश्चर्यजनक रूप से अलग-अलग चीजें हैं। लैम्ब्डा पर भी प्रतिबंध है कि उन्हें विधि प्रकार के मापदंडों की अनुमति नहीं है, जाहिर है क्योंकि कोई स्पष्ट संकेतन नहीं है (कुछ लोग सुझाव दे सकते हैं कि यह एक खराब बहाना जैसा लगता है)।

निष्कर्ष: यहाँ कम से कम एक अप्रयुक्त JDK बग है। reflect एपीआई और भाषा का यह लैम्ब्डा + जेनेरिक हिस्सा मेरे स्वाद के लिए नहीं है।





java-8