CodeGym/Blog Java/Ngẫu nhiên/Ký tự đại diện trong thuốc generic

Ký tự đại diện trong thuốc generic

Xuất bản trong nhóm
CHÀO! Hãy tiếp tục nghiên cứu về thuốc generic. Bạn đã có được một lượng kiến ​​thức đáng kể về chúng từ các bài học trước (về cách sử dụng varargs khi làm việc với generics và về loại eraser ), nhưng có một chủ đề quan trọng mà chúng ta chưa xem xét — ký tự đại diện . Đây là 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 một bài học riêng cho nó! Điều đó nói rằng, không có gì đặc biệt phức tạp về ký tự đại diện. Bạn sẽ thấy ngay thôi :) 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!");
       // No problem
       Object obj = str;

       List<String> strings = new ArrayList<String>();
       // Compilation error!
       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 trường hợp, chúng ta truyền một Stringđối tượng tới một Objectđối tượng. Không có vấn đề gì ở đây — mọi thứ hoạt động như mong đợi. Nhưng trong tình huống thứ hai, trình biên dịch tạo ra lỗi. Nhưng chúng ta đang làm điều tương tự, phải không? Lần này chúng ta chỉ đơn giản là sử dụng một tập hợp nhiều đối tượng. Nhưng tại sao lỗi xảy ra? Có gì khác biệt? Chúng ta đang truyền một Stringđối tượng tới một 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 Blớp là con của Alớp thì Collection<B>không phải là con của Collection<A>. Đây là lý do tại sao chúng tôi không thể chuyển List<String>sang mộtList<Object>. Stringlà con của Object, nhưng List<String>không phải là con của List<Object>. Điều này có vẻ không siêu trực quan. Tại sao những người tạo ra ngôn ngữ lại làm theo cách này? Hãy tưởng tượng rằng trình biên dịch không báo lỗi cho chúng ta:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
Trong trường hợp này, chẳng hạn, chúng ta có thể làm như sau:
objects.add(new Object());
String s = strings.get(0);
Bởi vì trình biên dịch không đưa ra bất kỳ lỗi nào cho chúng tôi và cho phép chúng tôi tạo một List<Object>tham chiếu trỏ đến strings, nên chúng tôi có thể thêm bất kỳ Objectđối tượng cũ nào vào stringsbộ sưu tập! Do đó, chúng tôi đã mất sự đảm bảo rằng bộ sưu tập của chúng tôi chỉ chứa các Stringđối tượng được chỉ định bởi đối số kiểu trong lệnh gọi kiểu chung . Nói cách khác, chúng tôi đã đánh mất lợi thế chính của thuốc generic - loại an toàn. Và bởi vì trình biên dịch không ngăn chúng ta làm điều này, nên chúng ta sẽ chỉ gặp lỗi trong thời gian chạy, điều 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ư thế này, trình biên dịch báo lỗi cho chúng ta:
// Compilation error
List<Object> objects = strings;
...và nhắc nhở chúng ta rằng đó List<String>không phải là hậu duệ của List<Object>. Đây là một quy tắc chắc chắn đối với thuốc generic, và nó phải được ghi nhớ khi làm việc với 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()");
   }
}
Hệ thống phân cấp được đứng đầu bởi một lớp Animal đơn giản, được kế thừa bởi Pet. Pet có 2 phân lớp: Chó và Mèo. Bây giờ giả sử rằng chúng ta cần tạo một iterateAnimals()phương thức đơn giản. Phương thức này sẽ lấy một tập hợp bất kỳ động vật nào ( Animal, Pet, Cat, Dog), lặp lại trên tất cả các phần tử và hiển thị thông báo trên bảng điều khiển trong mỗi lần lặp. Hãy thử viết một phương thức như vậy:
public static void iterateAnimals(Collection<Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
Có vẻ như vấn đề đã được giải quyết! Tuy nhiên, như chúng ta đã biết gần đây, List<Cat>List<Dog>không List<Pet>phải là hậu duệ của List<Animal>! Điều này có nghĩa là khi chúng ta cố gắng gọi iterateAnimals()phương thức với danh sách mèo, chúng ta sẽ gặp lỗi biên dịch:
import java.util.*;

public class Main3 {


   public static void iterateAnimals(Collection<Animal> animals) {

       for(Animal animal: animals) {

           System.out.println("Another iteration in the loop!");
       }
   }

   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());

       // Compilation error!
       iterateAnimals(cats);
   }
}
Tình hình có vẻ không tốt lắm cho chúng ta! Chúng ta có phải viết các phương pháp riêng biệt để liệt kê từng loại động vật không? Trên thực tế, không, chúng tôi không :) Và khi điều đó xảy ra, các ký tự đại diện giúp chúng tôi thực hiện điều này! Chúng ta có thể giải quyết vấn đề bằng một phương pháp đơn giản bằng cách sử dụng cấu trúc sau:
public static void iterateAnimals(Collection<? extends Animal> animals) {

   for(Animal animal: animals) {

       System.out.println("Another iteration in the loop!");
   }
}
Đây là một ký tự đại diện. Chính xác hơn, đây là loại đầu tiên trong một số loại ký tự đại diện. Nó được gọi là ký tự đại diện giới hạn trên và được biểu thị bằng ? kéo dài . Cấu trúc này cho chúng ta biết điều gì? Điều này có nghĩa là phương thức chấp nhận một tập hợp Animalcác đối tượng hoặc một tập hợp các đối tượng của bất kỳ lớp nào có nguồn gốc từ Animal(? extends Animal). Nói cách khác, phương thức có thể chấp nhận một tập hợp gồm Animal, Pet, Doghoặc Catđối tượng — điều đó không có gì khác biệt. Hãy thuyết phục bản thân rằng nó 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 bảng điều khiển:
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
Another iteration in the loop!
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 trên bảng điều khiển. Mọi thứ 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 gắn với các loại cụ thể vào một phương thức duy nhất. 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ần bao nhiêu phương pháp nếu ứng dụng của chúng ta được sở thú hoặc văn phòng thú y sử dụng :) Nhưng bây giờ hãy xem xét một tình huống khác. Hệ thống phân cấp thừa kế của chúng tôi vẫn không thay đổi: lớp cấp cao nhất là Animal, với Petlớp ngay bên dưới và lớp CatDogở cấp độ tiếp theo. Bây giờ bạn cần viết lại iterateAnimals()phương thức để nó hoạt động với bất kỳ loại động vật nào, ngoại trừ chó . Đó là, nó nên chấp nhận Collection<Animal>,Collection<Pet>hoặc Collection<Car>, nhưng nó không hoạt động với Collection<Dog>. Làm thế nào chúng ta có thể đạt được điều này? Có vẻ như chúng ta lại phải đối mặt với triển vọng viết một phương thức riêng biệt cho từng loại :/ Còn cách nào khác để chúng ta giải thích cho trình biên dịch những gì chúng ta muốn xảy ra? Nó thực sự khá đơn giản! Một lần nữa, các ký tự đại diện hỗ trợ chúng tôi ở đây. Nhưng lần này chúng ta sẽ sử dụng một loại ký tự đại diện khác — ký tự đại diện có giới hạn dưới , được biểu thị bằng cách sử dụng super .
public static void iterateAnimals(Collection<? super Cat> animals) {

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

       System.out.println("Another iteration in the loop!");
   }
}
Ở đây nguyên tắc là tương tự. Cấu <? super Cat>trúc cho trình biên dịch biết rằng iterateAnimals()phương thức có thể chấp nhận làm đầu vào cho một tập hợp Catcác đối tượng hoặc bất kỳ tổ tiên nào của Catlớp làm đầu vào. Trong trường hợp này, Catlớp, cha của nó Petvà cha của cha mẹ của nó, Animaltất cả đều phù hợp với mô tả này. Lớp này Dogkhông phù hợp với giới hạn của chúng ta, vì vậy nếu cố gắng sử dụng phương thức có List<Dog>đối số 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);

   // Compilation error!
   iterateAnimals(dogs);
}
Chúng tôi đã giải quyết được vấn đề của mình và một lần nữa, các ký tự đại diện lại cực kỳ hữu ích :) Đến đây, bài học đã kết thúc. Bây giờ bạn đã thấy khái quát quan trọng như thế nào trong quá trình học Java của bạn — chúng ta đã có 4 bài học về chúng! Nhưng bây giờ bạn đã thông thạo chủ đề này và bạn có thể chứng minh kỹ năng của mình trong các cuộc phỏng vấn xin việc :) Và bây giờ, đã đến lúc quay lại với nhiệm vụ! Chúc bạn thành công trong học tập! :)
Bình luận
  • Phổ biến
  • Mới
Bạn phải đăng nhập để đăng nhận xet
Trang này chưa có bất kỳ bình luận nào