List <Dog> เป็นคลาสย่อยของ List <Animal> หรือไม่ ทำไม Java generics ถึงไม่ polymorphic โดยปริยาย?


770

ฉันสับสนเล็กน้อยเกี่ยวกับวิธีที่ Java generics จัดการกับการสืบทอด / polymorphism

สมมติว่าลำดับชั้นดังต่อไปนี้ -

สัตว์ (ผู้ปกครอง)

สุนัข - แมว (เด็ก)

doSomething(List<Animal> animals)ดังนั้นคิดว่าฉันมีวิธี ตามกฎของมรดกและ polymorphism ทั้งหมดผมจะคิดว่าList<Dog> เป็นList<Animal>และList<Cat> เป็นList<Animal> - และเพื่อคนใดคนหนึ่งอาจจะส่งผ่านไปยังวิธีการนี้ ไม่เช่นนั้น ถ้าผมต้องการที่จะบรรลุพฤติกรรมนี้ผมต้องบอกอย่างชัดเจนวิธีการที่จะยอมรับรายการ subclass ของสัตว์ใด ๆ doSomething(List<? extends Animal> animals)โดยกล่าวว่า

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


17
และคำถามไวยากรณ์ที่ไม่เกี่ยวข้องทั้งหมดที่รบกวนฉันในตอนนี้ - ชื่อของฉันควรเป็น "เหตุใดจึงไม่ใช่ชื่อสามัญของ Java" หรือ "ทำไมชื่อสามัญของ Java ไม่ใช่ " พหูพจน์ "พหูพจน์" เป็นเพราะ s หรือเอกพจน์เพราะมันเป็นหนึ่งเอนทิตี้หรือไม่
froadie

25
ยาชื่อสามัญที่ทำใน Java เป็นรูปแบบของตัวแปรที่แตกต่างกันมาก อย่าใส่ศรัทธามากเกินไป (เหมือนที่ฉันเคยทำ) เพราะวันหนึ่งคุณจะต้องเผชิญกับข้อ จำกัด ที่น่าสงสาร: ศัลยแพทย์ขยาย HandScalize <Scalpel> Handable <Sponge> KABOOM! ไม่ได้คำนวณ [TM] มีข้อ จำกัด ทั่วไปเกี่ยวกับ Java ของคุณ OOA / OOD ใด ๆ สามารถแปลเป็นภาษา Java ได้ดี (และ MI สามารถทำได้อย่างดีโดยใช้ Java interfaces) แต่ข้อมูลทั่วไปไม่ได้ตัดทิ้ง พวกเขาใช้งานได้ดีสำหรับ "คอลเลกชัน" และการเขียนโปรแกรมตามขั้นตอนที่กล่าวไว้
ไวยากรณ์ T3rr0r

8
Super class ของ List <Dog> ไม่ใช่ List <Animal> แต่ List <?> (เช่นรายการที่ไม่ทราบประเภท) Generics ลบข้อมูลประเภทในรหัสที่รวบรวม สิ่งนี้ทำเพื่อให้รหัสที่ใช้ generics (java 5 & สูงกว่า) เข้ากันได้กับ java รุ่นก่อนหน้าโดยไม่มี generics
rai.skumar


9
@froadie เนื่องจากดูเหมือนว่าไม่มีใครตอบสนอง ... มันควรจะเป็น "ทำไมไม่ใช่จาวาของ Java ... " อีกประเด็นก็คือ "ทั่วไป" เป็นคำคุณศัพท์และ "generics" หมายถึงคำนามพหูพจน์ที่ถูกปรับเปลี่ยนโดย "generic" คุณสามารถพูดว่า "ฟังก์ชั่นนั้นเป็นแบบทั่วไป" แต่นั่นจะยุ่งยากกว่าการพูดว่า "ฟังก์ชั่นนั้นเป็นแบบทั่วไป" อย่างไรก็ตามมันค่อนข้างยุ่งยากที่จะพูดว่า "Java มีฟังก์ชั่นและคลาสทั่วไป" แทนที่จะเป็นเพียง "Java มีข้อมูลทั่วไป" ในฐานะคนที่เขียนวิทยานิพนธ์ปริญญาโทเรื่องคำคุณศัพท์ฉันคิดว่าคุณสะดุดคำถามที่น่าสนใจมาก!
dantiston

คำตอบ:


916

ไม่เป็นList<Dog>คือไม่ List<Animal>พิจารณาสิ่งที่คุณสามารถทำได้ด้วยList<Animal>- คุณสามารถเพิ่มสัตว์ใด ๆลงไป ... รวมถึงแมว ทีนี้คุณสามารถเพิ่มแมวเข้าไปในลูกสุนัขได้ไหม? ไม่ได้อย่างแน่นอน.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

ทันใดนั้นคุณมีแมวที่สับสนมาก

ตอนนี้คุณไม่สามารถเพิ่มCatไปเพราะคุณไม่ได้รู้ว่ามันเป็นList<? extends Animal> List<Cat>คุณสามารถดึงค่าและรู้ว่ามันจะเป็นAnimalแต่คุณไม่สามารถเพิ่มสัตว์โดยพลการ กลับเป็นจริงสำหรับList<? super Animal>- ในกรณีที่คุณสามารถเพิ่มAnimalให้กับมันได้อย่างปลอดภัย List<Object>แต่คุณไม่ได้รู้อะไรเกี่ยวกับสิ่งที่อาจจะดึงมาจากมันเพราะมันอาจจะเป็น


50
ที่น่าสนใจรายการสุนัขทุกตัวเป็นรายการสัตว์อย่างแน่นอนเช่นเดียวกับสัญชาตญาณบอกเรา ประเด็นก็คือไม่ใช่ว่าทุกรายการของสัตว์เป็นรายการของสุนัขดังนั้นการกลายพันธุ์ของรายการโดยการเพิ่มแมวเป็นปัญหา
Ingo

66
@Ingo: ไม่ไม่ได้จริงๆ: คุณสามารถเพิ่มแมวในรายชื่อสัตว์ แต่คุณไม่สามารถเพิ่มแมวในรายการสุนัข รายชื่อสุนัขเป็นรายการสัตว์ถ้าคุณคิดว่าเป็นแบบอ่านอย่างเดียว
Jon Skeet

12
@ JonSkeet - แน่นอน แต่ใครเป็นผู้บังคับให้สร้างรายการใหม่จากแมวและรายชื่อสุนัขจะเปลี่ยนรายชื่อสุนัขจริงหรือ นี่คือการตัดสินใจใช้งานโดยพลการใน Java สิ่งหนึ่งที่ตอบโต้กับตรรกะและสัญชาตญาณ
Ingo

7
@Ingo: ฉันจะไม่ใช้ "แน่นอน" เพื่อเริ่มต้นด้วย หากคุณมีรายการที่ระบุว่า "โรงแรมที่เราอาจต้องการไป" ด้านบนแล้วมีคนเพิ่มสระว่ายน้ำลงไปคุณคิดว่าถูกต้องหรือไม่ ไม่ - เป็นรายชื่อโรงแรมซึ่งไม่ใช่รายชื่ออาคาร และมันก็ไม่เหมือนที่ฉันพูดว่า "รายชื่อสุนัขไม่ใช่รายการสัตว์" - ฉันใส่มันลงไปในเทอมโค้ดในฟอนต์โค้ด ฉันไม่คิดว่าจะมีความกำกวมใด ๆ อยู่ที่นี่ การใช้คลาสย่อยจะไม่ถูกต้องอยู่ดี - มันเกี่ยวกับความเข้ากันได้ของการมอบหมายไม่ใช่คลาสย่อย
Jon Skeet

14
@ruakh: ปัญหาคือคุณต้องใช้เวลาในการประมวลผลสิ่งที่สามารถบล็อกได้ในเวลาคอมไพล์ และฉันขอยืนยันว่าความแปรปรวนของอาร์เรย์เป็นความผิดพลาดในการออกแบบที่จะเริ่มต้นด้วย
Jon Skeet

84

สิ่งที่คุณกำลังมองหาที่เรียกว่าประเภท covariantพารามิเตอร์ ซึ่งหมายความว่าหากวัตถุประเภทหนึ่งสามารถทดแทนได้ด้วยวิธีอื่น (เช่นAnimalสามารถถูกแทนที่ด้วยDog) สิ่งเดียวกันจะใช้กับนิพจน์ที่ใช้วัตถุเหล่านั้น (ดังนั้นList<Animal>อาจถูกแทนที่ด้วยList<Dog>) ปัญหาคือความแปรปรวนร่วมไม่ปลอดภัยสำหรับรายการที่ไม่แน่นอนโดยทั่วไป สมมติว่าคุณมีและมันจะถูกนำมาใช้เป็นList<Dog> List<Animal>จะเกิดอะไรขึ้นเมื่อคุณพยายามเพิ่มแมวลงไปในสิ่งนี้List<Animal>ซึ่งเป็นจริงList<Dog>หรือ การอนุญาตให้พารามิเตอร์ประเภทเป็น covariant แบ่งระบบประเภทโดยอัตโนมัติ

มันจะมีประโยชน์ในการเพิ่มไวยากรณ์เพื่อให้พารามิเตอร์ชนิดที่จะระบุเป็น covariant ซึ่งหลีกเลี่ยง? extends Fooการประกาศในวิธีการ แต่จะเพิ่มความซับซ้อนเพิ่มเติม


44

เหตุผลที่List<Dog>ไม่ใช่List<Animal>, ก็คือ, ตัวอย่างเช่น, คุณสามารถแทรก a CatลงไปList<Animal>, แต่ไม่ใช่ใน a List<Dog>... คุณสามารถใช้ wildcard เพื่อทำให้ยาสามัญขยายได้มากขึ้นถ้าเป็นไปได้; ตัวอย่างเช่นการอ่านจาก a List<Dog>คล้ายกับการอ่านจากList<Animal>- แต่ไม่ใช่การเขียน

Generics ในภาษา Javaและมาตราใน Generics จาก Java สอนมีดีมากคำอธิบายในเชิงลึกว่าทำไมบางสิ่งบางอย่างหรือไม่ได้รับอนุญาตหรือ polymorphic กับยาชื่อสามัญ


36

ประเด็นที่ฉันคิดว่าควรจะเพิ่มเข้าไปในสิ่งที่คนอื่น พูดถึงคือคำตอบ

List<Dog>ไม่ใช่ -a List<Animal> ใน Java

มันก็เป็นความจริงเช่นกัน

รายชื่อสุนัขเป็นรายการสัตว์ในภาษาอังกฤษ (ดีภายใต้การตีความที่สมเหตุสมผล)

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

หากต้องการกล่าวอีกวิธีหนึ่ง: A List<Dog>ใน Java ไม่ได้หมายถึง "รายชื่อสุนัข" ในภาษาอังกฤษหมายถึง "รายการที่สามารถมีสุนัขได้และไม่มีอะไรอื่น"

โดยทั่วไปแล้วสัญชาตญาณของ OP ให้ยืมตัวเองไปยังภาษาที่การดำเนินการกับวัตถุสามารถเปลี่ยนประเภทของพวกเขาหรือค่อนข้างประเภทของวัตถุเป็นฟังก์ชั่น (แบบไดนามิก) ของค่าของมัน


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

ในฐานะที่เป็นคนที่พบว่าการเปรียบเทียบกับอาร์เรย์มีความสับสนมากขึ้นคำตอบนี้ตอกย้ำมันสำหรับฉัน ปัญหาของฉันคือสัญชาตญาณภาษา
ฟอนลอน

32

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

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

รหัสนั้นรวบรวมได้ดี แต่เกิดข้อผิดพลาดรันไทม์ ( java.lang.ArrayStoreException: java.lang.Booleanในบรรทัดที่สอง) มันไม่ได้เป็นประเภทที่ปลอดภัย ประเด็นของ Generics คือการเพิ่มความปลอดภัยประเภทเวลารวบรวมมิฉะนั้นคุณสามารถติดกับชั้นเรียนธรรมดาโดยไม่ต้องใช้ยาสามัญ

ขณะนี้มีหลายครั้งที่คุณจำเป็นต้องมีความยืดหยุ่นมากขึ้นและนั่นคือสิ่งที่? super Classและ? extends Classมีไว้สำหรับ รูปแบบเดิมคือเมื่อคุณต้องการแทรกลงในประเภทCollection(ตัวอย่าง) และรูปแบบหลังใช้สำหรับเมื่อคุณต้องการอ่านจากรูปแบบที่ปลอดภัย แต่วิธีเดียวที่จะทำทั้งสองอย่างในเวลาเดียวกันคือการมีประเภทเฉพาะ


13
เนื้อหาความแปรปรวนของอาเรย์เป็นข้อบกพร่องในการออกแบบภาษา โปรดทราบว่าเนื่องจากการลบประเภทพฤติกรรมแบบเดียวกันเป็นไปไม่ได้ทางเทคนิคสำหรับการรวบรวมทั่วไป
Michael Borgwardt

" ฉันจะบอกว่าประเด็นทั้งหมดของ Generics คือมันไม่อนุญาตอย่างนั้น " คุณไม่สามารถมั่นใจได้: ระบบชนิดของ Java และ Scala นั้นไม่ปลอดภัย: วิกฤตการณ์ที่มีอยู่ของตัวชี้ Null (นำเสนอใน OOPSLA 2016) (ตั้งแต่แก้ไขดูเหมือน)
David Tonhofer

15

เพื่อให้เข้าใจถึงปัญหามีประโยชน์ในการเปรียบเทียบกับอาร์เรย์

List<Dog>คือไม่ได้List<Animal>เป็นรอง
แต่ Dog[] เป็นAnimal[]คลาสย่อยของ

อาร์เรย์มีreifiableและ covariant
พิสูจน์ได้หมายถึงข้อมูลประเภทของพวกเขาสามารถใช้ได้อย่างเต็มที่ในขณะใช้งานจริง
ดังนั้นอาร์เรย์จึงให้ความปลอดภัยประเภทรันไทม์ แต่ไม่ใช่ความปลอดภัยประเภทเวลาคอมไพล์

    // All compiles but throws ArrayStoreException at runtime at last line
    Dog[] dogs = new Dog[10];
    Animal[] animals = dogs; // compiles
    animals[0] = new Cat(); // throws ArrayStoreException at runtime

มันในทางกลับกันสำหรับข้อมูลทั่วไป:
Generics จะถูกลบและคงที่
ดังนั้นข้อมูลทั่วไปจึงไม่สามารถให้ความปลอดภัยประเภทรันไทม์ได้ แต่จะให้ความปลอดภัยประเภทเวลาคอมไพล์
ในรหัสด้านล่างหากยาชื่อสามัญมีความแปรปรวนร่วมมันจะเป็นไปได้ที่จะสร้างมลภาวะเป็นกองที่บรรทัดที่ 3

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());

2
มันอาจจะแย้งว่าแม่นยำเพราะของที่อาร์เรย์ใน Java จะแตก ,
leonbloy

อาร์เรย์กำลังเป็น covariant เป็นคอมไพเลอร์ "คุณสมบัติ"
Cristik

6

คำตอบที่ได้รับที่นี่ไม่ได้ทำให้ฉันมั่นใจอย่างเต็มที่ ดังนั้นฉันจึงทำตัวอย่างอีก

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

ฟังดูดีใช่มั้ย แต่คุณสามารถผ่านConsumers และSuppliers สำหรับAnimals เท่านั้น หากคุณมีMammalผู้บริโภค แต่เป็นDuckซัพพลายเออร์พวกเขาไม่ควรจะเหมาะกับสัตว์ทั้งคู่ เพื่อไม่อนุญาตสิ่งนี้จะมีการเพิ่มข้อ จำกัด เพิ่มเติม

แทนการข้างต้นเราต้องกำหนดความสัมพันธ์ระหว่างประเภทที่เราใช้

เช่น.,

public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

ตรวจสอบให้แน่ใจว่าเราสามารถใช้ซัพพลายเออร์ที่ให้วัตถุประเภทที่ถูกต้องแก่ผู้บริโภคเท่านั้น

OTOH เราทำได้เช่นกัน

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
    consumer.accept(supplier.get());
}

ที่เราไปทางอื่นเรากำหนดประเภทของSupplierและ จำกัด Consumerการที่จะสามารถใส่ลงไปใน

เราสามารถทำได้

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

ที่มีความสัมพันธ์ที่ใช้งานง่ายLife-> Animal-> Mammal-> Dog, Catฯลฯ เรายังสามารถใส่Mammalเป็นLifeของผู้บริโภค แต่ไม่Stringเป็นLifeผู้บริโภค


1
ในบรรดา 4 เวอร์ชัน # 2 อาจไม่ถูกต้อง เช่นเราไม่สามารถเรียกมันได้(Consumer<Runnable>, Supplier<Dog>)ในขณะที่Dogประเภทย่อยของAnimal & Runnable
ZhongYu

5

ตรรกะพื้นฐานสำหรับพฤติกรรมดังกล่าวเป็นGenericsไปตามกลไกการลบประเภท ดังนั้น ณ รันไทม์คุณจะไม่มีทางถ้าระบุประเภทของความcollectionแตกต่างarraysที่ไม่มีกระบวนการลบออก กลับมาที่คำถามของคุณ ...

ดังนั้นสมมติว่ามีวิธีการตามที่ระบุด้านล่าง:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

ตอนนี้หาก java อนุญาตให้ผู้โทรเพิ่ม List of type Animal ลงในเมธอดนี้คุณอาจเพิ่มสิ่งที่ไม่ถูกต้องลงในคอลเล็กชันและในเวลารันเกินไปจะรันเนื่องจากการลบประเภท ในขณะที่ในกรณีของอาร์เรย์คุณจะได้รับข้อยกเว้นเวลาทำงานสำหรับสถานการณ์ดังกล่าว ...

ดังนั้นในสาระสำคัญพฤติกรรมนี้ถูกนำมาใช้เพื่อที่จะไม่สามารถเพิ่มสิ่งผิดพลาดในการเก็บรวบรวม ตอนนี้ฉันเชื่อว่ามีการลบประเภทอยู่แล้วเพื่อให้เข้ากันได้กับ java ดั้งเดิมโดยไม่ต้องมีชื่อสามัญ ....


4

การพิมพ์ย่อยไม่แปรเปลี่ยนสำหรับชนิดที่กำหนดพารามิเตอร์ แม้ยากชั้นDogเป็นชนิดย่อยของAnimalชนิดแปรไม่ได้เป็นชนิดย่อยของList<Dog> List<Animal>ในทางตรงกันข้ามcovariant subtyping จะถูกใช้โดยอาร์เรย์ดังนั้นชนิดอาร์เรย์เป็นชนิดย่อยของDog[]Animal[]

ประเภทย่อยที่ไม่แปรเปลี่ยนทำให้แน่ใจได้ว่าข้อ จำกัด ประเภทที่บังคับใช้โดย Java จะไม่ถูกละเมิด พิจารณารหัสต่อไปนี้ที่ได้รับจาก @Jon Skeet:

List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);

ตามที่ระบุไว้โดย @Jon Skeet รหัสนี้ผิดกฎหมายเพราะไม่เช่นนั้นจะเป็นการละเมิดข้อ จำกัด ประเภทโดยส่งคืนแมวเมื่อสุนัขคาดหวัง

มันเป็นคำแนะนำในการเปรียบเทียบรหัสข้างต้นกับอะนาล็อกสำหรับอาร์เรย์

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

รหัสถูกกฎหมาย อย่างไรก็ตามพ่นยกเว้นการจัดเก็บอาร์เรย์ อาร์เรย์ดำเนินการชนิดของมันในเวลาทำงานด้วยวิธีนี้ JVM สามารถบังคับใช้ความปลอดภัยของประเภทของการพิมพ์ย่อย covariant

หากต้องการทำความเข้าใจเพิ่มเติมให้ดูที่โค้ดไบต์ที่สร้างโดยjavapคลาสด้านล่าง:

import java.util.ArrayList;
import java.util.List;

public class Demonstration {
    public void normal() {
        List normal = new ArrayList(1);
        normal.add("lorem ipsum");
    }

    public void parameterized() {
        List<String> parameterized = new ArrayList<>(1);
        parameterized.add("lorem ipsum");
    }
}

การใช้คำสั่งjavap -c Demonstrationจะแสดง Java bytecode ต่อไปนี้:

Compiled from "Demonstration.java"
public class Demonstration {
  public Demonstration();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void normal();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return

  public void parameterized();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return
}

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

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


3

ที่จริงแล้วคุณสามารถใช้อินเทอร์เฟซเพื่อบรรลุสิ่งที่คุณต้องการ

public interface Animal {
    String getName();
    String getVoice();
}
public class Dog implements Animal{
    @Override 
    String getName(){return "Dog";}
    @Override
    String getVoice(){return "woof!";}

}

จากนั้นคุณสามารถใช้คอลเลกชันโดยใช้

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());

1

หากคุณแน่ใจว่ารายการในรายการเป็นคลาสย่อยของประเภทซุปเปอร์ที่กำหนดคุณสามารถส่งรายการโดยใช้วิธีนี้:

(List<Animal>) (List<?>) dogs

นี่คือมีประโยชน์เมื่อคุณต้องการส่งรายการในนวกรรมิกหรือวนซ้ำมัน


2
สิ่งนี้จะสร้างปัญหามากกว่าที่จะแก้ได้จริง
Ferrybig

หากคุณพยายามที่จะเพิ่ม Cat ลงในรายการให้แน่ใจว่ามันจะสร้างปัญหา แต่สำหรับจุดประสงค์ในการวนซ้ำฉันคิดว่ามันเป็นคำตอบเดียวที่ไม่มีคำตอบ
sagits

1

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

ในกรณีส่วนใหญ่เราสามารถใช้Collection<? extends T>ค่อนข้างแล้วCollection<T>และนั่นควรเป็นตัวเลือกแรก อย่างไรก็ตามฉันกำลังค้นหากรณีที่มันไม่ง่ายที่จะทำ มันขึ้นอยู่กับการถกเถียงกันว่าเป็นสิ่งที่ดีที่สุดที่จะทำ ฉันกำลังนำเสนอคลาส DownCastCollection ที่สามารถแปลง a Collection<? extends T>เป็นCollection<T>(เราสามารถกำหนดคลาสที่คล้ายกันสำหรับ List, Set, NavigableSet, .. ) ที่จะใช้เมื่อใช้วิธีมาตรฐานไม่สะดวก ด้านล่างเป็นตัวอย่างของวิธีการใช้งาน (เราสามารถใช้Collection<? extends Object>ในกรณีนี้ได้เช่นกัน แต่ฉันทำให้มันง่ายที่จะแสดงการใช้ DownCastCollection

/**Could use Collection<? extends Object> and that is the better choice. 
* But I am doing this to illustrate how to use DownCastCollection. **/

public static void print(Collection<Object> col){  
    for(Object obj : col){
    System.out.println(obj);
    }
}
public static void main(String[] args){
  ArrayList<String> list = new ArrayList<>();
  list.addAll(Arrays.asList("a","b","c"));
  print(new DownCastCollection<Object>(list));
}

ตอนนี้ชั้นเรียน:

import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;

public DownCastCollection(Collection<? extends E> delegate) {
    super();
    this.delegate = delegate;
}

@Override
public int size() {
    return delegate ==null ? 0 : delegate.size();
}

@Override
public boolean isEmpty() {
    return delegate==null || delegate.isEmpty();
}

@Override
public boolean contains(Object o) {
    if(isEmpty()) return false;
    return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
    Iterator<? extends E> delegateIterator;

    protected MyIterator() {
        super();
        this.delegateIterator = delegate == null ? null :delegate.iterator();
    }

    @Override
    public boolean hasNext() {
        return delegateIterator != null && delegateIterator.hasNext();
    }

    @Override
    public  E next() {
        if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
        return delegateIterator.next();
    }

    @Override
    public void remove() {
        delegateIterator.remove();

    }

}
@Override
public Iterator<E> iterator() {
    return new MyIterator();
}



@Override
public boolean add(E e) {
    throw new UnsupportedOperationException();
}

@Override
public boolean remove(Object o) {
    if(delegate == null) return false;
    return delegate.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
    if(delegate==null) return false;
    return delegate.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
    throw new UnsupportedOperationException();
}

@Override
public boolean removeAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.removeAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
    if(delegate == null) return false;
    return delegate.retainAll(c);
}

@Override
public void clear() {
    if(delegate == null) return;
        delegate.clear();

}

}


นี่เป็นความคิดที่ดีมากจนมีอยู่ใน Java SE แล้ว ; )Collections.unmodifiableCollection
Radiodef

1
ขวา แต่คอลเล็กชันที่ฉันกำหนดสามารถแก้ไขได้
b

ใช่มันสามารถแก้ไขได้ Collection<? extends E>จัดการพฤติกรรมนั้นได้อย่างถูกต้องแล้วเว้นแต่ว่าคุณจะใช้มันในลักษณะที่ไม่ปลอดภัย (เช่นการส่งไปยังสิ่งอื่น) ข้อดีอย่างเดียวที่ฉันเห็นคือเมื่อคุณเรียกใช้addการดำเนินการมันจะส่งข้อยกเว้นถึงแม้ว่าคุณจะใช้มัน
Vlasec

0

ให้นำตัวอย่างจากการสอน JavaSE

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

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

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

ดังนั้น "สถาปนิก" ของ Java มี 2 ตัวเลือกซึ่งแก้ไขปัญหานี้:

  1. อย่าคิดว่าเป็นประเภทย่อยโดยปริยายมันเป็น supertype และให้ข้อผิดพลาดในการคอมไพล์เช่นมันเกิดขึ้นตอนนี้

  2. พิจารณาว่า subtype นั้นเป็น supertype และ จำกัด เมื่อคอมไพล์วิธี "add" (ดังนั้นในวิธี drawAll หากรายการของวงการวงกลมชนิดย่อยของรูปร่างจะถูกส่งผ่านคอมไพเลอร์ควรตรวจพบและ จำกัด คุณด้วยข้อผิดพลาดในการคอมไพล์ ที่).

ด้วยเหตุผลที่ชัดเจนว่าเลือกวิธีแรก


0

เราควรคำนึงถึงวิธีที่คอมไพเลอร์คุกคามคลาสทั่วไปด้วย: ใน "instantiates" ประเภทที่แตกต่างเมื่อใดก็ตามที่เราเติมอาร์กิวเมนต์ทั่วไป

ดังนั้นเราจึงมีListOfAnimal, ListOfDog, ListOfCatฯลฯ ซึ่งมีการเรียนแตกต่างกันที่จบลงด้วยการ "สร้าง" โดยรวบรวมเมื่อเราระบุข้อโต้แย้งทั่วไป และนี่คือลำดับชั้นเรียบ (อันที่จริงแล้วเกี่ยวข้องกับListไม่ใช่ลำดับชั้นเลย)

อีกเหตุผลหนึ่งที่ความแปรปรวนร่วมไม่สมเหตุสมผลในกรณีของคลาสทั่วไปคือข้อเท็จจริงที่ว่าที่คลาสทั้งหมดนั้นเหมือนกัน - เป็นListอินสแตนซ์ การเพิ่มความเชี่ยวชาญListโดยการกรอกอาร์กิวเมนต์ทั่วไปไม่ขยายชั้นเรียนเพียงแค่ทำให้มันใช้งานได้สำหรับอาร์กิวเมนต์ทั่วไปนั้น


0

ปัญหาได้รับการระบุอย่างดี แต่มันมีทางออก ทำให้doSomethingทั่วไป:

<T extends Animal> void doSomething<List<T> animals) {
}

ตอนนี้คุณสามารถเรียก doSomething ด้วย List <Dog> หรือ List <Cat> หรือ List <Animal>


0

โซลูชันอื่นคือการสร้างรายการใหม่

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());

0

เพิ่มเติมจากคำตอบของ Jon Skeet ซึ่งใช้โค้ดตัวอย่างนี้:

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

ในระดับที่ลึกที่สุดปัญหาที่นี่คือสิ่งนั้นdogsและanimalsแบ่งปันการอ้างอิง นั่นหมายความว่าวิธีหนึ่งที่จะทำให้งานนี้คือการคัดลอกรายการทั้งหมดซึ่งจะทำลายความเท่าเทียมกันของการอ้างอิง:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

หลังจากเรียกList<Animal> animals = new ArrayList<>(dogs);คุณจะไม่สามารถกำหนดภายหลังโดยตรงanimalsอย่างใดอย่างหนึ่งdogsหรือcats:

// These are both illegal
dogs = animals;
cats = animals;

ดังนั้นคุณไม่สามารถใส่ประเภทย่อยที่ไม่ถูกต้องAnimalลงในรายการได้เนื่องจากไม่มีประเภทย่อยที่ไม่ถูกต้อง - วัตถุใด ๆ ของประเภทย่อย? extends Animalสามารถเพิ่มเข้าไปanimalsได้

เห็นได้ชัดว่านี่เป็นการเปลี่ยนแปลงซีแมนทิกส์เนื่องจากรายการanimalsและdogsไม่ได้ถูกแบ่งปันอีกต่อไปดังนั้นการเพิ่มลงในรายการหนึ่งจะไม่เพิ่มไปยังอีกรายการหนึ่ง (ซึ่งเป็นสิ่งที่คุณต้องการอย่างแท้จริงเพื่อหลีกเลี่ยงปัญหาที่Catอาจเพิ่มลงในรายการที่เป็นเท่านั้น ควรจะมีDogวัตถุ) นอกจากนี้การคัดลอกรายการทั้งหมดอาจไม่มีประสิทธิภาพ อย่างไรก็ตามวิธีนี้จะช่วยแก้ปัญหาความเท่าเทียมกันของประเภทโดยแบ่งความเท่าเทียมกันอ้างอิง

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