สวัสดี! เรายังคงศึกษาหัวข้อเรื่องยาชื่อสามัญต่อไป คุณมีความรู้มากมายเกี่ยวกับสิ่งเหล่านี้จากการบรรยายครั้งก่อน ๆ (เกี่ยวกับการใช้ varargs เมื่อทำงานกับยาชื่อสามัญและเกี่ยวกับการลบประเภท ) แต่เรายังไม่ได้กล่าวถึงหัวข้อสำคัญหนึ่งหัวข้อ: ไวด์การ์ด นี่เป็นคุณลักษณะที่สำคัญมากของยาชื่อสามัญ มากเสียจนเราทุ่มเทการบรรยายแยกต่างหากให้กับมัน! อย่างไรก็ตาม ไม่มีอะไรซับซ้อนเกี่ยวกับไวด์การ์ด คุณจะเห็นว่าตอนนี้ :)
มาดูตัวอย่างกัน:
public class Main {
public static void main(String[] args) {
String str = new String("Test!");
// ниHowих проблем
Object obj = str;
List<String> strings = new ArrayList<String>();
// ошибка компиляции!
List<Object> objects = strings;
}
}
เกิดอะไรขึ้นที่นี่? เราเห็นสถานการณ์ที่คล้ายกันมากสองสถานการณ์ ในตอนแรก เรากำลังพยายามส่งวัตถุString
เพื่อObject
พิมพ์ ไม่มีปัญหากับสิ่งนี้ ทุกอย่างทำงานได้ตามปกติ แต่ในสถานการณ์ที่สอง คอมไพเลอร์แสดงข้อผิดพลาด แม้จะดูเหมือนว่าเรากำลังทำสิ่งเดียวกันก็ตาม ตอนนี้เรากำลังใช้คอลเลกชันของวัตถุหลายชิ้น แต่เหตุใดจึงเกิดข้อผิดพลาด? โดยพื้นฐานแล้วอะไรคือความแตกต่างไม่ว่าเราจะโยนวัตถุหนึ่งชิ้นString
เป็นประเภทObject
หรือวัตถุ 20 ชิ้น? มีความแตกต่างที่สำคัญระหว่างวัตถุและ ชุดของ วัตถุ หากคลาสเป็นผู้สืบทอดของคลาสก็จะไม่ใช่ผู้สืบทอด ด้วยเหตุนี้เราจึงไม่สามารถนำของเราไป เป็นทายาทแต่มิใช่ทายาท โดยสัญชาตญาณสิ่งนี้ดูไม่สมเหตุสมผลมากนัก เหตุใดผู้สร้างภาษาจึงได้รับคำแนะนำจากหลักการนี้ ลองจินตนาการว่าคอมไพเลอร์จะไม่ทำให้เรามีข้อผิดพลาดที่นี่: B
А
Collection<B>
Collection<A>
List<String>
List<Object>
String
Object
List<String>
List<Object>
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
ในกรณีนี้ เราสามารถทำสิ่งต่อไปนี้ได้:
objects.add(new Object());
String s = strings.get(0);
เนื่องจากคอมไพลเลอร์ไม่ได้ให้ข้อผิดพลาดแก่เราและอนุญาตให้เราสร้างการอ้างอิงList<Object> object
ถึงคอลเลกชันของสตริงstrings
เราจึงสามารถเพิ่มstrings
ไม่ใช่สตริง แต่เป็นเพียงวัตถุใด ๆObject
! ดังนั้นเราจึงสูญเสียการรับประกันว่ามีเพียงออบเจ็กต์ที่ระบุในแบบทั่วไปเท่านั้นที่อยู่ในคอลเลกชันของString
เรา นั่นคือเราได้สูญเสียข้อได้เปรียบหลักของความปลอดภัยประเภทยาชื่อสามัญ และเนื่องจากคอมไพเลอร์อนุญาตให้เราทำทั้งหมดนี้ได้ นั่นหมายความว่าเราจะได้รับข้อผิดพลาดระหว่างการทำงานของโปรแกรมเท่านั้น ซึ่งแย่กว่าข้อผิดพลาดในการคอมไพล์เสมอ เพื่อป้องกันสถานการณ์ดังกล่าว คอมไพลเลอร์แจ้งข้อผิดพลาดแก่เรา:
// ошибка компиляции
List<Object> objects = strings;
...และเตือนเขาว่า เขา ไม่ใช่List<String>
ทายาท List<Object>
นี่เป็นกฎที่เข้มงวดสำหรับการดำเนินการของยาชื่อสามัญ และจะต้องจำไว้เมื่อใช้ยาเหล่านี้ เดินหน้าต่อไป สมมติว่าเรามีลำดับชั้นขนาดเล็ก:
public class Animal {
public void feed() {
System.out.println("Animal.feed()");
}
}
public class Pet extends Animal {
public void call() {
System.out.println("Pet.call()");
}
}
public class Cat extends Pet {
public void meow() {
System.out.println("Cat.meow()");
}
}
ที่หัวของลำดับชั้นเป็นเพียงสัตว์: สัตว์เลี้ยงสืบทอดมาจากพวกมัน สัตว์เลี้ยงแบ่งออกเป็น 2 ประเภท - สุนัขและแมว ทีนี้ลองจินตนาการว่าเราต้องสร้างวิธีการiterateAnimals()
ง่ายๆ เมธอดควรยอมรับการรวบรวมสัตว์ใดๆ ( Animal
, Pet
, Cat
, Dog
) วนซ้ำองค์ประกอบทั้งหมด และส่งออกบางสิ่งไปยังคอนโซลในแต่ละครั้ง ลองเขียนวิธีนี้:
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
ดูเหมือนว่าปัญหาจะได้รับการแก้ไขแล้ว! อย่างไรก็ตามอย่างที่เราเพิ่งค้นพบList<Cat>
หรือList<Dog>
ไม่ใช่List<Pet>
ทายาทList<Animal>
! ดังนั้น เมื่อเราพยายามเรียกใช้เมธอดiterateAnimals()
ด้วยรายการ cat เราจะได้รับข้อผิดพลาดของคอมไพเลอร์:
import java.util.*;
public class Main3 {
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
public static void main(String[] args) {
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
//ошибка компилятора!
iterateAnimals(cats);
}
}
สถานการณ์ดูไม่ดีสำหรับเรา! ปรากฎว่าเราจะต้องเขียนวิธีแยกกันในการแจกแจงสัตว์ทุกประเภท? จริงๆ แล้ว ไม่ คุณไม่จำเป็นต้อง :) และไวด์การ์ด จะช่วยเราในเรื่องนี้ ! เราจะแก้ไขปัญหาด้วยวิธีการง่ายๆ เพียงวิธีเดียวโดยใช้โครงสร้างต่อไปนี้:
public static void iterateAnimals(Collection<? extends Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
นี่คือไวด์การ์ด แม่นยำยิ่งขึ้น นี่เป็นไวด์การ์ดประเภทแรกจากหลายประเภท - “ ขยาย ” (อีกชื่อหนึ่งคือUpper Bounded Wildcards ) การออกแบบนี้บอกอะไรเราบ้าง? ซึ่งหมายความว่าวิธีการนี้ใช้เป็นอินพุตคอลเลกชันของวัตถุคลาสAnimal
หรือวัตถุของคลาสสืบทอดใดAnimal (? extends Animal)
ๆ Animal
กล่าวอีกนัยหนึ่ง วิธีการสามารถยอมรับคอลเลก ชันPet
หรือDog
เป็นอินพุตCat
ก็ไม่สร้างความแตกต่าง มาตรวจสอบให้แน่ใจกันว่ามันใช้งานได้:
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
iterateAnimals(dogs);
}
เอาต์พุตคอนโซล:
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
เราได้สร้างคอลเลกชันทั้งหมด 4 รายการและออบเจ็กต์ 8 รายการ และมีรายการในคอนโซลทั้งหมด 8 รายการ ทุกอย่างใช้งานได้ดี! :) สัญลักษณ์ตัวแทนช่วยให้เราปรับตรรกะที่จำเป็นได้อย่างง่ายดายโดยเชื่อมโยงกับประเภทเฉพาะในวิธีเดียว เราไม่จำเป็นต้องเขียนวิธีการแยกต่างหากสำหรับสัตว์แต่ละประเภท ลองนึกภาพว่าเราจะมีวิธีการกี่วิธีหากใบสมัครของเราถูกนำไปใช้ในสวนสัตว์หรือคลินิกสัตวแพทย์ :) ตอนนี้เรามาดูสถานการณ์อื่นกันดีกว่า ลำดับชั้นการสืบทอดของเราจะยังคงเหมือนเดิม: คลาสระดับบนสุดคือAnimal
ด้านล่างคือคลาส Pets Pet
และในระดับถัดไปคือCat
และ Dog
ตอนนี้คุณต้องเขียนวิธีการใหม่iretateAnimals()
เพื่อให้สามารถใช้ได้กับสัตว์ทุกประเภทยกเว้นสุนัข Collection<Animal>
นั่นคือควรยอมรับ หรือCollection<Pet>
เป็นอินพุตCollection<Cat>
แต่ไม่ควรทำงานCollection<Dog>
กับ เราจะบรรลุเป้าหมายนี้ได้อย่างไร? ดูเหมือนว่าโอกาสในการเขียนวิธีการแยกสำหรับแต่ละประเภทกำลังปรากฏต่อหน้าเราอีกครั้ง :/ เราจะอธิบายตรรกะของเราให้คอมไพเลอร์ฟังได้อย่างไร และสามารถทำได้ง่ายมาก! ไวด์การ์ดจะมาช่วยเหลือเราอีกครั้ง แต่คราวนี้เราจะใช้ประเภทอื่น - “ super ” (อีกชื่อหนึ่งคือLower Bounded Wildcards )
public static void iterateAnimals(Collection<? super Cat> animals) {
for(int i = 0; i < animals.size(); i++) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
หลักการก็คล้ายกันที่นี่ โครงสร้าง<? super Cat>
จะบอกคอมไพลเลอร์ว่าวิธีการนี้ สามารถใช้เป็น ชุดiterateAnimals()
อินพุตของอ็อบเจ็กต์ของคลาสCat
หรือคลาสบรรพบุรุษอื่นๆ ในกรณีของเรา ตัวคลาสเอง , บรรพบุรุษของคลาส - และบรรพบุรุษของบรรพบุรุษ - เหมาะสมกับคำ อธิบายCat
นี้ คลาสไม่พอดีกับข้อจำกัดนี้ ดังนั้นการพยายามใช้เมธอดที่มีรายการจะส่งผลให้เกิดข้อผิดพลาดในการคอมไพล์: Cat
Pets
Animal
Dog
List<Dog>
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
//ошибка компиляции!
iterateAnimals(dogs);
}
ปัญหาของเราได้รับการแก้ไขแล้ว และสัญลักษณ์แทนกลับกลายเป็นว่ามีประโยชน์อย่างยิ่งอีกครั้ง :) นี่เป็นการสรุปการบรรยาย ตอนนี้คุณคงเห็นแล้วว่าหัวข้อเรื่องยาชื่อสามัญมีความสำคัญเพียงใดเมื่อเรียน Java - เราใช้เวลาบรรยายถึง 4 ครั้ง! แต่ตอนนี้คุณเข้าใจหัวข้อนี้ดีแล้วและจะสามารถพิสูจน์ตัวเองในการสัมภาษณ์ได้ :) และตอนนี้ก็ถึงเวลากลับไปสู่ภารกิจแล้ว! ขอให้โชคดีในการศึกษาของคุณ! :)
GO TO FULL VERSION