Java generics ต้องการ <เมื่อใด ขยาย T> แทน <T> และมีข้อเสียของการสลับ?


205

รับตัวอย่างต่อไปนี้ (ใช้ JUnit กับ Hamcrest matchers):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

สิ่งนี้ไม่ได้คอมไพล์ด้วยassertThatลายเซ็นเมธอดJUnit ของ:

public static <T> void assertThat(T actual, Matcher<T> matcher)

ข้อความแสดงข้อผิดพลาดของคอมไพเลอร์คือ:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

อย่างไรก็ตามถ้าฉันเปลี่ยนassertThatลายเซ็นวิธีเป็น:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

จากนั้นการรวบรวมก็ใช้งานได้

ดังนั้นสามคำถาม:

  1. ทำไมเวอร์ชั่นปัจจุบันจึงไม่คอมไพล์? แม้ว่าฉันจะเข้าใจปัญหาความแปรปรวนร่วมที่นี่ฉันไม่สามารถอธิบายได้อย่างแน่นอนถ้าฉันต้องทำ
  2. มีข้อเสียในการเปลี่ยนassertThatวิธีการMatcher<? extends T>หรือไม่ มีกรณีอื่นอีกไหมที่จะทำให้แตกถ้าคุณทำอย่างนั้น?
  3. มีจุดใดที่ทำให้assertThatวิธีการทั่วไปใน JUnit? Matcherระดับดูเหมือนจะไม่จำเป็นต้องใช้มันตั้งแต่ JUnit สายตรงวิธีการซึ่งไม่ได้ถูกพิมพ์โดยทั่วไปและเพียงแค่รูปลักษณ์ใด ๆ เช่นความพยายามที่จะบังคับให้มีความปลอดภัยประเภทที่ไม่ได้ทำอะไรเป็นMatcherจะเพียงแค่ไม่ได้อยู่ในความเป็นจริง จับคู่และการทดสอบจะล้มเหลวโดยไม่คำนึงถึง ไม่มีการดำเนินการที่ไม่ปลอดภัย (หรือดูเหมือนว่า)

สำหรับการอ้างอิงนี่คือการใช้ JUnit ของassertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}

นี่คือลิงค์มีประโยชน์มาก (ยาชื่อสามัญ, การสืบทอดและชนิดย่อย): docs.oracle.com/javase/tutorial/java/generics/inheritance.html
Dariush Jafari

คำตอบ:


145

ก่อนอื่น - ฉันต้องนำคุณไปที่http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html - เธอทำงานได้อย่างน่าอัศจรรย์

แนวคิดพื้นฐานคือคุณใช้

<T extends SomeClass>

เมื่อพารามิเตอร์ที่เกิดขึ้นจริงสามารถเป็นSomeClassหรือชนิดย่อยของมัน

ในตัวอย่างของคุณ

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

คุณกำลังจะบอกว่าexpectedสามารถมีวัตถุที่เป็นตัวแทนของระดับชั้นใด ๆ Serializableที่นำไปปฏิบัติ แผนที่ผลลัพธ์ของคุณบอกว่ามันสามารถเก็บDateวัตถุคลาสได้เท่านั้น

เมื่อคุณผ่านผลคุณตั้งค่ากำลังTให้ตรงMapของStringการDateวัตถุชั้นซึ่งไม่ตรงกับMapของอะไรว่าStringSerializable

สิ่งหนึ่งที่จะตรวจสอบ - คุณแน่ใจหรือว่าคุณต้องการClass<Date>และไม่ได้Date? แผนที่ของStringถึงClass<Date>ไม่มีประโยชน์อย่างมากในเรื่องทั่วไป (สิ่งที่สามารถเก็บได้คือDate.classค่าแทนที่จะเป็นของDate)

สำหรับการassertThatทำให้เป็นเรื่องทั่วไปความคิดก็คือวิธีการนั้นสามารถทำให้มั่นใจได้ว่าประเภทMatcherที่เหมาะกับประเภทผลลัพธ์นั้นผ่านไปแล้ว


ในกรณีนี้ใช่ฉันต้องการแผนที่ชั้นเรียน ตัวอย่างที่ฉันมอบให้มีการวางแผนให้ใช้คลาส JDK มาตรฐานแทนคลาสที่กำหนดเองของฉัน แต่ในกรณีนี้คลาสนั้นถูกสร้างอินสแตนซ์ผ่านการสะท้อนกลับและใช้ตามคีย์ (แอพแบบกระจายที่ลูกค้าไม่ได้มีคลาสเซิร์ฟเวอร์เพียงคีย์ของคลาสที่จะใช้เพื่อทำงานด้านเซิร์ฟเวอร์)
Yishai

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

ในการยืนยันที่ทำให้แน่ใจว่านักแสดงจะดำเนินการให้คุณวิธี matcher.matches () ไม่สนใจดังนั้นเนื่องจาก T ไม่เคยใช้ทำไมเกี่ยวข้องกับมัน (ชนิดวิธีการส่งกลับเป็นโมฆะ)
Yishai

Ahhh - นั่นคือสิ่งที่ฉันได้รับสำหรับการไม่อ่าน def ของการยืนยันที่ใกล้พอ ดูเหมือนว่ามันเป็นเพียงเพื่อให้แน่ใจว่าเหมาะสม Matcher ถูกส่งผ่านไปใน ...
สกอตต์ Stanchfield

28

ขอบคุณทุกคนที่ตอบคำถามมันช่วยชี้แจงให้ฉันจริงๆ ในท้ายที่สุดคำตอบของ Scott Stanchfield นั้นใกล้เคียงกับที่ฉันเข้าใจมากที่สุด แต่เนื่องจากฉันไม่เข้าใจเขาเมื่อเขาเขียนมันขึ้นมาครั้งแรกฉันจึงพยายามที่จะแก้ไขปัญหาเพื่อหวังว่าคนอื่นจะได้รับประโยชน์

ฉันจะทบทวนคำถามในแง่ของรายการเนื่องจากมีเพียงพารามิเตอร์ทั่วไปเพียงตัวเดียวและจะทำให้เข้าใจง่ายขึ้น

วัตถุประสงค์ของคลาส parametrized (เช่น List <Date>หรือ Map <K, V>ดังในตัวอย่าง) คือการบังคับให้ downcastและเพื่อให้คอมไพเลอร์รับประกันว่าสิ่งนี้ปลอดภัย (ไม่มีข้อยกเว้น runtime)

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

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

สิ่งนี้จะไม่คอมไพล์เนื่องจากพารามิเตอร์ list เป็นรายการของวันที่ไม่ใช่รายการของสตริง ยาสามัญจะไม่เป็นประโยชน์อย่างมากถ้ามันรวบรวม

สิ่งเดียวกันที่ใช้กับแผนที่มันไม่ได้เป็นสิ่งเดียวกับแผนที่<String, Class<? extends Serializable>> <String, Class<java.util.Date>>พวกมันไม่ใช่ covariant ดังนั้นถ้าฉันต้องการเอาค่าจากแผนที่ที่มีคลาสของวันที่และใส่ลงในแผนที่ที่มีองค์ประกอบที่ต่อเนื่องกันได้นั่นก็ดี แต่ลายเซ็นต์ของวิธีที่บอกว่า:

private <T> void genericAdd(T value, List<T> list)

ต้องการทำทั้งสองอย่าง:

T x = list.get(0);

และ

list.add(value);

ในกรณีนี้แม้ว่าวิธี Junit จะไม่สนใจสิ่งเหล่านี้ แต่ลายเซ็นของวิธีนั้นต้องการความแปรปรวนร่วมซึ่งไม่ได้รับดังนั้นจึงไม่ได้รวบรวม

ในคำถามที่สอง

Matcher<? extends T>

จะมีข้อเสียของการยอมรับอะไรจริงๆเมื่อ T เป็นวัตถุซึ่งไม่ได้เป็นความตั้งใจของ API จุดประสงค์คือเพื่อให้แน่ใจว่าผู้จับคู่ตรงกับวัตถุจริงและไม่มีวิธีที่จะแยกวัตถุออกจากการคำนวณนั้น

คำตอบสำหรับคำถามที่สามคือไม่มีสิ่งใดหายไปในแง่ของการทำงานที่ไม่ได้ตรวจสอบ (จะไม่มี typecasting ที่ไม่ปลอดภัยภายใน JUnit API หากวิธีนี้ไม่ได้เป็นแบบทั่วไป) แต่พวกเขาพยายามที่จะทำสิ่งอื่น - ให้แน่ใจว่า พารามิเตอร์สองตัวน่าจะตรงกัน

แก้ไข (หลังจากการไตร่ตรองและประสบการณ์เพิ่มเติม):

หนึ่งในปัญหาใหญ่ที่มีลายเซ็นเมธอด assert นั่นคือความพยายามที่จะทำให้เท่าเทียมกันตัวแปร T กับพารามิเตอร์ทั่วไปของ T ที่ไม่ทำงานเพราะพวกเขาไม่ใช่ covariant ตัวอย่างเช่นคุณอาจมี T ซึ่งเป็นList<String>แต่แล้วผ่านการจับคู่ที่คอมไพเลอร์ใช้งานMatcher<ArrayList<T>>ได้ ตอนนี้ถ้ามันไม่ใช่พารามิเตอร์ประเภทสิ่งต่าง ๆ น่าจะดีเพราะ List และ ArrayList เป็น covariant แต่เนื่องจาก Generics เท่าที่คอมไพเลอร์เกี่ยวข้องนั้นต้องใช้ ArrayList มันไม่สามารถทน List ได้ด้วยเหตุผลที่ฉันหวังว่าชัดเจน จากด้านบน


ฉันยังไม่เข้าใจว่าทำไมฉันถึงไม่สามารถ upcast ได้ ทำไมฉันไม่สามารถเปลี่ยนรายชื่อวันที่ให้เป็นรายการที่สามารถทำให้เป็นอนุกรมได้
โทมัส Ahle

@ThomasAhle เนื่องจากการอ้างอิงที่คิดว่าเป็นรายการวันที่จะพบข้อผิดพลาดในการส่งเมื่อพบ Strings หรือ Serializables อื่น ๆ
Yishai

ฉันเห็น แต่จะเกิดอะไรขึ้นถ้าฉันได้ลบข้อมูลอ้างอิงเก่าออกไปราวกับว่าฉันคืนค่าList<Date>จากวิธีที่เป็นประเภทList<Object>? นั่นควรจะปลอดภัยถูกต้องแม้ว่าจะไม่ได้รับอนุญาตจาก java
โทมัส Ahle

14

มันเดือดลงไปที่:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

คุณสามารถดูการอ้างอิงคลาส c1 อาจมีอินสแตนซ์แบบยาว (เนื่องจากวัตถุต้นแบบ ณ เวลาที่กำหนดอาจเป็นไปได้List<Long>) แต่ไม่สามารถส่งไปยังวันที่ได้เนื่องจากไม่มีการรับประกันว่าคลาส "ไม่ทราบ" เป็นวันที่ คอมไพเลอร์ไม่อนุญาต

อย่างไรก็ตามหากเราแนะนำวัตถุอื่น ๆ ให้พูดรายการ (ในตัวอย่างของคุณวัตถุนี้คือ Matcher) ดังนั้นสิ่งต่อไปนี้จะกลายเป็นจริง:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

... อย่างไรก็ตามหากประเภทของรายการกลายเป็น ขยาย T แทน T ....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

ฉันคิดว่าการเปลี่ยนMatcher<T> to Matcher<? extends T>คุณจะแนะนำสถานการณ์คล้ายกับการกำหนด l1 = l2;

มันยังคงสับสนอยู่มากเมื่อมี wildcards ที่ซ้อนกันอยู่ แต่หวังว่านั่นจะเป็นเหตุผลว่าทำไมมันถึงช่วยให้เข้าใจ generics โดยดูที่วิธีที่คุณสามารถมอบหมายการอ้างอิงทั่วไปให้กันและกัน นอกจากนี้ยังมีความสับสนเพิ่มเติมเนื่องจากคอมไพเลอร์อนุมานชนิดของ T เมื่อคุณทำการเรียกใช้ฟังก์ชัน (คุณไม่ได้บอกอย่างชัดเจนว่าเป็น T คือ)


9

เหตุผลที่รหัสดั้งเดิมของคุณไม่ได้รวบรวมนั้น<? extends Serializable>ไม่ได้หมายความว่า "คลาสใด ๆ ที่ขยาย Serializable ได้" แต่ "คลาสที่ไม่รู้จัก แต่เฉพาะบางคลาสที่ขยาย Serializable"

ตัวอย่างเช่นกำหนดรหัสเป็นเขียนเป็นอย่างดีที่ถูกต้องในการกำหนดที่จะnew TreeMap<String, Long.class>()> expectedหากคอมไพเลอร์อนุญาตให้โค้ดคอมไพล์แล้วassertThat()นั้นน่าจะแตกเพราะมันคาดหวังว่าDateอ็อบเจกต์แทนที่จะเป็นอLongอบเจ็กต์ที่พบในแผนที่


1
ฉันไม่ค่อยได้ติดตาม - เมื่อคุณพูดว่า "ไม่ได้หมายความว่า ... แต่ ... ": ความแตกต่างคืออะไร? (เช่นเดียวกับสิ่งที่จะเป็นตัวอย่างของการ "รู้จักกัน แต่ไม่เฉพาะเจาะจง" ชั้นที่เหมาะกับความหมายในอดีต แต่ไม่หลังแล้ว?)
poundifdef

ใช่ว่าเป็นเรื่องที่น่าอึดอัดใจเล็กน้อย ไม่แน่ใจว่าจะแสดงมันออกมาดีกว่า ... มันสมเหตุสมผลกว่าหรือที่จะพูดว่า "'?' ประเภทที่ไม่รู้จักไม่ใช่ประเภทที่ตรงกับอะไรเลยหรือ "
erickson

1
สิ่งที่อาจช่วยอธิบายได้คือสำหรับ "คลาสใด ๆ ที่ขยาย Serializable" คุณสามารถใช้<Serializable>
c0der

8

วิธีหนึ่งที่ฉันจะเข้าใจ wildcard คือคิดว่า wildcard ไม่ได้ระบุประเภทของวัตถุที่เป็นไปได้ที่ให้การอ้างอิงทั่วไปสามารถ "มี" แต่ประเภทของการอ้างอิงทั่วไปอื่น ๆ ที่เข้ากันได้ (อาจฟังดูสับสน ... ) เช่นนี้คำตอบแรกทำให้เข้าใจผิดมากเกี่ยวกับถ้อยคำ

กล่าวอีกนัยหนึ่งList<? extends Serializable>หมายความว่าคุณสามารถกำหนดการอ้างอิงนั้นไปยังรายการอื่นโดยที่ประเภทนั้นเป็นประเภทที่ไม่รู้จักซึ่งเป็นประเภทย่อยหรือเป็นอนุกรมย่อย อย่าคิดในแง่ของรายการเดียวที่สามารถถือคลาสย่อยของ Serializable ได้ (เพราะนั่นเป็นความหมายที่ไม่ถูกต้องและนำไปสู่การเข้าใจผิดของ Generics)


แน่นอนว่าช่วยได้ แต่ "อาจฟังดูสับสน" นั้นถูกแทนที่ด้วย "ฟังดูสับสน" ในฐานะที่เป็นผู้ติดตามดังนั้นทำไมตามคำอธิบายนี้วิธีการที่มีการ<? extends T>รวบรวมMatcher ?
Yishai

ถ้าเรานิยามเป็น List <Serializable> มันทำในสิ่งเดียวกันใช่มั้ย ฉันกำลังพูดถึงย่อหน้าที่สองของคุณ เช่นความหลากหลายจะจัดการกับมันได้หรือไม่
Supun Wijerathne

3

ฉันรู้ว่านี่เป็นคำถามเก่า แต่ฉันต้องการแบ่งปันตัวอย่างที่ฉันคิดว่าอธิบายไวด์การ์ดได้ดี java.util.Collectionsเสนอวิธีการนี้:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

หากเรามี List of T, แน่นอนว่า List สามารถมีอินสแตนซ์ของประเภทที่ขยายTได้ หากรายการมีสัตว์รายการจะมีทั้งสุนัขและแมว (สัตว์ทั้งสอง) สุนัขมีคุณสมบัติ "woofVolume" และ Cats มีคุณสมบัติ "meowVolume" แม้ว่าเราอาจต้องการเรียงลำดับตามคุณสมบัติเหล่านี้โดยเฉพาะกับคลาสย่อยของTเราจะคาดหวังได้อย่างไรว่าวิธีนี้จะทำเช่นนั้น ข้อ จำกัด ของ Comparator คือสามารถเปรียบเทียบสองสิ่งที่เป็นประเภทเดียวเท่านั้น ( T) ดังนั้นต้องการเพียงแค่Comparator<T>จะทำให้วิธีการนี้ใช้งานได้ แต่ผู้สร้างของวิธีการนี้ได้รับการยอมรับว่าถ้าสิ่งที่เป็นTแล้วมันยังเป็นตัวอย่างของ superclasses Tของ ดังนั้นเขาช่วยให้เราสามารถใช้เปรียบเทียบของTหรือ superclass ใดคือT? super T


1

เกิดอะไรขึ้นถ้าคุณใช้

Map<String, ? extends Class<? extends Serializable>> expected = null;

ใช่นี่คือสิ่งที่คำตอบข้างต้นของฉันขับรถไป
GreenieMeanie

ไม่นั่นไม่ได้ช่วยสถานการณ์อย่างน้อยวิธีที่ฉันลอง
Yishai
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.