เหตุผลนี้ขึ้นอยู่กับวิธีการที่จาวาดำเนินการเรื่อง generics
ตัวอย่างอาร์เรย์
ด้วย Array คุณสามารถทำได้ (Array เป็น covariant)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
แต่จะเกิดอะไรขึ้นถ้าคุณพยายามทำเช่นนี้?
myNumber[0] = 3.14; //attempt of heap pollution
บรรทัดสุดท้ายนี้จะรวบรวมได้ แต่ถ้าคุณเรียกใช้รหัสนี้คุณจะได้รับ ArrayStoreException
แต่ถ้าคุณเรียกใช้รหัสนี้คุณอาจได้รับ เพราะคุณกำลังพยายามใส่ double ลงในอาร์เรย์จำนวนเต็ม (โดยไม่คำนึงถึงการเข้าถึงผ่านการอ้างอิงตัวเลข)
ซึ่งหมายความว่าคุณสามารถหลอกคอมไพเลอร์ แต่คุณไม่สามารถหลอกระบบประเภทรันไทม์ และนี่ก็เป็นเช่นนั้นเพราะอาร์เรย์เป็นสิ่งที่เราเรียกว่าชนิดที่เรียกคืนได้ ซึ่งหมายความว่าที่รันไทม์ Java Number[]
รู้ว่าอาร์เรย์นี้ที่อินสแตนซ์จริงเป็นอาร์เรย์ของจำนวนเต็มซึ่งก็เกิดขึ้นจะเข้าถึงได้ผ่านการอ้างอิงของพิมพ์
อย่างที่คุณเห็นสิ่งหนึ่งคือประเภทของวัตถุที่แท้จริงและอีกอย่างคือประเภทของการอ้างอิงที่คุณใช้เข้าถึงมันใช่ไหม
ปัญหาเกี่ยวกับ Java Generics
ตอนนี้ปัญหาเกี่ยวกับประเภททั่วไปของ Java คือข้อมูลประเภทถูกทิ้งโดยคอมไพเลอร์และไม่สามารถใช้ได้ในเวลาทำงาน กระบวนการนี้เรียกว่าลบออกประเภท มีเหตุผลที่ดีสำหรับการใช้งาน generics เช่นนี้ใน Java แต่นั่นเป็นเรื่องที่ยาวและต้องทำเหนือสิ่งอื่นใดด้วยความเข้ากันได้ของไบนารีกับรหัสที่มีอยู่ก่อน (ดูวิธีที่เราได้รับข้อมูลทั่วไปที่เรามี )
แต่จุดสำคัญที่นี่ก็คือตั้งแต่ที่รันไทม์ไม่มีข้อมูลประเภทไม่มีทางที่จะมั่นใจได้ว่าเราไม่ได้ก่อมลภาวะเป็นกอง
ตัวอย่างเช่น
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution
หากคอมไพเลอร์ Java ไม่ได้หยุดคุณจากการทำเช่นนี้ระบบชนิดรันไทม์ไม่สามารถหยุดคุณได้เช่นกันเนื่องจากไม่มีวิธีรันไทม์เพื่อตรวจสอบว่ารายการนี้ควรจะเป็นรายการของจำนวนเต็มเท่านั้น รันไทม์ Java จะช่วยให้คุณใส่สิ่งที่คุณต้องการลงในรายการนี้เมื่อมันควรจะมีจำนวนเต็มเท่านั้นเพราะเมื่อมันถูกสร้างขึ้นมันถูกประกาศว่าเป็นรายการของจำนวนเต็ม
ดังนั้นผู้ออกแบบของ Java จึงแน่ใจว่าคุณไม่สามารถหลอกผู้แปลได้ หากคุณไม่สามารถหลอกคอมไพเลอร์ได้ (อย่างที่เราสามารถทำได้กับอาเรย์) คุณไม่สามารถหลอกระบบประเภทรันไทม์
ด้วยเหตุนี้เราจึงกล่าวว่าประเภททั่วไปไม่สามารถนำกลับมาใช้ซ้ำได้
เห็นได้ชัดว่าสิ่งนี้จะขัดขวางความแตกต่าง ลองพิจารณาตัวอย่างต่อไปนี้:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
ตอนนี้คุณสามารถใช้มันได้เช่นนี้:
Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));
แต่ถ้าคุณพยายามใช้รหัสเดียวกันกับคอลเลกชันทั่วไปคุณจะไม่ประสบความสำเร็จ:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
คุณจะได้รับข้อผิดพลาดคอมไพเลอร์ถ้าคุณพยายาม ...
List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error
การแก้ปัญหาคือการเรียนรู้ที่จะใช้สองคุณสมบัติที่มีประสิทธิภาพของ Java generics ที่เรียกว่าความแปรปรวนร่วมและความแปรปรวน
แปรปรวน
ด้วยความแปรปรวนร่วมคุณสามารถอ่านรายการจากโครงสร้างได้ แต่คุณไม่สามารถเขียนอะไรลงไปได้ ทั้งหมดนี้คือการประกาศที่ถูกต้อง
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();
และคุณสามารถอ่านได้จากmyNums
:
Number n = myNums.get(0);
เนื่องจากคุณสามารถมั่นใจได้ว่าสิ่งใดก็ตามที่มีอยู่ในรายการจริงมันสามารถถูกถ่ายทอดไปยังหมายเลข (หลังจากที่ทุกสิ่งที่ขยายหมายเลขเป็นหมายเลขใช่มั้ย)
อย่างไรก็ตามคุณไม่ได้รับอนุญาตให้นำสิ่งใดเข้าไปในโครงสร้าง covariant
myNumst.add(45L); //compiler error
สิ่งนี้จะไม่ได้รับอนุญาตเนื่องจาก Java ไม่สามารถรับประกันประเภทของวัตถุที่แท้จริงในโครงสร้างทั่วไป สามารถเป็นอะไรก็ได้ที่ขยาย Number แต่คอมไพเลอร์ไม่แน่ใจ ดังนั้นคุณสามารถอ่าน แต่ไม่เขียน
contravariance
ด้วยความแตกต่างคุณสามารถทำสิ่งที่ตรงกันข้าม คุณสามารถใส่สิ่งต่าง ๆ ลงในโครงสร้างทั่วไป แต่คุณไม่สามารถอ่านออกมาได้
List<Object> myObjs = new List<Object>();
myObjs.add("Luke");
myObjs.add("Obi-wan");
List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);
ในกรณีนี้ธรรมชาติที่แท้จริงของวัตถุนั้นคือ List of Objects และจากการที่คุณสามารถนำ Numbers เข้ามาโดยทั่วไปเนื่องจากตัวเลขทั้งหมดมี Object เป็นบรรพบุรุษร่วมกัน ดังนั้นตัวเลขทั้งหมดจึงเป็นวัตถุดังนั้นจึงถูกต้อง
อย่างไรก็ตามคุณไม่สามารถอ่านอะไรได้อย่างปลอดภัยจากโครงสร้างที่ขัดแย้งกันนี้โดยสันนิษฐานว่าคุณจะได้รับหมายเลข
Number myNum = myNums.get(0); //compiler-error
อย่างที่คุณเห็นถ้าคอมไพเลอร์อนุญาตให้คุณเขียนบรรทัดนี้คุณจะได้ ClassCastException ขณะใช้งานจริง
รับ / ใส่หลักการ
ดังนั้นให้ใช้ความแปรปรวนร่วมเมื่อคุณตั้งใจจะเอาค่าทั่วไปออกจากโครงสร้างใช้ความแปรปรวนเมื่อคุณตั้งใจจะใส่ค่าทั่วไปลงในโครงสร้างและใช้ประเภทสามัญที่แน่นอนเมื่อคุณต้องการทำทั้งสองอย่าง
ตัวอย่างที่ดีที่สุดที่ฉันมีคือสิ่งต่อไปนี้ที่คัดลอกตัวเลขทุกชนิดจากรายการหนึ่งไปยังอีกรายการ มันจะได้รับไอเท็มจากแหล่งที่มาและจะวางไอเท็มในเป้าหมายเท่านั้น
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
ขอขอบคุณพลังแห่งความแปรปรวนร่วมและความแปรปรวนนี้ใช้ได้กับกรณีเช่นนี้:
List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);