Java แล้วเปรียบเทียบลายเซ็นตัวแทน

Aug 26 2020

เหตุใดคำประกาศจึงมีลักษณะเช่นนี้:

default <U extends Comparable<? super U>> Comparator<T> thenComparing(
            Function<? super T, ? extends U> keyExtractor)

ฉันเข้าใจมากที่สุด มันสมเหตุสมผลที่Uจะเป็นอะไรก็ได้ตราบเท่าที่มันเปรียบได้กับ superclass ของตัวมันเองและยังเปรียบได้กับตัวมันเอง

แต่ฉันไม่ได้รับส่วนนี้: Function<? super T, ? extends U>

ทำไมไม่เพียงแค่มี: Function<? super T, U>

U ไม่สามารถกำหนดพารามิเตอร์เป็นสิ่งที่ keyExtractor ส่งคืนและยังคงขยายComparable<? super U>เหมือนเดิมทั้งหมดได้หรือไม่?

คำตอบ

15 DelfikPro Sep 23 2020 at 01:48

ทำไมถึงเป็น? extends Uและไม่U?

เนื่องจากข้อกำหนดของรหัส ตรวจสอบคำตอบของ @ deduperสำหรับคำอธิบายที่ดี

มีความแตกต่างจริงหรือไม่?

เมื่อเขียนโค้ดตามปกติคอมไพเลอร์ของคุณจะสรุปว่าถูกต้องTสำหรับสิ่งต่างๆเช่นSupplier<T>และFunction<?, T>ดังนั้นจึงไม่มีเหตุผลที่เป็นประโยชน์ในการเขียนSupplier<? extends T>หรือFunction<?, ? extends T>เมื่อพัฒนา API

แต่จะเกิดอะไรขึ้นถ้าเราระบุประเภทด้วยตนเอง ?

void test() {
    Supplier<Integer> supplier = () -> 0;

    this.strict(supplier); // OK (1)
    this.fluent(supplier); // OK

    this.<Number>strict(supplier); // compile error (2)
    this.<Number>fluent(supplier); // OK (3)
}

<T> void strict(Supplier<T>) {}
<T> void fluent(Supplier<? extends T>) {}
  1. อย่างที่คุณเห็นstrict()ทำงานได้ดีโดยไม่มีการประกาศอย่างชัดเจนเนื่องจากTกำลังอนุมานว่าIntegerตรงกับประเภททั่วไปของตัวแปรในเครื่อง

  2. จากนั้นก็จะหยุดพักเมื่อเราพยายามที่จะผ่านSupplier<Integer>เป็นSupplier<Number>เพราะIntegerและNumber ไม่ได้เข้ากันได้

  3. และจากนั้นก็ทำงานร่วมกับfluent()เพราะ? extends NumberและInteger มีความเข้ากันได้

ในทางปฏิบัติจะเกิดขึ้นได้ก็ต่อเมื่อคุณมีประเภททั่วไปหลายประเภทคุณต้องระบุประเภทหนึ่งอย่างชัดเจนและระบุอีกประเภทหนึ่งไม่ถูกต้อง ( Supplierหนึ่ง) ตัวอย่างเช่น:

void test() {
    Supplier<Integer> supplier = () -> 0;
    // If one wants to specify T, then they are forced to specify U as well:
    System.out.println(this.<List<?>, Number> supplier);
    // And if U happens to be incorrent, then the code won't compile.
}

<T, U> T method(Supplier<U> supplier);

ตัวอย่างด้วยComparator(คำตอบเดิม)

พิจารณาComparator.comparingลายเซ็นวิธีต่อไปนี้:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
    Function<? super T, U> keyExtractor
)

นอกจากนี้ยังเป็นลำดับชั้นของชั้นเรียนการทดสอบ:

class A implements Comparable<A> {
    public int compareTo(A object) { return 0; }
}

class B extends A { }

ตอนนี้ลองสิ่งนี้:

Function<Object, B> keyExtractor = null;
Comparator.<Object, A>comparing(keyExtractor); // compile error
error: incompatible types: Function<Object,B> cannot be converted to Function<? super Object,A>
8 deduper Sep 24 2020 at 15:56

TL; DR :

Comparator.thenComparing(Function< ? super T, ? extends U > keyExtractor)( วิธีที่คำถามของคุณถามโดยเฉพาะ ) อาจได้รับการประกาศในลักษณะนี้ว่าเป็นหลักการเขียนโค้ดแบบสำนวน / เฮาส์ที่ทีมพัฒนา JDK ได้รับคำสั่งให้ปฏิบัติตามด้วยเหตุผลของความสอดคล้องกันตลอดทั้ง API


รุ่นที่ยืดยาว

“ … แต่ฉันไม่เข้าใจส่วนนี้: Function<? super T, ? extends U>… “

ส่วนนั้นกำลังวางข้อ จำกัดเกี่ยวกับประเภทเฉพาะที่ต้องส่งคืน ดูเหมือนว่าคุณจะทำส่วนนั้นลงไปแล้วFunction

ผลตอบแทนที่ไม่ได้เป็นเพียงเก่า ๆอย่างไร มันจะต้องมีคุณสมบัติเฉพาะ ( อาคา“ขอบเขต” ) ประกาศในส่วนของพารามิเตอร์วิธีการ:UFunctionU<U extends Comparable<? super U>>

“ …ทำไมไม่มีแค่: Function<? super T, U>… “

เพื่อให้ง่ายที่สุดเท่าที่จะทำได้ ( เพราะฉันคิดแค่นั้นเฉยๆเมื่อเทียบกับแบบเป็นทางการ ): เหตุผลก็เพราะว่าUไม่ใช่ประเภทเดียวกับ? extends U .

การเปลี่ยนComparable< ? super U >ไปใช้List< ? super U >และComparator< T >เพื่อSet< T >อาจทำให้ความสงสัยของคุณง่ายขึ้นในการหาเหตุผล ...

default < U extends List< ? super U > > Set< T > thenComparing(
    Function< ? super T, ? extends U > keyExtractor ) {
        
    T input = …;
        
    /* Intuitively, you'd think this would be compliant; it's not! */
    /* List< ? extends U > wtf = keyExtractor.apply( input ); */
      
    /* This doesn't comply to „U extends List< ? super U >“ either */
    /* ArrayList< ? super U > key = keyExtractor.apply( input ); */
        
    /* This is compliant because key is a „List extends List< ? super U >“
     * like the method declaration requires of U 
     */
    List< ? super U > key = keyExtractor.apply( input );
        
    /* This is compliant because List< E > is a subtype of Collection< E > */
    Collection< ? super U > superKey = key;
        
    …
}

“ ไม่สามารถกำหนดUพารามิเตอร์ให้เป็นkeyExtractorผลตอบแทนอะไรก็ได้และยังขยายComparable<? super U>เหมือนเดิมทั้งหมดได้หรือไม่… “

ฉันได้ทำการทดลองแล้วว่าFunction< ? super T, ? extends U > keyExtractorสามารถปรับโครงสร้างให้เข้ากับสิ่งที่ จำกัด มากขึ้น Function< ? super T, U > keyExtractorและยังคงรวบรวมและทำงานได้ดีอย่างสมบูรณ์แบบ ตัวอย่างเช่นแสดงความคิดเห็น / ไม่แสดงความคิดเห็น/*? extends*/ในบรรทัดที่ 27 ของการทดลองของฉันUnboundedComparatorเพื่อสังเกตว่าการโทรทั้งหมดนี้ประสบความสำเร็จไม่ว่าจะด้วยวิธีใดก็ตาม ...

…
Function< Object, A > aExtractor = ( obj )-> new B( );
Function< Object, B > bExtractor = ( obj )-> new B( ) ;
Function< Object, C > cExtractor = ( obj )-> new C( ) ;
        
UnboundedComparator.< Object, A >comparing( aExtractor ).thenComparing( bExtractor );
UnboundedComparator.< Object, A >comparing( bExtractor ).thenComparing( aExtractor );
UnboundedComparator.< Object, A >comparing( bExtractor ).thenComparing( bExtractor );
UnboundedComparator.< Object, B >comparing( bExtractor ).thenComparing( bExtractor );
UnboundedComparator.< Object, B >comparing( bExtractor ).thenComparing( aExtractor );
UnboundedComparator.< Object, B >comparing( bExtractor ).thenComparing( cExtractor );
…

เทคนิคคุณสามารถทำเทียบเท่าdeboundingในรหัสจริง จากการทดลองที่เรียบง่ายที่ฉันได้ทำ - บนthenComparing()โดยเฉพาะเนื่องจากว่าเป็นสิ่งที่คำถามของคุณเกี่ยวกับการถาม - ฉันไม่สามารถหาเหตุผลในทางปฏิบัติใด ๆ จะชอบมากกว่า? extends UU

?แต่แน่นอนผมยังไม่ได้รับการทดสอบอย่างละเอียดถี่ถ้วนกรณีใช้ทุกวิธีการที่มีและไม่มีขอบเขต

ฉันจะแปลกใจถ้านักพัฒนาของ JDKไม่ได้ทดสอบอย่างละเอียดถี่ถ้วน

การทดลองของฉัน -จำกัด ฉันยอมรับ - ทำให้ฉันเชื่อว่าอาจถูกประกาศในลักษณะนั้นโดยไม่มีเหตุผลอื่นใดนอกจากการเขียนโค้ดแบบสำนวน / บ้านที่ทีมพัฒนา JDK ปฏิบัติตามComparator.thenComparing(Function< ? super T, ? extends U > keyExtractor)

มองไปที่ฐานรหัสของ JDKก็ไม่มีเหตุผลที่จะเข้าใจว่าที่ใดที่หนึ่งใครบางคนได้มีคำสั่ง: « เมื่อใดก็ตามที่มีต้องมีขีด จำกัด ล่างFunction< T, R >T ( ผู้บริโภค / สิ่งที่คุณป้อนข้อมูล ) และR จะต้องมีขอบเขตบน ( ผู้ผลิต / คุณจะได้รับ สิ่งที่ส่งคืนให้คุณ ) ».

สำหรับเหตุผลที่ชัดเจน แต่ไม่ได้เช่นเดียวกับU ? extends Uดังนั้นไม่ควรคาดหวังว่าอดีตจะสามารถทดแทนได้ในภายหลัง

การประยุกต์ใช้สาธารณรัฐโคลัมเบีย :มันเป็นเรื่องง่ายที่จะคาดหวังว่าการทดสอบหมดจดพัฒนาระบบของ JDK ได้ทำได้จัดตั้งว่าUสัญลักษณ์แทน bounded -upper เป็นสิ่งที่จำเป็นเพื่อให้ครอบคลุมจำนวนกว้างของกรณีการใช้งาน

1 MatthewS. Sep 26 2020 at 15:25

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

อันดับแรกเราควรทราบว่าไวด์การ์ดชนิดที่กำหนดพารามิเตอร์ไม่สามารถเข้าถึงสมาชิกที่เป็นพารามิเตอร์ประเภทที่เกี่ยวข้องได้ นี่คือเหตุผลที่ในกรณีเฉพาะของคุณ? extends Uสามารถใช้ทดแทนUและยังคงทำงานได้ดี

วิธีนี้ใช้ไม่ได้ในทุกกรณี อาร์กิวเมนต์ประเภทUไม่มีความเก่งกาจและความปลอดภัยประเภทเพิ่มเติมที่? extends Uมี สัญลักษณ์แทนเป็นอาร์กิวเมนต์ประเภทที่ไม่ซ้ำกันซึ่งการสร้างอินสแตนซ์ของชนิดที่กำหนดพารามิเตอร์ (ที่มีอาร์กิวเมนต์ประเภทสัญลักษณ์แทน) จะไม่ถูก จำกัด โดยอาร์กิวเมนต์ type เนื่องจากอาร์กิวเมนต์ type เป็นพารามิเตอร์ประเภทคอนกรีตหรือประเภท สัญลักษณ์แทนเป็นตัวยึดที่มีลักษณะทั่วไปมากกว่าพารามิเตอร์ประเภทและประเภทคอนกรีต (เมื่อใช้เป็นอาร์กิวเมนต์ประเภท) ประโยคแรกในบทช่วยสอน java เกี่ยวกับไวลด์การ์ดอ่าน:

ในรหัสทั่วไปเครื่องหมายคำถาม (?) เรียกว่าสัญลักษณ์แทนหมายถึงประเภทที่ไม่รู้จัก

เพื่อเป็นตัวอย่างให้ดูที่จุดนี้

class A <T> {}

ตอนนี้เรามาประกาศคลาสนี้สองครั้งแบบหนึ่งเป็นแบบคอนกรีตและอีกแบบมีไวด์การ์ดจากนั้นเราจะสร้างอินสแตนซ์

A <Number> aConcrete = new A <Integer>(); // Compile time error
A <? extends Number> aWild = new A<Integer>() // Works fine

ดังนั้นจึงควรแสดงให้เห็นว่าอาร์กิวเมนต์ประเภทสัญลักษณ์ตัวแทนไม่ จำกัด การสร้างอินสแตนซ์มากเท่ากับชนิดที่เป็นรูปธรรม แล้วพารามิเตอร์ประเภทล่ะ? ปัญหาเกี่ยวกับการใช้พารามิเตอร์ประเภทปรากฏได้ดีที่สุดในวิธีการ เพื่อเป็นตัวอย่างการตรวจสอบชั้นเรียนนี้:

class C <U> {
    void parameterMethod(A<U> a) {}
    void wildMethod(A<? extends U> a) {}
    void test() {
        C <Number> c = new C();
        A<Integer> a = new A();
        c.parameterMethod(a); // Compile time error
        c.wildMethod(a); // Works fine
    }

สังเกตว่าการอ้างอิงcและaประเภทคอนกรีตเป็นอย่างไร ตอนนี้สิ่งนี้ได้รับการแก้ไขแล้วในคำตอบอื่น แต่สิ่งที่ไม่ได้ระบุไว้ในคำตอบอื่นคือแนวคิดของอาร์กิวเมนต์ประเภทเกี่ยวข้องกับข้อผิดพลาดเวลาคอมไพล์อย่างไร (เหตุใดอาร์กิวเมนต์ประเภทหนึ่งจึงทำให้เกิดข้อผิดพลาดเวลาคอมไพล์และอีกข้อไม่ได้) และสิ่งนี้ ความสัมพันธ์คือสาเหตุที่การประกาศที่เป็นปัญหาถูกประกาศด้วยไวยากรณ์ที่ประกาศด้วย และความสัมพันธ์นั้นคือสัญลักษณ์แทนความปลอดภัยและความสามารถรอบด้านเพิ่มเติมที่ให้พารามิเตอร์ประเภทมากกว่าและไม่ใช่รูปแบบการพิมพ์บางอย่าง ตอนนี้เพื่อแสดงให้เห็นถึงจุดนี้เราจะต้องให้Aสมาชิกประเภทพารามิเตอร์ดังนั้น:

class A<T> { T something; }

อันตรายจากการใช้พารามิเตอร์ type ใน parameterMethod () คือพารามิเตอร์ type สามารถอ้างถึงในรูปแบบของ cast ซึ่งทำให้สามารถเข้าถึงsomethingสมาชิกได้

class C<U> {
    parameterMethod(A<U> a) { a.something = (U) "Hi"; }
}

ซึ่งจะช่วยให้เกิดมลพิษจากกองขยะ ด้วยการใช้พารามิเตอร์วิธีนี้คำสั่งC<Number> c = new C();ในวิธีการทดสอบ () อาจทำให้เกิดมลพิษจากกอง ด้วยเหตุนี้คอมไพเลอร์จึงออกข้อผิดพลาดเวลาคอมไพล์เมื่อเมธอดที่มีอาร์กิวเมนต์ของพารามิเตอร์ type ถูกส่งผ่านอ็อบเจ็กต์ใด ๆ โดยไม่มี cast จากภายในพารามิเตอร์ type ที่ประกาศคลาส สมาชิกของพารามิเตอร์ type ที่เท่าเทียมกันจะแสดงข้อผิดพลาดเวลาคอมไพล์หากอินสแตนซ์ไปยัง Object ใด ๆ โดยไม่ต้องร่ายจากภายในคลาสการประกาศของพารามิเตอร์ type สิ่งที่สำคัญมากในการเน้นย้ำคือการไม่มีการร่ายเพราะคุณยังสามารถส่งผ่านวัตถุไปยังเมธอดที่มีอาร์กิวเมนต์ประเภทพารามิเตอร์ได้ แต่จะต้องส่งไปยังพารามิเตอร์ประเภทนั้น (หรือในกรณีนี้ให้ส่งไปยังประเภทที่มีพารามิเตอร์ type) . ในตัวอย่างของฉัน

    void test() {
        C <Number> c = new C();
        A<Integer> a = new A();
        c.parameterMethod(a); // Compile time error
        c.wildMethod(a); // Works fine
    }

c.parameterMethod(a)จะทำงานถ้าaถูกโยนไปA<U>ดังนั้นหากเส้นมองเช่นนี้c.parameterMethod((A<U>) a);ข้อผิดพลาดไม่มีเวลารวบรวมจะเกิดขึ้น แต่คุณจะได้รับข้อผิดพลาด castclassexection เวลาทำงานถ้าคุณพยายามที่จะตั้งค่าintตัวแปรเท่ากับa.somethingหลังจากที่parameterMethod()เรียกว่า (และอีกครั้งคอมไพเลอร์ ต้องใช้นักแสดงเพราะUสามารถแสดงถึงอะไรก็ได้) สถานการณ์ทั้งหมดนี้จะมีลักษณะดังนี้:

    void test() {
        C <Number> c = new C();
        A<Integer> a = new A();
        c.parameterMethod((A<U>) a); // No compile time error cuz of cast
        int x = a.something; // doesn't issue compile time error and will cause run-time ClassCastException error
    }

ดังนั้นเนื่องจากพารามิเตอร์ type สามารถอ้างอิงได้ในรูปแบบของ cast จึงเป็นการผิดกฎหมายที่จะส่งผ่านอ็อบเจ็กต์จากภายในพารามิเตอร์ type ที่ประกาศคลาสไปยังเมธอดที่มีอาร์กิวเมนต์ของพารามิเตอร์ type หรือมีพารามิเตอร์ type ไม่สามารถอ้างอิงตัวแทนในรูปแบบของนักแสดงดังนั้นain wildMethod(A<? extends U> a)จึงไม่สามารถเข้าถึงสมาชิก T ของ A ได้ เนื่องจากความปลอดภัยประเภทเพิ่มเติมนี้เนื่องจากความเป็นไปได้ของมลพิษจากกองจะถูกหลีกเลี่ยงด้วยสัญลักษณ์ตัวแทนคอมไพเลอร์ java จึงอนุญาตให้ส่งประเภทคอนกรีตไปยัง wildMethod โดยไม่ต้องร่ายเมื่อเรียกใช้โดยการอ้างอิง c ในC<Number> c = new C(); นี่คือเหตุผลที่ไวด์การ์ดชนิดที่กำหนดพารามิเตอร์สามารถสร้างอินสแตนซ์ให้กับคอนกรีตได้โดยไม่ต้องหล่อ เมื่อฉันพูดถึงความเก่งกาจของอาร์กิวเมนต์ประเภทฉันกำลังพูดถึงการโต้ตอบที่พวกเขาอนุญาตในบทบาทของประเภทพารามิเตอร์ และเมื่อฉันพูดถึงความปลอดภัยประเภทเพิ่มเติมฉันกำลังพูดถึงการไม่สามารถอ้างอิงสัญลักษณ์แทนในรูปแบบของการร่ายที่หลีกเลี่ยง heapPollution

ฉันไม่รู้ว่าทำไมใครบางคนถึงใช้พารามิเตอร์ประเภท แต่ฉันรู้ว่าอย่างน้อยนักพัฒนาจะชอบความเก่งกาจของอักขระตัวแทนเทียบกับพารามิเตอร์ประเภท ฉันอาจเขียนคำถามนี้อย่างสับสนหรืออาจเข้าใจผิดคำถามของคุณดูเหมือนว่าฉันจะเกี่ยวกับข้อโต้แย้งประเภททั่วไปแทนที่จะเป็นคำประกาศเฉพาะนี้ นอกจากนี้หากมีการใช้ keyExtractor จากการประกาศFunction<? super T, ? extends U> keyExtractorในลักษณะที่สมาชิกที่เป็นสมาชิกFunctionของพารามิเตอร์ประเภทที่สองจะไม่ถูกเข้าถึงอีกครั้งสัญลักษณ์แทนจะเหมาะอย่างยิ่งเพราะพวกเขาไม่สามารถเข้าถึงสมาชิกเหล่านั้นได้ เหตุใดนักพัฒนาจึงไม่ต้องการความเก่งกาจที่กล่าวถึงที่นี่ซึ่งสัญลักษณ์แทนมีให้? เป็นเพียงผลประโยชน์เท่านั้น