Java แล้วเปรียบเทียบลายเซ็นตัวแทน
เหตุใดคำประกาศจึงมีลักษณะเช่นนี้:
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>เหมือนเดิมทั้งหมดได้หรือไม่?
คำตอบ
ทำไมถึงเป็น? 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>) {}
อย่างที่คุณเห็น
strict()ทำงานได้ดีโดยไม่มีการประกาศอย่างชัดเจนเนื่องจากTกำลังอนุมานว่าIntegerตรงกับประเภททั่วไปของตัวแปรในเครื่องจากนั้นก็จะหยุดพักเมื่อเราพยายามที่จะผ่าน
Supplier<Integer>เป็นSupplier<Number>เพราะIntegerและNumberไม่ได้เข้ากันได้และจากนั้นก็ทำงานร่วมกับ
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>
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 เป็นสิ่งที่จำเป็นเพื่อให้ครอบคลุมจำนวนกว้างของกรณีการใช้งาน
ดูเหมือนว่าคำถามของคุณจะเกี่ยวกับประเภทอาร์กิวเมนต์โดยทั่วไปดังนั้นสำหรับคำตอบของฉันฉันจะแยกอาร์กิวเมนต์ประเภทที่คุณระบุจากประเภทที่เป็นของในคำตอบของฉันเพื่อความเรียบง่าย
อันดับแรกเราควรทราบว่าไวด์การ์ดชนิดที่กำหนดพารามิเตอร์ไม่สามารถเข้าถึงสมาชิกที่เป็นพารามิเตอร์ประเภทที่เกี่ยวข้องได้ นี่คือเหตุผลที่ในกรณีเฉพาะของคุณ? 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ของพารามิเตอร์ประเภทที่สองจะไม่ถูกเข้าถึงอีกครั้งสัญลักษณ์แทนจะเหมาะอย่างยิ่งเพราะพวกเขาไม่สามารถเข้าถึงสมาชิกเหล่านั้นได้ เหตุใดนักพัฒนาจึงไม่ต้องการความเก่งกาจที่กล่าวถึงที่นี่ซึ่งสัญลักษณ์แทนมีให้? เป็นเพียงผลประโยชน์เท่านั้น