การเลือกลายเซ็นของเมธอดสำหรับแลมบ์ดานิพจน์ที่มีเป้าหมายหลายประเภท


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
    คอมไพเลอร์ Eclipse
  • openjdk รุ่น "1.8.0_232"
    คอมไพเลอร์ Eclipse

แก้ไข : โค้ดด้านบนไม่สามารถคอมไพล์ในการสร้างด้วย maven ในขณะที่ Eclipse รวบรวมบรรทัดสุดท้ายของรหัสเรียบร้อยแล้ว


3
ไม่มีการคอมไพล์สามตัวบน Java 8 ตอนนี้ฉันไม่แน่ใจว่านี่เป็นข้อผิดพลาดที่ได้รับการแก้ไขในรุ่นที่ใหม่กว่าหรือข้อผิดพลาด / คุณสมบัติที่แนะนำ ... คุณควรระบุเวอร์ชัน Java
Sweeper

@Sweeper ตอนแรกฉันได้รับสิ่งนี้โดยใช้ jdk-13 การทดสอบที่ตามมาใน java 8 (jdk8u232) แสดงข้อผิดพลาดเดียวกัน ไม่แน่ใจว่าทำไมคนสุดท้ายไม่ได้คอมไพล์ในเครื่องของคุณ
ernest_k

ไม่สามารถทำซ้ำในคอมไพเลอร์ออนไลน์สองรายการ ( 1 , 2 ) ฉันใช้ 1.8.0_221 กับเครื่องของฉัน นี่คือการทำให้แปลกและ Weirder ...
กวาด

1
@ernest_k Eclipse มีการใช้งานคอมไพเลอร์ของตัวเอง นั่นอาจเป็นข้อมูลที่สำคัญสำหรับคำถาม นอกจากนี้ความจริงแล้วข้อผิดพลาดในการสร้างข้อผิดพลาดที่บรรทัดสุดท้ายก็ควรถูกเน้นในคำถามในความคิดของฉัน ในอีกด้านหนึ่งเกี่ยวกับคำถามที่เชื่อมโยงสมมติฐานที่ว่า OP กำลังใช้ Eclipse ก็สามารถทำให้กระจ่างได้เนื่องจากรหัสไม่สามารถทำซ้ำได้
Naman

2
ในขณะที่ฉันเข้าใจว่าคุณค่าทางปัญญาของคำถามฉันสามารถให้คำแนะนำกับวิธีการสร้างการโอเวอร์โหลดที่แตกต่างกันในส่วนต่อประสานการทำงานเท่านั้นและคาดว่าจะสามารถเรียกผ่านแลมบ์ดาได้อย่างปลอดภัย ฉันไม่เชื่อว่าการรวมตัวกันของแลมบ์ดาการอนุมานและการบรรทุกเกินพิกัดเป็นสิ่งที่โปรแกรมเมอร์โดยเฉลี่ยจะเข้าใกล้ความเข้าใจ มันเป็นสมการที่มีตัวแปรมากมายที่ผู้ใช้ไม่สามารถควบคุมได้ หลีกเลี่ยงสิ่งนี้โปรด :)
Stephan Herrmann

คำตอบ:


8

TL; DR นี่เป็นข้อผิดพลาดของคอมไพเลอร์

ไม่มีกฎที่จะให้ความสำคัญกับวิธีการเฉพาะเมื่อมีการสืบทอดหรือวิธีการเริ่มต้น ที่น่าสนใจเมื่อฉันเปลี่ยนรหัสเป็น

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));คำสั่งผลิตข้อผิดพลาดใน Eclipse

เนื่องจากไม่มีการเปลี่ยนแปลงคุณสมบัติของforEach(Consumer<? super T) c)เมธอดจากIterable<T>อินเตอร์เฟสเมื่อประกาศโอเวอร์โหลดอื่นการตัดสินใจของ Eclipse เพื่อเลือกวิธีนี้จึงไม่สามารถ (สม่ำเสมอ) ตามคุณสมบัติใด ๆ ของเมธอด มันยังคงเป็นวิธีการสืบทอดเท่านั้นยังคงเป็น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())เลย และคอมไพเลอร์ Java ทั้งหมดเห็นด้วยกับเรื่องนี้

โปรดทราบว่าaList::addนี่คือการอ้างอิงวิธีการที่คลุมเครือเนื่องจากaddวิธีการดังกล่าวมีมากเกินไปดังนั้นจึงไม่สามารถช่วยเลือกวิธีได้ แต่การอ้างอิงวิธีการอาจได้รับการประมวลผลด้วยรหัสที่แตกต่างกันอยู่แล้ว การเปลี่ยนเป็นการใช้งานที่ไม่คลุมเครือaList::containsหรือเปลี่ยนListไปใช้Collectionเพื่อให้เกิดความaddกำกวมไม่ได้เปลี่ยนผลลัพธ์ในการติดตั้ง Eclipse ของฉัน (ฉันใช้2019-06)


1
@ howlger ความคิดเห็นของคุณไม่สมเหตุสมผล วิธีการเริ่มต้นเป็นวิธีการสืบทอดและวิธีการที่มากเกินไป ไม่มีวิธีอื่น ความจริงที่ว่าวิธีการสืบทอดเป็นdefaultวิธีการเป็นเพียงจุดเพิ่มเติม คำตอบของฉันแสดงตัวอย่างแล้วโดยที่ Eclipse ไม่ได้ให้ความสำคัญกับวิธีการเริ่มต้น
Holger

1
@ howlger มีความแตกต่างพื้นฐานในพฤติกรรมของเรา คุณค้นพบว่าการลบdefaultการเปลี่ยนแปลงผลลัพธ์และสันนิษฐานว่าพบสาเหตุของพฤติกรรมที่สังเกตได้ทันที คุณมีความมั่นใจมากเกินไปเกี่ยวกับเรื่องนี้ที่คุณเรียกว่าคำตอบอื่น ๆ ที่ผิดแม้ว่าพวกเขาจะไม่ได้ขัดแย้งกัน เพราะคุณฉายภาพพฤติกรรมของคุณเองมากกว่าคนอื่นฉันไม่เคยอ้างสิทธิ์ในมรดกเลย ฉันพิสูจน์แล้วว่าไม่ใช่ ฉันแสดงให้เห็นว่าพฤติกรรมนั้นไม่สอดคล้องกันเนื่องจาก Eclipse เลือกวิธีเฉพาะในสถานการณ์หนึ่ง แต่ไม่ใช่ในอีกสถานการณ์หนึ่งซึ่งมีสามโอเวอร์โหลด
Holger

1
@howlger นอกเหนือจากที่ผมอยู่แล้วชื่ออีกสถานการณ์ในตอนท้ายของความคิดเห็นนี้สร้างหนึ่งในอินเตอร์เฟซมรดกไม่มีสองวิธีหนึ่งdefaultที่คนอื่น ๆabstractมีสองผู้บริโภคเช่นการขัดแย้งและลอง Eclipse บอกอย่างถูกต้องว่ามันคลุมเครือแม้จะเป็นdefaultวิธีหนึ่งก็ตาม เห็นได้ชัดว่าการสืบทอดยังคงเกี่ยวข้องกับข้อผิดพลาด Eclipse นี้ แต่ไม่เหมือนกับคุณฉันจะไม่โกรธและเรียกคำตอบอื่น ๆ ที่ผิดเพียงเพราะพวกเขาไม่ได้วิเคราะห์ข้อผิดพลาดให้ครบถ้วน นั่นไม่ใช่งานของเราที่นี่
Holger

1
@ howlger ไม่ประเด็นคือว่านี่เป็นข้อผิดพลาด ผู้อ่านส่วนใหญ่จะไม่สนใจรายละเอียด Eclipse สามารถหมุนลูกเต๋าทุกครั้งที่เลือกวิธีการมันไม่สำคัญ Eclipse ไม่ควรเลือกวิธีเมื่อมันคลุมเครือดังนั้นจึงไม่สำคัญว่าทำไมจึงเลือกวิธีหนึ่ง คำตอบนี้พิสูจน์ว่าพฤติกรรมนั้นไม่สอดคล้องกันซึ่งเพียงพอแล้วที่จะบ่งชี้ว่ามันเป็นแมลง ไม่จำเป็นต้องชี้ไปที่บรรทัดในซอร์สโค้ดของ Eclipse ที่มีสิ่งผิดปกติ นั่นไม่ใช่จุดประสงค์ของ Stackoverflow บางทีคุณอาจสับสนกับ Stackoverflow ด้วยตัวติดตามบั๊กของ Eclipse
Holger

1
@ howlger คุณกำลังอ้างสิทธิ์อีกครั้งอย่างไม่ถูกต้องว่าฉันได้แถลง (ผิด) ว่าทำไม Eclipse จึงเลือกผิด อีกครั้งฉันทำไม่ได้เพราะคราสไม่ควรเลือกเลย วิธีนี้ไม่ชัดเจน จุด. เหตุผลที่ฉันใช้คำว่า " สืบทอด " นั้นเป็นเพราะจำเป็นต้องใช้วิธีการระบุชื่อที่แตกต่างกัน ฉันสามารถพูดว่า " วิธีการเริ่มต้น " แทนโดยไม่ต้องเปลี่ยนตรรกะ อย่างถูกต้องฉันควรใช้วลี“ วิธีที่ดีที่สุดของ Eclipse ที่เลือกไม่ถูกต้องไม่ว่าด้วยเหตุผลใดก็ตาม ” คุณสามารถใช้หนึ่งในสามวลีที่สลับกันได้และตรรกะจะไม่เปลี่ยนแปลง
Holger

2

คอมไพเลอร์ Eclipse แก้ไขdefaultวิธีได้อย่างถูกต้องเนื่องจากเป็นวิธีที่เฉพาะเจาะจงที่สุดตามข้อกำหนดภาษา Java 15.12.2.5 :

หากหนึ่งในวิธีที่เฉพาะเจาะจงที่สุดคือคอนกรีต (นั่นคือไม่ใช่abstractหรือค่าเริ่มต้น) มันเป็นวิธีที่เฉพาะเจาะจงมากที่สุด

javac(ใช้โดย Maven และ IntelliJ โดยค่าเริ่มต้น) บอกว่าการเรียกเมธอดนั้นไม่ชัดเจนที่นี่ แต่ตามข้อกำหนดภาษา Java มันไม่ชัดเจนเนื่องจากหนึ่งในสองวิธีนี้เป็นวิธีที่เฉพาะเจาะจงที่สุดที่นี่

การแสดงออกแลมบ์ดาที่พิมพ์โดยนัยโดยนัยได้รับการจัดการแตกต่างจากการแสดงออกแลมบ์ดาที่พิมพ์โดยชัดเจนใน Java ตรงกันข้ามกับการแสดงออกแลมบ์ดาที่พิมพ์โดยชัดแจ้งโดยปริยายในช่วงแรกเพื่อระบุวิธีการอุทธรณ์อย่างเข้มงวด (ดูJava Language Specification 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
คุณพลาดเงื่อนไขก่อนหน้าประโยคที่อ้างถึง:“ ถ้าวิธีที่เฉพาะเจาะจงมากที่สุดมีการแทนที่ด้วยลายเซ็นที่เทียบเท่ากัน ” แน่นอนว่าสองวิธีที่อาร์กิวเมนต์เป็นอินเตอร์เฟสที่ไม่เกี่ยวข้องทั้งหมดจะไม่มีลายเซ็นเทียบเท่าที่แทนที่ นอกจากนั้นสิ่งนี้จะไม่อธิบายว่าทำไม Eclipse หยุดเลือกdefaultวิธีเมื่อมีวิธีการสมัครสามวิธีหรือเมื่อทั้งสองวิธีถูกประกาศในอินเทอร์เฟซเดียวกัน
Holger

1
@ โฮลเกอร์คำตอบของคุณอ้างว่า"ไม่มีกฎใดที่จะให้ความสำคัญกับวิธีการเฉพาะเมื่อมีการสืบทอดหรือเป็นวิธีเริ่มต้น" ฉันเข้าใจคุณอย่างถูกต้องหรือไม่ว่าคุณกำลังบอกว่าเงื่อนไขเบื้องต้นสำหรับกฎที่ไม่มีอยู่นี้ไม่ได้ใช้ที่นี่ โปรดทราบว่าพารามิเตอร์ที่นี่เป็นส่วนต่อประสานการทำงาน (ดู JLS 9.8)
howlger

1
คุณฉีกประโยคออกจากบริบท ประโยคนี้อธิบายถึงการเลือกวิธีการแทนที่ที่เทียบเท่ากันกล่าวคือทางเลือกระหว่างการประกาศที่ทุกคนจะเรียกใช้วิธีการเดียวกันที่รันไทม์เนื่องจากจะมีวิธีคอนกรีตเพียงวิธีเดียวในคลาสคอนกรีต นี่ไม่เกี่ยวข้องกับกรณีของวิธีการที่แตกต่างกันเช่นforEach(Consumer)และforEach(Consumer2)ซึ่งไม่สามารถจบลงที่วิธีการใช้งานเดียวกัน
Holger

2
@StephanHerrmann ฉันไม่รู้จัก JEP หรือ JSR แต่การเปลี่ยนแปลงดูเหมือนว่าการแก้ไขจะสอดคล้องกับความหมายของ "คอนกรีต" เช่นเปรียบเทียบกับJLS§9.4 :“ วิธีการเริ่มต้นแตกต่างจากวิธีคอนกรีต (§8.4) 3.1) ซึ่งถูกประกาศในคลาส ” ซึ่งไม่เคยเปลี่ยน
Holger

2
@StephanHerrmann ใช่ดูเหมือนว่าการเลือกผู้สมัครจากวิธีการแทนที่ที่เทียบเท่านั้นมีความซับซ้อนมากขึ้นและเป็นเรื่องที่น่าสนใจที่จะทราบถึงเหตุผลเบื้องหลัง แต่ก็ไม่เกี่ยวข้องกับคำถามในมือ ควรมีเอกสารอื่นที่อธิบายการเปลี่ยนแปลงและแรงจูงใจ ในอดีตมี แต่ด้วยนโยบาย "รุ่นใหม่ทุก ๆ ปี" การรักษาคุณภาพดูเหมือนเป็นไปไม่ได้ ...
Holger

2

รหัสที่ Eclipse ใช้ JLS §15.12.2.5ไม่พบวิธีใดวิธีหนึ่งที่เฉพาะเจาะจงกว่าวิธีอื่นแม้แต่ในกรณีของแลมบ์ดาที่พิมพ์อย่างชัดเจน

ดังนั้น Eclipse จะหยุดที่นี่และรายงานความกำกวม น่าเสียดายที่การใช้ความละเอียดเกินพิกัดมีรหัสที่ไม่น่าสนใจนอกเหนือไปจากการใช้ JLS จากความเข้าใจของฉันรหัสนี้ (ซึ่งวันที่จากเวลาเมื่อ Java 5 ใหม่) ต้องเก็บไว้เพื่อเติมช่องว่างบางอย่างใน JLS

ฉันได้ยื่นhttps://bugs.eclipse.org/562538เพื่อติดตามสิ่งนี้

เป็นอิสระจากข้อผิดพลาดนี้โดยเฉพาะฉันสามารถแนะนำอย่างยิ่งกับสไตล์ของรหัสนี้ การบรรทุกเกินพิกัดเป็นสิ่งที่ดีสำหรับจำนวนที่น่าประหลาดใจใน Java, คูณด้วยการอนุมานแลมบ์ดาชนิด, ความซับซ้อนค่อนข้างเกินสัดส่วนที่ได้รับ


ขอบคุณ. มีการบันทึกbugs.eclipse.org/bugs/show_bug.cgi?id=562507เรียบร้อยแล้วบางทีคุณอาจช่วยลิงก์หรือปิดเป็นรายการที่ซ้ำกันได้ ...
ernest_k
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.