ประเภทผลตอบแทนทั่วไปที่ถูกผูกไว้ด้านบน - ส่วนต่อประสานกับชั้นเรียน - รหัสที่ถูกต้องน่าแปลกใจ


171

นี่คือตัวอย่างในโลกแห่งความเป็นจริงจาก API ไลบรารีบุคคลที่ 3 แต่ง่ายขึ้น

คอมไพล์ด้วย Oracle JDK 8u72

พิจารณาทั้งสองวิธี:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

ทั้งคู่รายงานคำเตือน "นักแสดงที่ไม่ได้ตรวจสอบ" - ฉันเข้าใจว่าทำไม สิ่งที่ทำให้ฉันงุนงงคือทำไมฉันถึงโทรได้

Integer x = getCharSequence();

และมันรวบรวม? คอมไพเลอร์ควรจะรู้ว่าไม่ใช้Integer CharSequenceการโทรไป

Integer y = getString();

ให้ข้อผิดพลาด (ตามที่คาดไว้)

incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

มีคนอธิบายได้ไหมว่าเหตุใดพฤติกรรมนี้จึงถูกต้อง มันจะมีประโยชน์อย่างไร

ลูกค้าไม่ทราบว่าการโทรนี้ไม่ปลอดภัย - รหัสของลูกค้ารวบรวมโดยไม่มีการเตือน ทำไมคอมไพล์จึงไม่เตือนว่าเกิดข้อผิดพลาด?

นอกจากนี้มันแตกต่างจากตัวอย่างนี้อย่างไร:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

การพยายามส่งผ่านList<Integer>มีข้อผิดพลาดตามที่คาดไว้:

method doCharSequence in class generic.GenericTest cannot be applied to given types;
  required: java.util.List<X>
  found: java.util.List<java.lang.Integer>
  reason: inference variable X has incompatible bounds
    equality constraints: java.lang.Integer
    upper bounds: java.lang.CharSequence

หากรายงานว่าเป็นข้อผิดพลาดทำไมถึงInteger x = getCharSequence();ไม่ใช่


15
! ที่น่าสนใจ การคัดเลือกนักแสดงที่ LHS Integer x = getCharSequence();จะรวบรวม แต่การคัดเลือกนักแสดงที่ RHS Integer x = (Integer) getCharSequence();ไม่สามารถรวบรวมได้
สะเก็ด

คุณกำลังใช้คอมไพเลอร์ java เวอร์ชันใดอยู่? โปรดระบุข้อมูลนี้ในคำถาม
Federico Peralta Schaffner

@FedericoPeraltaSchaffner ไม่สามารถดูสาเหตุที่สำคัญ - นี่เป็นคำถามโดยตรงเกี่ยวกับ JLS
Boris the Spider

@BoristheSpider เนื่องจากกลไกการอนุมานแบบมีการเปลี่ยนแปลงสำหรับ java8
Federico Peralta Schaffner

1
@FedericoPeraltaSchaffner - ฉันติดแท็กคำถามแล้วด้วย [java-8] แต่ฉันได้เพิ่มเวอร์ชั่นคอมไพเลอร์ในโพสต์ทันที
Adam Michalik

คำตอบ:


184

CharSequenceinterfaceเป็น ดังนั้นแม้ว่าSomeClassจะไม่ได้ใช้CharSequenceมันจะเป็นไปได้อย่างสมบูรณ์แบบในการสร้างชั้นเรียน

class SubClass extends SomeClass implements CharSequence

ดังนั้นคุณสามารถเขียน

SomeClass c = getCharSequence();

เพราะประเภทที่สรุปเป็นประเภทสี่แยกXSomeClass & CharSequence

นี่เป็นเรื่องแปลกในกรณีของIntegerเพราะIntegerเป็นที่สิ้นสุด แต่finalไม่มีบทบาทใด ๆ ในกฎเหล่านี้ ตัวอย่างเช่นคุณสามารถเขียน

<T extends Integer & CharSequence>

ในทางตรงกันข้ามStringมันไม่ได้เป็นinterfaceดังนั้นจึงเป็นไปไม่ได้ที่จะขยายSomeClassเพื่อรับ subtype Stringเนื่องจาก java ไม่สนับสนุนการสืบทอดหลายค่าสำหรับคลาส

ด้วยListตัวอย่างคุณต้องจำไว้ว่ายาชื่อสามัญไม่ได้แปรปรวนร่วมและไม่แปรปรวน ซึ่งหมายความว่าถ้าXเป็นชนิดย่อยของY, List<X>ไม่เป็นชนิดย่อยหรือมิได้ supertype List<Y>ของ เนื่องจากIntegerไม่ได้ใช้CharSequenceคุณไม่สามารถใช้List<Integer>ในdoCharSequenceวิธีการของคุณ

อย่างไรก็ตามคุณสามารถรวบรวมมันได้

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

หากคุณมีวิธีการที่ส่งกลับสิ่งList<T>นี้:

static <T extends CharSequence> List<T> foo() 

คุณทำได้

List<? extends Integer> list = foo();

อีกครั้งนี้เป็นเพราะชนิดอนุมานเป็นและนี้เป็นชนิดย่อยของInteger & CharSequenceInteger

ประเภททางแยกเกิดขึ้นโดยปริยายเมื่อคุณระบุหลายขอบเขต (เช่น<T extends SomeClass & CharSequence>)

สำหรับข้อมูลเพิ่มเติมที่นี่เป็นส่วนหนึ่งของ JLS ที่อธิบายถึงวิธีการทำงานของขอบเขตประเภท คุณสามารถรวมหลายอินเตอร์เฟสเช่น

<T extends String & CharSequence & List & Comparator>

แต่เฉพาะขอบเขตแรกอาจไม่ใช่ส่วนต่อประสาน


62
ฉันไม่รู้ว่าคุณสามารถใส่&คำจำกัดความทั่วไปได้ +1
สะเก็ด

13
@flkes คุณสามารถใส่มากกว่าหนึ่ง แต่อาร์กิวเมนต์แรกเท่านั้นที่สามารถไม่ใช่ส่วนต่อประสาน <T extends String & List & Comparator>ไม่เป็นไร แต่<T extends String & Integer>ไม่ใช่เพราะIntegerไม่ใช่อินเทอร์เฟซ
Paul Boddington

7
@ PaulBoddington มีการใช้งานจริงสำหรับวิธีการเหล่านี้ ตัวอย่างเช่นหากไม่ได้ใช้ประเภทจริงสำหรับข้อมูลที่เก็บไว้ สำหรับตัวอย่างนี้เช่นเดียวกับCollections.emptyList() Optional.empty()การใช้งานที่ส่งคืนของอินเตอร์เฟสทั่วไป แต่ไม่เก็บอะไร
Stefan Dollase

6
และไม่มีใครพูดว่าคลาสที่อยู่finalในเวลาคอมไพล์จะเป็นfinalเวลารันไทม์
Holger

7
@Federico Peralta Schaffner: จุดนี้คือวิธีที่getCharSequence()สัญญาว่าจะคืนสิ่งXที่ผู้โทรต้องการซึ่งรวมถึงการส่งคืนประเภทที่ขยายIntegerและนำไปใช้CharSequenceหากผู้โทรต้องการและภายใต้สัญญานี้ถูกต้องเพื่อให้สามารถกำหนดผลลัพธ์Integerได้ มันเป็นวิธีการgetCharSequence()ที่พังเพราะไม่รักษาสัญญา แต่นั่นไม่ใช่ความผิดของคอมไพเลอร์
Holger

59

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

มีอีกหนึ่งความคุ้มค่าเป็นไปได้สำหรับเป็นประเภทนี้:Integer & CharSequence nullด้วยการใช้งานต่อไปนี้:

<X extends CharSequence> X getCharSequence() {
    return null;
}

การมอบหมายต่อไปนี้จะทำงาน:

Integer x = getCharSequence();

เนื่องจากค่าที่เป็นไปได้นี้จึงไม่มีเหตุผลว่าทำไมการมอบหมายควรจะผิดแม้ว่ามันจะไร้ประโยชน์อย่างเห็นได้ชัด คำเตือนจะมีประโยชน์

ปัญหาที่แท้จริงคือ API ไม่ใช่ไซต์การโทร

ในความเป็นจริงผมเคย blogged เมื่อเร็ว ๆ นี้เกี่ยวกับเรื่องนี้การออกแบบรูปแบบการป้องกัน API คุณควร (เกือบ) ไม่ควรออกแบบวิธีการทั่วไปเพื่อส่งกลับประเภทโดยพลการเพราะคุณสามารถ (เกือบ) ไม่เคยรับประกันว่าจะส่งมอบประเภทที่อนุมานได้ ข้อยกเว้นเป็นวิธีการเช่นCollections.emptyList()ในกรณีที่ความว่างเปล่าของรายการ (และการลบประเภททั่วไป) เป็นเหตุผลที่การอนุมานใด ๆ ที่<T>จะทำงาน:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.