Java - Використовуйте параметри поліморфізму або обмеженого типу


17

Припустимо, у мене є ієрархія цього класу ...

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() {
    }
}

А тоді я….

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

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

Яка різниця при використанні параметрів обмеженого типу або поліморфізму?

А коли користуватися тим чи іншим?


1
Ці два визначення не отримують великої користі від параметрів типу. Але спробуйте написати addAnimals(List<Animal>)та додати Список котів!
Кіліан Фот

7
Ну, наприклад, якби кожен із перерахованих вище методів повернув би щось також, той, хто використовує дженерики, може повернути Т, а інший може повернути лише Тварину. Тож для людини, що використовує ці методи, у першому випадку він отримає саме те, що хотів: Dod dog = addAnimal (new Dog ()); тоді як за допомогою 2-го методу його змусять закинути, щоб отримати собаку: Dog d = (Dog) addAnimalPoly (new Dog ());
Дракон Шиван

Більшість моїх використання обмежених типів - це наклеювання шпалер через погану реалізацію дженерики Java (наприклад, Список <T> має різні правила відхилення від Т тільки). У цьому випадку немає ніяких переваг, це по суті два способи вираження тієї ж концепції, хоча , як то кажуть , @ShivanDragon це дійсно означає, що Т в compiletime замість тварини. Ви можете ставитися до нього як до тварини всередині, але можете запропонувати його як Т зовнішньо.
Фосі

Відповіді:


14

Ці два приклади еквівалентні, і насправді будуть складені в один і той же байт-код.

Є два способи, що додавання обмеженого родового типу до методу, як у першому прикладі, зробить що завгодно.

Передача параметра типу іншому типу

Ці два підписи методу виявляються однаковими в байтовому коді, але компілятор застосовує безпеку типу:

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

public static void addAnimals(Collection<Animal> animals)

У першому випадку допускається лише Collection(або підтип) AnimalУ другому випадку допускається Collection(або підтип) із загальним типом Animalабо підтипом.

Наприклад, у першому методі дозволено наступне, але не другому:

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

Причина полягає в тому, що другий дозволяє проводити колекції тварин, тоді як перший дозволяє збирати будь-який об'єкт, який можна віднести до тварин (тобто підтипи). Зауважте, що якби у цьому списку був список тварин, у яких трапилася кішка, будь-який метод прийняв би це: проблема полягає в загальній специфікації колекції, а не в тому, що вона насправді містить.

Повернення об'єктів

Інший раз, коли це важливо, це повернення об'єктів. Припустимо, існував такий метод:

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

Ви зможете зробити з ним наступне:

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

Хоча це надуманий приклад, є випадки, коли це має сенс. Без дженерики метод повинен був би повернутися, Animalі вам потрібно буде додати кастинг типів, щоб він працював (саме це компілятор додає до байтового коду все-таки поза кадром).


" У першому випадку дозволена лише колекція (або підтип) тварини. У другому випадку дозволена колекція (або підтип) із загальним типом тварини або підтипу. " - Ви хочете подвоїти- перевірити свою логіку там?
Позов про Моніку

3

Використовуйте дженерики замість того, щоб применшити. "Downcasting" - це погано, переходячи від більш загального типу до більш конкретного:

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

... ви довіряєте, що aце кішка, але компілятор не може цього гарантувати. Це може виявитися собакою під час виконання.

Ось де ви використовуєте дженерики:

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

Тепер ви можете вказати, що потрібно мисливця на котів:

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

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

Тепер компілятор може гарантує , що hunterC буде вловлювати тільки кішка, і hunterD буде тільки захоплення собаки.

Тому просто використовуйте регулярний поліморфізм, якщо ви просто хочете обробляти конкретні класи як їх базовий тип. Оновлення - це добра річ. Але якщо ви потрапляєте в ситуацію, коли вам потрібно обробляти конкретні класи як власний тип, загалом, використовуйте дженерики.

Або, дійсно, якщо вам здається, що вам доведеться перейти на спад, тоді використовуйте дженерики.

EDIT: більш загальний випадок, коли ви хочете відкласти рішення про те, які типи обробляти. Так типи стають параметрами, а також значеннями.

Скажіть, я хочу, щоб мій клас "Зоопарк" поводився з кішками або губками. У мене немає спільного суперкласу. Але я все одно можу використовувати:

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

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

ступінь, на яку ви це заблокуєте, залежить від того, що ви намагаєтеся зробити;)


2

Це питання є застарілим, але, здається, важливим фактором, який слід враховувати, коли використовувати параметри поліморфізму проти обмеженого типу. Цей фактор може бути дещо дотичним до прикладу, наведеного у запитанні, але, я вважаю, дуже важливим для більш загального "Коли використовувати параметри поліморфізму проти обмеженого типу?"

TL; DR

Якщо ви коли-небудь опинитеся переміщенням коду з підкласу до базового класу проти вашого кращого судження, через неможливість отримати доступ до нього в поліморфний спосіб, параметри обмеженого типу можуть стати потенційним рішенням.

Повний відповідь

Параметри обмеженого типу можуть піддавати конкретні, не успадковані методи підкласу для змінної елемента, що передається. Поліморфізм не може

Щоб уточнити, розширивши приклад:

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

Якщо для абстрактного класу AnimalOwner було визначено полі protected Animal pet;та поліморфізм, компілятор помилиться у pet.scratchBelly();рядку, повідомляючи, що цей метод не визначений для Animal.


1

У вашому прикладі ви не (і не повинні) використовувати обмежений тип. Використовуйте параметри обмеженого типу лише тоді, коли це потрібно , оскільки вони більш заплутані для розуміння.

Ось декілька ситуацій, коли ви будете використовувати параметри обмеженого типу:

  • Параметри колекцій

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

    тоді ви можете зателефонувати

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

    zoo.add(dogs)не вдасться компілювати без <? extends Animal>, тому що дженерики не є коваріантними.

  • Підкласифікація

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

    щоб обмежити тип підкласу, який можна надати.

Ви також можете використовувати кілька діапазонів, <T extends A1 & A2 & A3>щоб переконатися, що тип є підтипом усіх типів у списку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.