JavaRush /Blog Java /Random-VI /Ký tự đại diện trong Java Generics

Ký tự đại diện trong Java Generics

Xuất bản trong nhóm
Xin chào! Chúng tôi tiếp tục nghiên cứu chủ đề về thuốc generic. Bạn đã có một lượng kiến ​​thức khá tốt về chúng từ các bài giảng trước (về cách sử dụng biến thể khi làm việc với các tổng quát và về cách xóa kiểu ), nhưng chúng ta vẫn chưa đề cập đến một chủ đề quan trọng: ký tự đại diện . Đây là một tính năng rất quan trọng của thuốc generic. Nhiều đến mức chúng tôi đã dành hẳn một bài giảng riêng cho nó! Tuy nhiên, không có gì phức tạp về ký tự đại diện, bây giờ bạn sẽ thấy điều đó :) Ký tự đại diện trong thuốc generic - 1Hãy xem một ví dụ:
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;
   }
}
Những gì đang xảy ra ở đây? Chúng tôi thấy hai tình huống rất giống nhau. Trong phần đầu tiên, chúng tôi đang cố gắng chuyển một đối tượng Stringsang loại Object. Không có vấn đề gì với điều này, mọi thứ đều hoạt động như bình thường. Nhưng trong tình huống thứ hai, trình biên dịch đưa ra lỗi. Mặc dù có vẻ như chúng tôi đang làm điều tương tự. Chỉ là bây giờ chúng ta đang sử dụng một tập hợp nhiều đối tượng. Nhưng tại sao lại xảy ra lỗi? Về cơ bản, sự khác biệt là gì khi chúng ta chuyển một đối tượng Stringthành một loại Objecthay 20 đối tượng? Có một sự khác biệt quan trọng giữa một đối tượng và một tập hợp các đối tượng . Nếu một lớp là lớp kế thừa của một lớp thì nó không phải là lớp kế thừa . Chính vì lý do này mà chúng tôi không thể mang của mình đến . là người thừa kế nhưng không phải là người thừa kế . Bằng trực giác, điều này có vẻ không logic lắm. Tại sao những người tạo ra ngôn ngữ lại được hướng dẫn bởi nguyên tắc này? Hãy tưởng tượng rằng trình biên dịch sẽ không báo lỗi cho chúng ta ở đây: BАCollection<B>Collection<A>List<String>List<Object>StringObjectList<String>List<Object>
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
Trong trường hợp này, ví dụ: chúng ta có thể làm như sau:
objects.add(new Object());
String s = strings.get(0);
Vì trình biên dịch không cung cấp cho chúng tôi lỗi và cho phép chúng tôi tạo tham chiếu List<Object> objectđến một tập hợp các chuỗi strings, nên chúng tôi stringskhông thể thêm một chuỗi mà chỉ có thể thêm bất kỳ đối tượng nào Object! Vì vậy, chúng tôi đã mất đảm bảo rằng chỉ các đối tượng được chỉ định trong generic mới có trong bộ sưu tập của chúng tôiString . Tức là chúng ta đã đánh mất lợi thế chính của thuốc generic - an toàn về chủng loại. Và vì trình biên dịch cho phép chúng ta thực hiện tất cả những điều này, điều đó có nghĩa là chúng ta sẽ chỉ gặp lỗi trong quá trình thực thi chương trình, lỗi này luôn tệ hơn nhiều so với lỗi biên dịch. Để ngăn chặn những tình huống như vậy, trình biên dịch sẽ báo lỗi cho chúng tôi:
// ошибка компиляции
List<Object> objects = strings;
...và nhắc nhở anh ta rằng List<String>anh ta không phải là người thừa kế List<Object>. Đây là một quy tắc sắt thép cho hoạt động của thuốc generic và phải ghi nhớ khi sử dụng chúng. Tiếp tục nào. Giả sử chúng ta có một hệ thống phân cấp lớp nhỏ:
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()");
   }
}
Đứng đầu hệ thống phân cấp đơn giản là Động vật: Thú cưng được thừa hưởng từ chúng. Thú cưng được chia thành 2 loại - Chó và Mèo. Bây giờ hãy tưởng tượng rằng chúng ta cần tạo một phương thức đơn giản iterateAnimals(). Phương thức này phải chấp nhận một tập hợp bất kỳ động vật nào ( Animal, Pet, Cat, Dog), lặp qua tất cả các phần tử và xuất ra một cái gì đó cho bảng điều khiển mỗi lần. Hãy thử viết phương pháp này:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Có vẻ như vấn đề đã được giải quyết! Tuy nhiên, như chúng tôi mới phát hiện ra, List<Cat>hoặc không phải List<Dog>List<Pet>người thừa kế List<Animal>! Do đó, khi cố gắng gọi một phương thức iterateAnimals()có danh sách mèo, chúng ta sẽ gặp lỗi trình biên dịch:
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);
   }
}
Tình hình có vẻ không ổn cho chúng ta! Hóa ra chúng ta sẽ phải viết các phương pháp riêng biệt để liệt kê tất cả các loại động vật? Trên thực tế, không, bạn không cần phải làm vậy :) Và các ký tự đại diện sẽ giúp chúng ta điều này ! Chúng ta sẽ giải quyết vấn đề bằng một phương pháp đơn giản, sử dụng cấu trúc sau:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Đây là ký tự đại diện. Chính xác hơn, đây là loại đầu tiên trong số một số loại ký tự đại diện - “ mở rộng ” (tên khác là Ký tự đại diện giới hạn trên ). Thiết kế này cho chúng ta biết điều gì? Điều này có nghĩa là phương thức này lấy đầu vào là một tập hợp các đối tượng lớp Animalhoặc các đối tượng của bất kỳ lớp con cháu nào Animal (? extends Animal). AnimalNói cách khác, phương thức có thể chấp nhận bộ sưu tập , Pethoặc làm đầu vào - điều đó không tạo ra sự khác biệt nào Dog. CatHãy chắc chắn rằng điều này hoạt động:
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);
}
Đầu ra của bảng điều khiển:

Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Chúng tôi đã tạo tổng cộng 4 bộ sưu tập và 8 đối tượng và có chính xác 8 mục trong bảng điều khiển. Mọi thứ đều hoạt động tuyệt vời! :) Ký tự đại diện cho phép chúng tôi dễ dàng điều chỉnh logic cần thiết bằng cách liên kết các loại cụ thể vào một phương thức. Chúng tôi đã loại bỏ nhu cầu viết một phương pháp riêng cho từng loại động vật. Hãy tưởng tượng chúng ta sẽ có bao nhiêu phương pháp nếu ứng dụng của chúng ta được sử dụng trong vườn thú hoặc phòng khám thú y :) Bây giờ chúng ta hãy xem xét một tình huống khác. Hệ thống phân cấp kế thừa của chúng ta sẽ giữ nguyên: lớp cấp cao nhất là Animal, ngay bên dưới là lớp Pets Petvà ở cấp độ tiếp theo là CatDog. Bây giờ bạn cần viết lại phương thức iretateAnimals()để nó có thể hoạt động với bất kỳ loại động vật nào ngoại trừ chó . Collection<Animal>Nghĩa là, nó phải chấp nhận hoặc làm đầu vào nhưng không hoạt động Collection<Pet>với . Làm thế nào chúng ta có thể đạt được điều này? Có vẻ như triển vọng viết một phương thức riêng cho từng loại lại hiện ra trước mắt chúng ta:/ Làm cách nào khác để chúng ta có thể giải thích logic của mình cho trình biên dịch? Và điều này có thể được thực hiện rất đơn giản! Ở đây các ký tự đại diện sẽ lại hỗ trợ chúng ta. Nhưng lần này chúng ta sẽ sử dụng một loại khác - “ super ” (tên gọi khác là Lower Bounded Wildcards ). Collection<Cat>Collection<Dog>
public static void iterateAnimals(Collection<? super Cat> animals) {

   for(int i = 0; i < animals.size(); i++) {

       System.out.println("Еще один шаг в цикле пройден!");
   }
}
Nguyên tắc ở đây cũng tương tự. Việc xây dựng <? super Cat>cho trình biên dịch biết rằng phương thức này iterateAnimals()có thể lấy đầu vào là một tập hợp các đối tượng của lớp Cathoặc bất kỳ lớp tổ tiên nào khác Cat. CatTrong trường hợp của chúng tôi, chính lớp đó , tổ tiên của nó - Petsvà tổ tiên của tổ tiên - phù hợp với mô tả này Animal. Lớp này Dogkhông phù hợp với ràng buộc này và do đó việc cố gắng sử dụng một phương thức có danh sách List<Dog>sẽ dẫn đến lỗi biên dịch:
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);
}
Vấn đề của chúng ta đã được giải quyết và một lần nữa các ký tự đại diện lại cực kỳ hữu ích :) Bài giảng này kết thúc. Bây giờ bạn thấy chủ đề tổng quát quan trọng như thế nào khi học Java - chúng tôi đã dành 4 bài giảng về nó! Nhưng bây giờ bạn đã nắm bắt tốt chủ đề và có thể chứng tỏ bản thân tại cuộc phỏng vấn :) Và bây giờ là lúc quay lại nhiệm vụ! Chúc may mắn trong các nghiên cứu của bạn! :)
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION