เหตุใดอาร์เรย์ของโควาเรียนท์ แต่ยาสามัญมีค่าคงที่?


160

จาก Java ที่มีประสิทธิภาพโดย Joshua Bloch

  1. อาร์เรย์แตกต่างจากประเภททั่วไปในสองวิธีที่สำคัญ อาร์เรย์แรกเป็น covariant ยาสามัญมีค่าคงที่
  2. Covariant นั้นหมายถึงว่าถ้า X เป็นชนิดย่อยของ Y ดังนั้น X [] ก็จะเป็นประเภทย่อยของ Y [] อาร์เรย์เป็น covariant เนื่องจากสตริงเป็นชนิดย่อยของ Object So

    String[] is subtype of Object[]

    ค่าคงที่นั้นหมายถึงโดยไม่คำนึงว่า X เป็นชนิดย่อยของ Y หรือไม่

     List<X> will not be subType of List<Y>.

คำถามของฉันคือทำไมการตัดสินใจที่จะทำให้อาร์เรย์ covariant ใน Java? มีการโพสต์ SO อื่น ๆ เช่นทำไมอาเรย์คงที่ แต่รายการ covariant? แต่ดูเหมือนว่าพวกเขาจะเพ่งความสนใจไปที่ Scala และฉันไม่สามารถติดตามได้


1
นี่ไม่ใช่เพราะยาชื่อสามัญถูกเพิ่มในภายหลังหรือไม่
Sotirios Delimanolis

1
ฉันคิดว่าการเปรียบเทียบระหว่างอาร์เรย์และคอลเลกชันนั้นไม่ยุติธรรมคอลเลกชันใช้อาร์เรย์ในพื้นหลัง !!
อาเหม็ด Adel Ismail

4
@ EL-conteDe-monteTereBentikh LinkedListไม่คอลเลกชันทั้งหมดตัวอย่างเช่น
Paul Bellora

@ PaulBellora ฉันรู้ว่า Maps นั้นแตกต่างจากผู้ใช้งานคอลเล็กชัน แต่ฉันอ่านใน SCPJ6 ที่ส่วนใหญ่อาศัยในอาร์เรย์ !!
อาเหม็ด Adel Ismail

เนื่องจากไม่มี ArrayStoreException เมื่อใส่องค์ประกอบที่ไม่ถูกต้องในคอลเล็กชันที่มีอาร์เรย์ ดังนั้นคอลเล็กชั่นสามารถพบสิ่งนี้ได้เฉพาะในเวลาดึงและนั่นก็เพราะการคัดเลือก ดังนั้นยาชื่อสามัญจะช่วยแก้ปัญหานี้ได้
Kanagavelu Sugumar

คำตอบ:


150

ผ่านวิกิพีเดีย :

Java เวอร์ชันแรกและ C # ไม่ได้รวม generics (หรือที่รู้จักว่าตัวแปรเชิงตัวแปร)

ในการตั้งค่าดังกล่าวทำให้อาร์เรย์มีค่าคงที่ออกกฎโปรแกรม polymorphic ที่มีประโยชน์ ตัวอย่างเช่นลองเขียนฟังก์ชั่นเพื่อสุ่มอาร์เรย์หรือฟังก์ชั่นที่ทดสอบสองอาร์เรย์เพื่อความเท่าเทียมกันโดยใช้Object.equalsวิธีการในองค์ประกอบ การใช้งานไม่ได้ขึ้นอยู่กับชนิดขององค์ประกอบที่แน่นอนที่เก็บไว้ในอาเรย์ดังนั้นจึงควรจะสามารถเขียนฟังก์ชั่นเดียวที่ใช้งานได้กับอาเรย์ทุกประเภท มันง่ายที่จะใช้ฟังก์ชั่นประเภท

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

อย่างไรก็ตามถ้าประเภทอาร์เรย์ได้รับการปฏิบัติเหมือนไม่คงที่จะสามารถเรียกฟังก์ชันเหล่านี้ได้ในอาร์เรย์ที่มีชนิดObject[]ทั้งหมดเท่านั้น ตัวอย่างเช่นไม่สามารถสับเปลี่ยนอาร์เรย์ของสตริงได้

ดังนั้นทั้ง Java และ C # จึงจัดการกับประเภทอาเรย์อย่างต่อเนื่อง ยกตัวอย่างเช่นใน C # string[]เป็นชนิดย่อยของobject[]และ Java เป็นชนิดย่อยของString[]Object[]

นี้จะตอบคำถามที่ว่า "ทำไมอาร์เรย์ covariant?" หรืออย่างแม่นยำมากขึ้น "ทำไมมีอาร์เรย์ covariant ทำในเวลานั้น ?"

เมื่อแนะนำยาชื่อสามัญพวกเขาไม่ได้ทำโควาเรียนท์ด้วยเหตุผลที่ชี้ให้เห็นในคำตอบนี้โดย Jon Skeet :

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

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

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

แรงจูงใจดั้งเดิมสำหรับการทำอาร์เรย์ covariant ที่อธิบายไว้ในบทความ wikipedia ไม่ได้ใช้กับยาสามัญเพราะสัญลักษณ์ตัวแทนทำให้การแสดงออกของความแปรปรวนร่วม (และความแปรปรวน) เป็นไปได้เช่น:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);

3
ใช่ arrays อนุญาตให้มีพฤติกรรม polymorphic อย่างไรก็ตามมันแนะนำการรันแบบรันไทม์ (ซึ่งแตกต่างจากข้อยกเว้นเวลาคอมไพล์กับคอมเพล็ก เช่น:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn

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

1
ทำไม "หลายคน" มองว่ามันผิดพลาด? มันมีประโยชน์มากกว่าที่จะไม่มีความแปรปรวนร่วมของอาร์เรย์ คุณเห็น ArrayStoreException บ่อยแค่ไหน พวกมันค่อนข้างหายาก ประชดที่นี่คือ imo ยกโทษให้ไม่ได้ ... ในบรรดาข้อผิดพลาดที่เลวร้ายที่สุดที่เคยทำใน Java คือความแปรปรวนของเว็บไซต์ใช้งานหรือที่รู้จัก wildcard
สกอตต์

3
@ScottMcKinney: "ทำไม" หลายคน "ถึงเห็นว่ามันผิดพลาด" AIUI นี้เป็นเพราะความแปรปรวนของอาร์เรย์ต้องการการทดสอบประเภทไดนามิกในการดำเนินการกำหนดอาเรย์ทั้งหมด (แม้ว่าการปรับแต่งคอมไพเลอร์อาจช่วยได้?) ซึ่งอาจทำให้เกิดโอเวอร์เฮดของรันไทม์ที่สำคัญ
Dominique Devriese

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

30

เหตุผลก็คือทุกอาร์เรย์รู้ประเภทองค์ประกอบในขณะที่การรวบรวมทั่วไปไม่ได้เกิดจากการลบประเภท

ตัวอย่างเช่น:

String[] strings = new String[2];
Object[] objects = strings;  // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime

ถ้าสิ่งนี้ได้รับอนุญาตด้วยคอลเลกชันทั่วไป:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;  // let's say it is valid
objects.add(12);  // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this

แต่สิ่งนี้จะทำให้เกิดปัญหาในภายหลังเมื่อมีคนพยายามเข้าถึงรายการ:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String

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

@ user2708477 ใช่มีการแนะนำการลบประเภทเนื่องจากความเข้ากันได้แบบย้อนหลัง และใช่คำตอบของฉันพยายามที่จะตอบคำถามในชื่อทำไม generics จะไม่เปลี่ยนแปลง
Katona

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

@supercat ถูกต้องสิ่งที่ฉันต้องการชี้ให้เห็นคือสำหรับ generics ที่มีการลบประเภทในสถานที่ความแปรปรวนร่วมไม่สามารถนำมาใช้กับความปลอดภัยที่น้อยที่สุดของการตรวจสอบ runtime
Katona

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

22

อาจเป็นความช่วยเหลือนี้ : -

ยาสามัญไม่มีความแปรปรวนร่วม

อาร์เรย์ในภาษาจาวาคือ covariant - ซึ่งหมายความว่าถ้าจำนวนเต็มขยายจำนวน (ซึ่งมัน) แล้วไม่เพียง แต่เป็นจำนวนเต็มยังเป็นจำนวน แต่จำนวนเต็ม [] ยังเป็นNumber[]และคุณมีอิสระที่จะผ่านหรือมอบหมายInteger[]ที่Number[]เรียกว่าสำหรับ (เป็นทางการมากขึ้นถ้า Number เป็น supertype ของ Integer ดังนั้นจึงNumber[]เป็น supertype ของInteger[]) คุณอาจคิดว่าเหมือนกันกับประเภททั่วไปเช่นกัน - นั่นList<Number>คือ supertype ของList<Integer>และคุณสามารถผ่านList<Integer>ตำแหน่งที่List<Number>คาดไว้ น่าเสียดายที่มันใช้งานไม่ได้

มันกลับกลายเป็นว่ามีเหตุผลที่ดีที่มันไม่ได้ผล: มันจะทำลายความปลอดภัยทั่วไปที่ควรจะให้ ลองนึกภาพคุณสามารถกำหนดให้เป็นList<Integer> List<Number>จากนั้นรหัสต่อไปนี้จะอนุญาตให้คุณใส่สิ่งที่ไม่ใช่จำนวนเต็มลงในList<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

เนื่องจาก ln เป็น a List<Number>, การเพิ่ม Float ให้ดูเหมือนอย่างถูกกฎหมาย แต่ถ้า ln ถูกใช้นามแฝงliแล้วมันจะทำลายสัญญาความปลอดภัยประเภทโดยนัยในคำจำกัดความของ li - ว่ามันเป็นรายการของจำนวนเต็มซึ่งเป็นสาเหตุที่ประเภททั่วไปไม่สามารถแปรเปลี่ยนได้


3
สำหรับ Arrays คุณจะได้รับArrayStoreExceptionat runtime
Sotirios Delimanolis

4
คำถามของฉันWHYคืออาร์เรย์ทำ covariant ดังที่ Sotirios ได้กล่าวไว้กับ Arays หนึ่งจะได้รับ ArrayStoreException ณ รันไทม์หาก Arrays ไม่แปรเปลี่ยนดังนั้นเราสามารถตรวจพบข้อผิดพลาดนี้ในเวลารวบรวมที่ถูกต้องหรือไม่
eagertoLearn

@eagertoLearn: หนึ่งในความอ่อนแอความหมายที่สำคัญของ Java ก็คือว่ามันไม่มีอะไรในระบบการพิมพ์ที่สามารถแยกแยะความแตกต่าง "อาร์เรย์ซึ่งถืออะไร แต่สัญญาซื้อขายล่วงหน้าของAnimalซึ่งไม่ต้องยอมรับรายการใด ๆ ที่ได้รับจากที่อื่น" จาก "อาร์เรย์ซึ่งจะต้องประกอบด้วยอะไร แต่Animal, และจะต้องยินดีที่จะยอมรับการอ้างอิงภายนอกที่ให้Animalมารหัสที่ต้องใช้ในอดีตควรยอมรับอาร์เรย์ของCatแต่รหัสที่ต้องการหลังไม่ควรถ้าคอมไพเลอร์สามารถแยกความแตกต่างทั้งสองประเภทก็สามารถให้ตรวจสอบเวลารวบรวม น่าเสียดายที่มีเพียงสิ่งเดียวที่ทำให้พวกเขาโดดเด่น ...
supercat

... คือว่าจริง ๆ แล้วรหัสพยายามที่จะเก็บอะไรลงในพวกเขาและไม่มีทางรู้ว่าจนกว่าจะถึงเวลารันไทม์
supercat

3

อาร์เรย์มี covariant อย่างน้อยสองเหตุผล:

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

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

ความจริงที่ว่าอาร์เรย์นั้นมีความแปรปรวนร่วมอาจถูกมองว่าเป็นการแฮกที่น่าเกลียด แต่ในกรณีส่วนใหญ่มันจะช่วยในการสร้างรหัสการทำงาน


1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- จุดดี
eagertoLearn

3

คุณลักษณะที่สำคัญของประเภทตัวแปรคือความสามารถในการเขียนขั้นตอนวิธีการ polymorphic Arrays.sort()ขั้นตอนวิธีการเช่นที่ทำงานบนโครงสร้างข้อมูลโดยไม่คำนึงถึงค่าพารามิเตอร์เช่น

ด้วยยาชื่อสามัญที่ทำกับตัวแทนประเภท:

<E extends Comparable<E>> void sort(E[]);

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

void sort(Comparable[]);

อย่างไรก็ตามความเรียบง่ายนั้นเปิดช่องโหว่ในระบบสแตติกชนิด:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

ต้องการการตรวจสอบรันไทม์ของการเข้าถึงเพื่อเขียนทุกประเภทอาเรย์

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


2

ยาชื่อสามัญไม่เปลี่ยนแปลง : จากJSL 4.10 :

... การพิมพ์ย่อยไม่ขยายผ่านประเภททั่วไป: T <: U ไม่ได้หมายความว่าC<T><: C<U>...

และอีกไม่กี่บรรทัด JLS ก็อธิบายว่า
อาร์เรย์เป็นตัวแปรร่วม (กระสุนแรก):

4.10.3 การพิมพ์ย่อยระหว่างประเภทอาเรย์

ป้อนคำอธิบายรูปภาพที่นี่


2

ฉันคิดว่าพวกเขาตัดสินใจผิดตั้งแต่แรกที่สร้างอาร์เรย์ covariant มันแบ่งความปลอดภัยประเภทตามที่อธิบายไว้ที่นี่และพวกเขาติดอยู่กับที่เนื่องจากความเข้ากันได้ย้อนหลังและหลังจากนั้นพวกเขาพยายามที่จะไม่ทำผิดพลาดเหมือนกันสำหรับทั่วไป และนั่นเป็นหนึ่งในเหตุผลที่Joshua Blochชอบที่จะแสดง arra ys ในข้อ 25 ของหนังสือ "Effective Java (edition ที่สอง)"


Josh Block เป็นผู้แต่งกรอบงานคอลเลกชันของ Java (1.2) และผู้แต่งเรื่อง Java (1.5) ดังนั้นคนที่สร้างยาชื่อสามัญที่ทุกคนบ่นก็เหมือนกันโดยบังเอิญคนที่เขียนหนังสือเล่มนี้บอกว่าพวกเขาเป็นวิธีที่ดีกว่าที่จะไป? ไม่แปลกใจเลย!
cpurdy

1

สิ่งที่ฉันต้องทำ: เมื่อโค้ดคาดหวังว่าอาร์เรย์ A [] และคุณให้ B [] โดยที่ B เป็นคลาสย่อยของ A มีสองสิ่งที่ต้องกังวลเกี่ยวกับ: เกิดอะไรขึ้นเมื่อคุณอ่านองค์ประกอบอาร์เรย์และสิ่งที่เกิดขึ้นถ้าคุณเขียน มัน. ดังนั้นจึงไม่ยากที่จะเขียนกฎภาษาเพื่อให้แน่ใจว่าความปลอดภัยของประเภทนั้นได้รับการเก็บรักษาไว้ในทุกกรณี (กฎหลักคือการที่ArrayStoreExceptionอาจถูกโยนทิ้งได้หากคุณพยายามที่จะใส่ A ลงใน B []) อย่างไรก็ตามสำหรับคนทั่วไปเมื่อคุณประกาศชั้นเรียนSomeClass<T>มีหลายวิธีที่Tใช้ในเนื้อหาของชั้นเรียนและฉันเดาว่ามันซับซ้อนเกินไปที่จะหาชุดค่าผสมที่เป็นไปได้ทั้งหมดเพื่อเขียนกฎเกี่ยวกับเมื่อใด สิ่งที่ได้รับอนุญาตและเมื่อพวกเขาไม่

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