Java - Sử dụng đa hình hoặc tham số loại giới hạn


17

Giả sử tôi có hệ thống phân cấp lớp này ...

public abstract class Animal {
    public abstract void eat();
    public abstract void talk();
}
class Dog extends Animal {
    @Override
    public void eat() {
    }

    @Override
    public void talk() {
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
    }

    @Override
    public void talk() {
    }
}

Và rồi tôi có ....

public static <T extends Animal> void addAnimal(T animal) {
    animal.eat();
    animal.talk();
}

public static void addAnimalPoly(Animal animal) {
    animal.eat();
    animal.talk();
}

Sự khác biệt khi sử dụng tham số loại đa hình hoặc đa hình là gì?

Và khi nào nên sử dụng cái này hay cái kia?


1
Hai định nghĩa này không thu được nhiều lợi nhuận từ các tham số loại. Nhưng hãy thử viết addAnimals(List<Animal>)và thêm Danh sách Mèo!
Kilian Foth

7
Chà, ví dụ, nếu mỗi phương thức trên của bạn sẽ trả về một cái gì đó tốt, thì phương thức sử dụng thuốc generic có thể trả về T, trong khi phương thức kia chỉ có thể trả về Động vật. Vì vậy, đối với người sử dụng các phương pháp đó, trong trường hợp đầu tiên, anh ta sẽ lấy lại chính xác những gì anh ta muốn: Dod dog = addAnimal (new Dog ()); trong khi với phương pháp thứ 2, anh ta buộc phải bỏ vai để có được một con chó: Dog d = (Dog) addAnimalPoly (new Dog ());
Shivan Dragon

Phần lớn việc sử dụng các loại giới hạn của tôi là tạo ra ảnh hưởng đối với việc triển khai các khái quát kém của Java (Ví dụ, Danh sách <T> có các quy tắc phương sai khác nhau đối với T). Trong trường hợp này không có lợi thế, về cơ bản, đó là hai cách để thể hiện cùng một khái niệm, mặc dù như @ShivanDragon nói rằng điều đó có nghĩa là bạn có T tại thời điểm thay vì Động vật. Bạn chỉ có thể coi nó như một con vật bên trong, nhưng bạn có thể cung cấp nó như một T bên ngoài.
Phoshi

Câu trả lời:


14

Hai ví dụ này là tương đương và trên thực tế sẽ biên dịch theo cùng mã byte.

Có hai cách để thêm một kiểu chung bị ràng buộc vào một phương thức như trong ví dụ đầu tiên của bạn sẽ làm bất cứ điều gì.

Truyền tham số loại cho loại khác

Hai chữ ký phương thức này cuối cùng giống nhau trong mã byte, nhưng trình biên dịch thực thi an toàn kiểu:

public static <T extends Animal> void addAnimals(Collection<T> animals)

public static void addAnimals(Collection<Animal> animals)

Trong trường hợp đầu tiên, chỉ cho phép một Collection(hoặc kiểu con) Animal. Trong trường hợp thứ hai, một Collection(hoặc kiểu con) với một loại chung Animalhoặc một kiểu con được cho phép.

Ví dụ, điều sau đây được cho phép trong phương thức đầu tiên nhưng không phải là phương thức thứ hai:

List<Cat> cats = new ArrayList<Cat>();
cats.add(new Cat());
addAnimals(cats);

Lý do là cái thứ hai chỉ cho phép các bộ sưu tập động vật, trong khi cái thứ nhất cho phép các bộ sưu tập của bất kỳ đối tượng nào có thể gán cho động vật (tức là các kiểu con). Lưu ý rằng nếu danh sách này là danh sách các động vật tình cờ có chứa một con mèo, thì một trong hai phương pháp sẽ chấp nhận nó: vấn đề là đặc điểm chung của bộ sưu tập, chứ không phải những gì nó thực sự chứa.

Trả lại đồ vật

Lần khác nó quan trọng là với các đối tượng trở lại. Chúng ta hãy giả sử phương pháp sau tồn tại:

public static <T extends Animal> T feed(T animal) {
  animal.eat();
  return animal;
}

Bạn sẽ có thể làm như sau với nó:

Cat c1 = new Cat();
Cat c2 = feed(c1);

Trong khi đây là một ví dụ giả định, có những trường hợp nó có ý nghĩa. Nếu không có khái quát, phương thức sẽ phải trả về Animalvà bạn sẽ cần thêm kiểu truyền để làm cho nó hoạt động (đó là những gì trình biên dịch thêm vào mã byte dù sao đằng sau hậu trường).


" Trong trường hợp đầu tiên, chỉ cho phép Bộ sưu tập (hoặc tiểu loại) của Động vật. Trong trường hợp thứ hai, Bộ sưu tập (hoặc tiểu loại) có loại Động vật chung hoặc loại phụ được cho phép. " - Bạn có muốn nhân đôi kiểm tra logic của bạn ở đó?
Vụ kiện của Quỹ Monica

3

Sử dụng thuốc generic thay vì downcasting. "Downcasting" là xấu, đi từ một loại tổng quát hơn đến một loại cụ thể hơn:

Animal a = hunter.captureOne();
Cat c = (Cat)a;  // ACK!!!!!! What if it's a Dog? ClassCastException!

... Bạn đang tin rằng đó alà một con mèo, nhưng trình biên dịch không thể đảm bảo điều đó. Nó có thể trở thành một con chó trong thời gian chạy.

Đây là nơi bạn sử dụng thuốc generic:

public class <A> Hunter() {
    public A captureOne() { ... }
}

Bây giờ bạn có thể chỉ định rằng bạn muốn một thợ săn mèo:

Hunter<Cat> hunterC = new Hunter<Cat>();
Cat c = hunterC.captureOne();

Hunter<Dog> hunterD = new Hunter<Dog>();
Dog d = hunterD.captureOne();

Bây giờ trình biên dịch có thể đảm bảo rằng hunterC sẽ chỉ bắt mèo và hunterD sẽ chỉ bắt chó.

Vì vậy, chỉ sử dụng đa hình thông thường nếu bạn chỉ muốn xử lý các lớp cụ thể như loại cơ sở của chúng. Upcasting là một điều tốt. Nhưng nếu bạn gặp phải một tình huống mà bạn cần xử lý các lớp cụ thể theo kiểu riêng của họ, nói chung, hãy sử dụng thuốc generic.

Hoặc, thực sự, nếu bạn thấy bạn phải downcast thì sử dụng generic.

EDIT: trường hợp tổng quát hơn là khi bạn muốn đưa ra quyết định loại nào sẽ xử lý. Vì vậy, các loại trở thành một tham số, cũng như các giá trị.

Nói rằng tôi muốn lớp Zoo của tôi xử lý Mèo hoặc Bọt biển. Tôi không có một siêu hạng phổ biến. Nhưng tôi vẫn có thể sử dụng:

public class <T> Zoo() { ... }

Zoo<Sponge> spongeZoo = ...
Zoo<Cat> catZoo = ...

mức độ mà bạn khóa nó phụ thuộc vào những gì bạn đang cố gắng làm;)


2

Câu hỏi này đã cũ, nhưng một yếu tố quan trọng cần xem xét dường như đã bị bỏ qua liên quan đến thời điểm sử dụng đa hình so với tham số loại giới hạn. Yếu tố này có thể hơi tiếp xúc với ví dụ được đưa ra trong câu hỏi, nhưng, tôi cảm thấy, rất phù hợp với khái quát hơn "Khi nào nên sử dụng đa hình so với tham số loại giới hạn?"

TL; DR

Nếu bạn thấy mình chuyển mã từ lớp con sang lớp cơ sở chống lại sự phán xét tốt hơn của bạn, vì không thể truy cập nó theo cách đa hình, các tham số loại giới hạn có thể là một giải pháp tiềm năng.

Câu trả lời đầy đủ

Các tham số kiểu giới hạn có thể hiển thị các phương thức lớp con cụ thể, không được kế thừa cho một biến thành viên được kế thừa. Đa hình không thể

Để giải thích bằng cách mở rộng ví dụ của bạn:

public abstract class AnimalOwner<T extends Animal> {
   protected T pet;
   public abstract void rewardPet();
}

// Modify the dog class
class Dog extends Animal {
   // ...
   // This method is not inherited from anywhere!
   public void scratchBelly() {
      System.out.println("Belly: Scratched");
   }
}

class DogOwner extends AnimalOwner<Dog> {
   DogOwner(Dog dog) {
     this.pet = dog;
   }

   @Override
   public void rewardPet()
   {
      // ---- Note this call ----
      pet.scratchBelly();
   }
}

Nếu lớp trừu tượng AnimalOwner được xác định là có protected Animal pet;và chọn tham gia đa hình, trình biên dịch sẽ lỗi trên pet.scratchBelly();dòng, cho bạn biết rằng phương thức này không được xác định cho Animal.


1

Trong ví dụ của bạn, bạn không (và không nên) sử dụng loại giới hạn. Chỉ sử dụng các tham số loại giới hạn khi bạn phải , vì chúng khó hiểu hơn.

Dưới đây là một số tình huống mà bạn sẽ sử dụng các tham số loại giới hạn:

  • Thông số bộ sưu tập

    class Zoo {
    
      private List<Animal> animals;
    
      public void add(Collection<? extends Animal> newAnimals) {
        animals.addAll(newAnimals);
      }
    }

    sau đó bạn có thể gọi

    List<Dog> dogs = ...
    zoo.add(dogs);

    zoo.add(dogs)sẽ không biên dịch mà không có <? extends Animal>, bởi vì generic không phải là covariant.

  • Phân lớp

    abstract class Warrior<T extends Weapon> {
    
      public abstract T getWeapon();
    }

    để giới hạn lớp con loại có thể cung cấp.

Bạn cũng có thể sử dụng nhiều giới hạn <T extends A1 & A2 & A3>để đảm bảo một loại là một kiểu con của tất cả các loại trong danh sách.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.