Java - użyj parametrów polimorfizmu lub typu ograniczonego

17

Załóżmy, że mam tę hierarchię klas ...

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

A potem mam ....

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

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

Jaka jest różnica przy stosowaniu parametrów typu ograniczonego lub polimorfizmu?

A kiedy używać jednego lub drugiego?

HugoMelo
źródło
1
Te dwie definicje niewiele zyskują na parametrach typu. Ale spróbuj napisać addAnimals(List<Animal>)i dodać Listę kotów!
Kilian Foth,
7
Na przykład, jeśli każda z powyższych metod zwróci coś równie dobrze, to ta, która używa ogólnych, może zwrócić T, podczas gdy druga może zwrócić tylko Zwierzę. Tak więc dla osoby stosującej te metody w pierwszym przypadku odzyskałby dokładnie to, czego chciał: Dod dog = addAnimal (new Dog ()); podczas gdy przy drugiej metodzie byłby zmuszony rzucić, aby zdobyć psa: Pies d = (Pies) addAnimalPoly (nowy Pies ());
Shivan Dragon,
Większość moich zastosowań typów ograniczonych to tapetowanie z powodu słabej implementacji generycznych Javy (na przykład Lista <T> ma inne reguły wariancji niż sama T). W tym przypadku nie ma żadnej przewagi, są to zasadniczo dwa sposoby wyrażenia tej samej koncepcji, chociaż jak mówi @ShivanDragon, oznacza to, że masz T w czasie kompilacji zamiast Animal. Możesz traktować go jak zwierzę wewnętrznie, ale możesz zaoferować go jako T zewnętrznie.
Phoshi

Odpowiedzi:

14

Te dwa przykłady są równoważne i faktycznie skompilują się do tego samego kodu bajtowego.

Istnieją dwa sposoby dodania ograniczonego typu ogólnego do metody, tak jak w pierwszym przykładzie.

Przekazywanie parametru type do innego typu

Te dwie sygnatury metod są takie same w kodzie bajtów, ale kompilator wymusza bezpieczeństwo typu:

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

public static void addAnimals(Collection<Animal> animals)

W pierwszym przypadku dozwolony jest tylko Collection(lub podtyp) Animal. W drugim przypadku dozwolony jest Collection(lub podtyp) o typie ogólnym Animallub podtyp.

Na przykład dozwolone są w pierwszej metodzie, ale nie w drugiej:

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

Powodem jest to, że drugi zezwala tylko na kolekcje zwierząt, podczas gdy pierwszy zezwala na kolekcje dowolnego obiektu, który można przypisać zwierzęciu (tj. Podtypom). Zauważ, że jeśli ta lista byłaby listą zwierząt, które zawierały kota, każda metoda by to zaakceptowała: problemem jest ogólna specyfikacja kolekcji, a nie jej zawartość.

Zwracane przedmioty

Drugi czas ma znaczenie w przypadku zwracanych przedmiotów. Załóżmy, że istniała następująca metoda:

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

Można dzięki temu wykonać następujące czynności:

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

Chociaż jest to wymyślony przykład, istnieją przypadki, w których ma to sens. Bez generycznych metod metoda musiałaby powrócić Animali trzeba by dodać rzutowanie typu, aby działało (co kompilator dodaje do kodu bajtu za kulisami).


źródło
W pierwszym przypadku dozwolona jest tylko kolekcja (lub podtyp) Animal. W drugim przypadku dozwolona jest kolekcja (lub podtyp) z ogólnym typem Animal lub podtypem. ” - Czy chcesz dwukrotnie sprawdź swoją logikę?
Pozew funduszu Moniki,
3

Używaj ogólnych zamiast downcastingu. „Downcasting” jest zły, przechodząc z bardziej ogólnego typu na bardziej szczegółowy:

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

... ufasz, że ato kot, ale kompilator nie może tego zagwarantować. Może się okazać, że jest to pies w czasie wykonywania.

Oto, gdzie możesz użyć ogólnych:

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

Teraz możesz określić, że chcesz łowcę kotów:

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

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

Teraz kompilator może Gwarantujemy że hunterC przechwyci tylko koty i psy hunterD tylko przechwytywania.

Używaj więc zwykłego polimorfizmu, jeśli chcesz obsługiwać określone klasy jako ich podstawowy typ. Upcasting to dobra rzecz. Ale jeśli znajdziesz się w sytuacji, w której musisz obsługiwać określone klasy jako ich własne typy, na ogół używaj rodzajów ogólnych.

Lub, naprawdę, jeśli okaże się, że musisz spuszczać, skorzystaj z generycznych.

EDYCJA: bardziej ogólny przypadek ma miejsce, gdy chcesz odłożyć decyzję, jakie rodzaje typów obsługiwać. Zatem typy stają się parametrem, a także wartościami.

Powiedz, że chcę, aby moja klasa Zoo zajmowała się Kotami lub Gąbkami. Nie mam wspólnej super klasy. Ale nadal mogę używać:

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

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

stopień, w jakim to zablokujesz, zależy od tego, co próbujesz zrobić;)

Obrabować
źródło
2

To pytanie jest stare, ale wydaje się, że pominięto ważny czynnik dotyczący tego, kiedy użyć polimorfizmu w porównaniu do parametrów typu ograniczonego. Ten czynnik może być nieco styczny do przykładu podanego w pytaniu, ale wydaje mi się, że jest bardzo istotny dla bardziej ogólnego „Kiedy stosować polimorfizm a parametry typu ograniczonego?”

TL; DR

Jeśli kiedykolwiek zdarzy ci się przenieść kod z podklasy do klasy bazowej wbrew lepszej ocenie, z powodu niemożności uzyskania dostępu do niego w sposób polimorficzny, parametry typu ograniczonego mogą być potencjalnym rozwiązaniem.

Pełna odpowiedź

Parametry typu ograniczonego mogą ujawniać konkretne, nie odziedziczone metody podklasy dla dziedziczonej zmiennej elementu. Polimorfizm nie może

Aby rozwinąć, rozszerzając swój przykład:

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

Jeśli abstrakcyjna klasa AnimalOwner została zdefiniowana jako posiadająca protected Animal pet;i wybierająca polimorfizm, kompilator popełni błąd w pet.scratchBelly();linii, informując, że ta metoda jest niezdefiniowana dla Animal.

kredytodawca9
źródło
1

W twoim przykładzie nie (i nie powinieneś) używać ograniczonego typu. Używaj parametrów ograniczonego typu tylko wtedy, gdy musisz , ponieważ są bardziej mylące ze zrozumieniem.

Oto kilka sytuacji, w których użyjesz parametrów typu ograniczonego:

  • Parametry kolekcji

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

    wtedy możesz zadzwonić

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

    zoo.add(dogs)nie udało się skompilować bez <? extends Animal>, ponieważ generyczne nie są kowariantne.

  • Podklasowanie

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

    w celu ograniczenia podklasy typu może zapewnić.

Możesz także użyć wielu granic, <T extends A1 & A2 & A3>aby upewnić się, że typ jest podtypem wszystkich typów na liście.


źródło