कई मिलान लक्ष्य प्रकारों के साथ लैम्ब्डा अभिव्यक्ति के लिए विधि हस्ताक्षर चयन


11

मैं एक प्रश्न का उत्तर दे रहा था और ऐसे परिदृश्य में भाग रहा था जिसे मैं समझा नहीं सकता। इस कोड पर विचार करें:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

मुझे समझ में नहीं आता है कि स्पष्ट रूप से लंबोदर के पैरामीटर को टाइप करने के (A a) -> aList.add(a)कारण कोड संकलित हो जाता है। इसके अतिरिक्त, यह Iterableएक के बजाय एक अधिभार से क्यों जुड़ता है CustomIterable?
क्या इसके बारे में कुछ स्पष्टीकरण है या कल्पना के संबंधित अनुभाग का लिंक है?

नोट: iterable.forEach((A a) -> aList.add(a));केवल संकलित जब CustomIterable<T>फैली Iterable<T>(साफ में तरीकों अधिक भार CustomIterableअस्पष्ट त्रुटि में परिणाम)


दोनों पर यह हो रही है:

  • Openjdk संस्करण "13.0.2" 2020-01-14
    ग्रहण संकलक
  • Openjdk संस्करण "1.8.0_232"
    ग्रहण संकलक

संपादित करें : उपरोक्त कोड मावेन के साथ निर्माण पर संकलित करने में विफल रहता है जबकि ग्रहण सफलतापूर्वक कोड की अंतिम पंक्ति को संकलित करता है।


3
जावा पर तीन में से कोई भी संकलन नहीं। अब मुझे यकीन नहीं है कि यह एक बग है जिसे एक नए संस्करण में तय किया गया है, या एक बग / सुविधा जो पेश की गई है ... आपको संभवतः जावा संस्करण निर्दिष्ट करना चाहिए
स्वीपर

@ स्वीपर I ने शुरू में इसे jdk-13 का उपयोग करके प्राप्त किया। जावा 8 (jdk8u232) में बाद में परीक्षण एक ही त्रुटियों को दर्शाता है। निश्चित नहीं है कि अंतिम आपके मशीन पर या तो संकलन क्यों नहीं करता है।
ernest_k

या तो दो ऑनलाइन कंपाइलरों पर पुन: पेश नहीं किया जा सकता ( 1 , 2 )। मैं अपनी मशीन पर 1.8.0_221 का उपयोग कर रहा हूं। यह निराई और निराई हो रही है ...
स्वीपर

1
@ernest_k ग्रहण का अपना संकलक कार्यान्वयन है। यह प्रश्न के लिए महत्वपूर्ण जानकारी हो सकती है। इसके अलावा, तथ्य यह है कि एक स्वच्छ मावेन त्रुटियों का निर्माण अंतिम पंक्ति भी मेरे विचार में प्रश्न पर प्रकाश डाला जाना चाहिए। दूसरी ओर, जुड़े हुए प्रश्न के बारे में, यह धारणा कि ओपी भी ग्रहण का उपयोग कर रहा है, को स्पष्ट किया जा सकता है, क्योंकि कोड प्रजनन योग्य नहीं है।
नमन

2
जबकि मैं समझता हूं कि प्रश्न के बौद्धिक मूल्य, मैं केवल ओवरलोड तरीकों को बनाने के खिलाफ सलाह दे सकता हूं जो केवल कार्यात्मक इंटरफेस में भिन्न होते हैं और उम्मीद करते हैं कि इसे सुरक्षित रूप से लंबोदर कहा जा सकता है। मेरा मानना ​​है कि लैम्ब्डा प्रकार के अनुमान और ओवरलोडिंग का संयोजन कुछ ऐसा है जो औसत प्रोग्रामर कभी भी समझ के करीब आएगा। यह बहुत से चर के साथ एक समीकरण है जिसे उपयोगकर्ता नियंत्रित नहीं करते हैं। AVOID यह, कृपया :)
Stephan Herrmann

जवाबों:


8

टीएल; डीआर, यह एक कंपाइलर बग है।

ऐसा कोई नियम नहीं है जो विरासत में मिलने या तयशुदा तरीके से होने पर किसी विशेष लागू पद्धति को वरीयता दे। दिलचस्प है, जब मैं कोड को बदल देता हूं

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

iterable.forEach((A a) -> aList.add(a));बयान ग्रहण करने में कोई त्रुटि पैदा करता है।

चूंकि दूसरे अधिभार की घोषणा करते समय इंटरफ़ेस forEach(Consumer<? super T) c)से विधि की कोई संपत्ति नहीं Iterable<T>बदली, इसलिए विधि के किसी भी गुण के आधार पर इस विधि का चयन करने का ग्रहण का निर्णय (लगातार) नहीं हो सकता है। यह अभी भी केवल विरासत में मिली विधि है, अभी भी एकमात्र defaultविधि है, अभी भी एकमात्र JDK विधि है, और इसी तरह। इन गुणों में से किसी को भी विधि चयन को प्रभावित नहीं करना चाहिए।

ध्यान दें कि घोषणा को बदलने के लिए

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

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

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


लेकिन यह समझ में आता है कि आपके उदाहरण के निर्माण को संकलक में विभिन्न कार्यान्वयन कोड द्वारा नियंत्रित किया जा सकता है, एक बग प्रदर्शित करता है जबकि दूसरा नहीं करता है।
a -> aList.add(a)एक स्पष्ट रूप से टाइप किया गया लैम्ब्डा अभिव्यक्ति है, जिसका उपयोग अधिभार संकल्प के लिए नहीं किया जा सकता है। इसके विपरीत, (A a) -> aList.add(a)एक स्पष्ट रूप से टाइप की गई लैम्ब्डा अभिव्यक्ति है जिसका उपयोग ओवरलोड तरीकों से मिलान विधि का चयन करने के लिए किया जा सकता है, लेकिन यह यहां मदद नहीं करता है (यहां मदद नहीं करनी चाहिए), क्योंकि सभी विधियों में बिल्कुल कार्यात्मक हस्ताक्षर के साथ पैरामीटर प्रकार हैं ।

एक काउंटर-उदाहरण के रूप में, के साथ

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

कार्यात्मक हस्ताक्षर भिन्न होते हैं, और स्पष्ट रूप से टाइप किए गए लैम्ब्डा अभिव्यक्ति का उपयोग करना वास्तव में सही विधि का चयन करने में मदद कर सकता है जबकि अंतर्निहित टाइप किए गए लैम्ब्डा अभिव्यक्ति में मदद नहीं करता है, इसलिए forEach(s -> s.isEmpty())एक कंपाइलर त्रुटि पैदा करता है। और सभी जावा कंपाइलर इसके बारे में सहमत हैं।

ध्यान दें कि aList::addएक अस्पष्ट विधि संदर्भ है, क्योंकि addविधि भी अतिभारित है, इसलिए यह एक विधि का चयन करने में भी मदद नहीं कर सकता है, लेकिन विधि संदर्भों को अलग-अलग कोड द्वारा संसाधित किया जा सकता है। एक अस्पष्ट aList::containsया स्विच करने के Listलिए Collection, addअस्पष्ट बनाने के लिए स्विच करना , मेरे ग्रहण स्थापना (मैंने इस्तेमाल किया 2019-06) में परिणाम नहीं बदला ।


1
@ आपकी टिप्पणी का कोई मतलब नहीं है। डिफ़ॉल्ट विधि है विरासत में मिला विधि और विधि ओवरलोड हो गया है। कोई दूसरी विधि नहीं है। तथ्य यह है कि विरासत में मिली विधि defaultकेवल एक अतिरिक्त बिंदु है। मेरा जवाब पहले से ही एक उदाहरण दिखाता है जहां ग्रहण डिफ़ॉल्ट विधि से पूर्वता नहीं देता है।
होल्गर

1
@ हमारे व्यवहार में मूलभूत अंतर है। आपने केवल यह पता लगाया कि defaultपरिणाम को हटाने से तुरंत मान लिया गया है कि पाया गया व्यवहार का कारण पाया गया है। आप इसके बारे में इतने अधिक आत्मविश्वास से भरे हैं कि आप अन्य उत्तरों को गलत कहते हैं, इस तथ्य के बावजूद कि वे विरोधाभासी भी नहीं हैं। क्योंकि आप दूसरे के ऊपर अपना व्यवहार प्रस्तुत कर रहे हैं, मैंने कभी दावा नहीं किया कि विरासत का कारण था । मैंने साबित किया कि यह नहीं है। मैंने दिखाया कि व्यवहार असंगत है, क्योंकि ग्रहण एक परिदृश्य में विशेष पद्धति का चयन करता है, लेकिन दूसरे में नहीं, जहां तीन अधिभार मौजूद हैं।
होल्गर

1
@ इसके अलावा, मैंने पहले से ही इस टिप्पणी के अंत में एक और परिदृश्य का नाम दिया , एक इंटरफ़ेस, कोई विरासत, दो तरीके, एक defaultदूसरे abstract, दो उपभोक्ता जैसे तर्कों के साथ बनाएं और इसे आज़माएं। ग्रहण सही ढंग से कहता है कि यह अस्पष्ट है, इसके बावजूद एक defaultतरीका है। जाहिरा तौर पर विरासत अभी भी इस ग्रहण बग के लिए प्रासंगिक है, लेकिन आप के विपरीत, मैं पागल नहीं जा रहा हूं और अन्य उत्तरों को गलत कहता हूं, सिर्फ इसलिए कि वे बग को इसकी संपूर्णता का विश्लेषण नहीं करते थे। यहां हमारा काम नहीं है।
होल्गर

1
@ कोई भी बात नहीं है, यह एक बग है। अधिकांश पाठकों को विवरणों की भी परवाह नहीं होगी। हर बार एक विधि का चयन करने पर ग्रहण पासा पलट सकता है, इससे कोई फर्क नहीं पड़ता। अस्पष्ट होने पर ग्रहण विधि का चयन नहीं करना चाहिए, इसलिए इससे कोई फर्क नहीं पड़ता कि यह एक का चयन क्यों करता है। यह उत्तर साबित करता है कि व्यवहार असंगत है, जो पहले से ही दृढ़ता से इंगित करने के लिए पर्याप्त है कि यह एक बग है। यह ग्रहण के स्रोत कोड में उस रेखा को इंगित करने के लिए आवश्यक नहीं है जहां चीजें गलत हो जाती हैं। यह स्टैकओवरफ़्लो का उद्देश्य नहीं है। शायद, आप एक्लिप्स के बग ट्रैकर के साथ स्टैकओवरफ्लो को भ्रमित कर रहे हैं।
होल्गर

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

2

एक्लिप्स कंपाइलर सही तरीके से defaultविधि का निर्धारण करता है , क्योंकि यह जावा लैंग्वेज स्पेसिफिकेशन 15.12.2.5 के अनुसार सबसे विशिष्ट विधि है :

यदि वास्तव में अधिकतम विशिष्ट विधियों में से एक ठोस है (जो कि, गैर- abstractया डिफ़ॉल्ट है), तो यह सबसे विशिष्ट विधि है।

javac(डिफ़ॉल्ट रूप से मैवेन और इंटेलीजे द्वारा उपयोग किया जाता है) बताता है कि विधि कॉल यहां अस्पष्ट है। लेकिन जावा लैंग्वेज स्पेसिफिकेशन के अनुसार यह अस्पष्ट नहीं है क्योंकि दो विधियों में से एक यहाँ सबसे विशिष्ट विधि है।

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

आपके मामले में, इस बग के लिए वैकल्पिक रूप से टाइप किए गए लंबोदर अभिव्यक्ति का उपयोग करने के बजाय कार्यात्मक इंटरफ़ेसjavac के प्रकार को निर्दिष्ट करना है:

iterable.forEach((ConsumerOne<A>) aList::add);

या

iterable.forEach((Consumer<A>) aList::add);

यहाँ आपके उदाहरण को परीक्षण के लिए और छोटा कर दिया गया है:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}

3
आप उद्धृत वाक्य के ठीक पहले वाली शर्त से चूक गए: " यदि सभी अधिकतम विशिष्ट विधियों में ओवरराइड-समतुल्य हस्ताक्षर हैं ", तो दो विधियाँ जिनके तर्क पूर्णतया असंबंधित हैं, उनमें अतिरंजित समतुल्य हस्ताक्षर नहीं हैं। इसके अलावा, यह स्पष्ट नहीं करेगा कि defaultतीन उम्मीदवार विधियाँ या जब दोनों विधियाँ एक ही इंटरफ़ेस में घोषित की जाती हैं, तो ग्रहण विधि का चयन क्यों रोक देता है।
होल्गर

1
@Holger आपका जवाब दावा करता है, "ऐसा कोई नियम नहीं है जो विरासत में मिलने या तयशुदा तरीके से होने पर किसी विशेष लागू पद्धति को वरीयता दे।" क्या मैं आपको सही ढंग से समझता हूं कि आप कह रहे हैं कि इस गैर-मौजूद नियम के लिए पूर्व शर्त यहाँ लागू नहीं होती है? कृपया ध्यान दें, यहाँ पैरामीटर एक कार्यात्मक इंटरफ़ेस है (JLS 9.8 देखें)।
हॉवेलर

1
आपने एक वाक्य को संदर्भ से बाहर निकाल दिया। वाक्य में ओवरराइड-समतुल्य विधियों के चयन का वर्णन है , दूसरे शब्दों में, घोषणाओं के बीच एक विकल्प जो सभी रनटाइम पर एक ही विधि को लागू करेगा, क्योंकि कंक्रीट वर्ग में केवल एक ही ठोस विधि होगी। यह अलग-अलग तरीकों के मामले के लिए अप्रासंगिक है forEach(Consumer)और forEach(Consumer2)जो कभी भी एक ही कार्यान्वयन विधि पर समाप्त नहीं हो सकते हैं।
होल्गर

2
@StephanHerrmann मुझे JEP या JSR नहीं पता है, लेकिन परिवर्तन "कंक्रीट" के अर्थ के अनुरूप होने के लिए एक फिक्स की तरह दिखता है, अर्थात JLS§9.4 के साथ तुलना करें : " डिफ़ॉल्ट तरीके ठोस तरीकों (§4.4) से अलग हैं। 3.1), जो कक्षाओं में घोषित किए जाते हैं। ”, जो कभी नहीं बदला।
होल्गर

2
@StephanHerrmann हाँ, ऐसा लगता है कि ओवरराइड-समतुल्य विधियों से एक उम्मीदवार का चयन करना अधिक जटिल हो गया है और इसके पीछे के तर्क को जानना दिलचस्प होगा, लेकिन यह प्रश्न के लिए प्रासंगिक नहीं है। परिवर्तनों और प्रेरणाओं को समझाने वाला एक और दस्तावेज होना चाहिए। अतीत में, वहाँ था, लेकिन इस "एक नया संस्करण हर आधा साल 'की नीति के साथ, गुणवत्ता रखने के लिए असंभव लगता है ...
होल्गर

2

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

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

मैंने https://bugs.eclipse.org/562538 दायर किया है ट्रैक करने के लिए ।

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


धन्यवाद। पहले से ही bugs.eclipse.org/bugs/show_bug.cgi?id=562507 लॉग इन किया था , हो सकता है कि आप उन्हें लिंक करने में मदद करें या किसी को डुप्लिकेट के रूप में बंद करें ...
ernest_k
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.