Могу ли я использовать параметры типа с несколькими границами, чтобы во время компиляции гарантировать, что содержимое контейнера соответствует определенным характеристикам?
Это, вероятно, лучше всего выражается в коде:
public class TestCase {
// our base type
public static abstract class Animal { }
// a container of animals, but we'd like to restrict to certain kinds
public static class Zoo<T extends Animal> {
private List<T> animals = new ArrayList<>();
public void addAnimal(T animal) {
this.animals.add(animal);
}
public List<T> getAnimals() {
return Collections.unmodifiableList(animals);
}
}
// Animal traits - we want to build a Zoo to only house animals with some traits
public static interface Bird {}
public static interface Mammal {}
public static interface Large {}
// An assortment of animals with different traits
public static class Sparrow extends Animal implements Bird { }
public static class Ostrich extends Animal implements Bird, Large { }
public static class Meerkat extends Animal implements Mammal { }
public static class Buffalo extends Animal implements Mammal, Large { }
// some different types of zoos we could build
public static class BirdZoo<T extends Animal & Bird> extends Zoo<T> {}
public static class LargeAnimalZoo<T extends Animal & Large> extends Zoo<T> {}
public static class LargeMammalZoo<T extends Animal & Large & Mammal> extends Zoo<T> {}
// BirdZoo should accept Ostrich & Sparrow, not Meerkat or Buffalo
public static void main(String[] args) {
BirdZoo rawBirdZoo = new BirdZoo();
rawBirdZoo.addAnimal(new Ostrich()); // warning - unchecked
rawBirdZoo.addAnimal(new Sparrow()); // warning - unchecked
rawBirdZoo.addAnimal(new Meerkat()); // warning - unchecked
rawBirdZoo.addAnimal(new Buffalo()); // warning - unchecked
BirdZoo<?> wildBirdZoo = new BirdZoo<>();
wildBirdZoo.addAnimal(new Ostrich()); // error - incompatible types
wildBirdZoo.addAnimal(new Sparrow()); // error - incompatible types
wildBirdZoo.addAnimal(new Meerkat()); // error - incompatible types
wildBirdZoo.addAnimal(new Buffalo()); // error - incompatible types
BirdZoo<? extends Bird> boundedBirdZoo_B = new BirdZoo<>();
boundedBirdZoo_B.addAnimal(new Ostrich()); // error - incompatible types
boundedBirdZoo_B.addAnimal(new Sparrow()); // error - incompatible types
boundedBirdZoo_B.addAnimal(new Meerkat()); // error - incompatible types
boundedBirdZoo_B.addAnimal(new Buffalo()); // error - incompatible types
BirdZoo<? extends Animal> boundedBirdZoo_A = new BirdZoo();
boundedBirdZoo_A.addAnimal(new Ostrich()); // error - incompatible types
boundedBirdZoo_A.addAnimal(new Sparrow()); // error - incompatible types
boundedBirdZoo_A.addAnimal(new Meerkat()); // error - incompatible types
boundedBirdZoo_A.addAnimal(new Buffalo()); // error - incompatible types
BirdZoo<Ostrich> ostrichZoo = new BirdZoo<>();
ostrichZoo.addAnimal(new Ostrich());
ostrichZoo.addAnimal(new Sparrow()); // error - incompatible types
ostrichZoo.addAnimal(new Meerkat()); // error - incompatible types
ostrichZoo.addAnimal(new Buffalo()); // error - incompatible types
}
}
Я хочу, чтобы BirdZoo принимал и страуса, и воробья, и отвергал сурикатов и буйволов. Есть ли другой способ построить такой контейнер?
@NicholasK нет, но есть параметр типа. BirdZoo
сам расширяет Zoo
, а его параметр типа расширяет Animal
, потому что все Zoo
s должны содержать Animal
s.
@Derefactoro Да, видел. Вопрос в том, используете ли вы это для создания контейнера с гарантиями времени компиляции, что его содержимое соответствует всем границам?
В принципе, нет, вы не можете. Концептуальная причина заключается в том, как связаны универсальные типы. Когда вы создаете такой класс, как Zoo<T extends Animal>
, T
не означает «любой тип, который расширяет Animal
», это означает «конкретный тип, который расширяет Animal
, который будет предоставлен во время выполнения». Обычно это позволяет вам делать то, что вы хотите, но ваш случай, похоже, проверяет границы (ba-dum-tiss) этой системы.
Я думаю, что более конкретный ответ должен будет войти в систему привязки подстановочных знаков (?
) - что-то в ? extends A & B
означает, что он не может доказать, что тип C
, который расширяет A & B & D
, действительно соответствует.
(Хуже) дизайн, который достигает вашей цели, выглядит так:
public static abstract class Zoo{
private List<Animal> animals = new ArrayList<>();
protected void addAnimalHelper(Animal animal) {
this.animals.add(animal);
}
}
public static class BirdZoo extends Zoo {
public <T extends Animal & Bird> void addAnimal(T animal) {
addAnimalHelper(animal);
}
}
BirdZoo birdZoo = new BirdZoo();
birdZoo.addAnimal(new Ostrich()); // ok
birdZoo.addAnimal(new Sparrow()); // ok
birdZoo.addAnimal(new Meerkat()); // Meekrat doesn't conform to Bird
birdZoo.addAnimal(new Buffalo()); // Buffalo doesn't conform to Bird
С параметром типа, закодированным в сигнатуре метода, система типов может свободно выбирать новый T
при каждом вызове метода, что позволяет ей привязываться к Ostrich при одном вызове и к Sparrow при следующем.
Очевидно, что у этого есть некоторые недостатки по сравнению с желаемым дизайном:
Animal
), типизированный подкласс обеспечивает типизацию элементов.Другой вариант, который работает только частично:
// Note - inheritance isn't used here since it breaks due to method overriding issues.
public static class BirdZoo<T extends Animal & Bird> {
private List<T> animals = new ArrayList<>();
public <X extends Animal & Bird> void addAnimal(X animal) {
this.animals.add((T)animal); // Unchecked cast warning
}
public List<T> getAnimals() {
return Collections.unmodifiableList(animals);
}
}
BirdZoo<?> wildBirdZoo = new BirdZoo<>();
wildBirdZoo.addAnimal(new Ostrich()); // ok
wildBirdZoo.addAnimal(new Sparrow()); // ok
wildBirdZoo.addAnimal(new Meerkat()); // error - incompatible types
wildBirdZoo.addAnimal(new Buffalo()); // error - incompatible types
Тот факт, что он жалуется на приведение X
к T
, указывает на проблему, вокруг которой мы танцуем. Например, когда экземпляр создан с помощью BirdZoo<?>
, в основном все в порядке. С другой стороны, представьте, что зоопарк был построен с использованием BirdZoo<Ostrich>
и называется ostrichBirdZoo.addAnimal(new Sparrow());
. Тогда у нас есть T=Ostrich
и X=Sparrow
. И T
, и X
расширяют и Animal
, и Bird
, но T != X
, НО ни статическая, ни проверка типов недостаточно умны, чтобы сказать!
BirdZoo<?> wildBirdZoo = new BirdZoo<>();
wildBirdZoo.addAnimal(new Ostrich()); // ok
wildBirdZoo.addAnimal(new Sparrow()); // ok
BirdZoo<Ostrich> ostrichBirdZoo = new BirdZoo<>();
ostrichBirdZoo.addAnimal(new Ostrich()); // ok
ostrichBirdZoo.addAnimal(new Sparrow()); // ok and doesn't throw at runtime --> Sparrow to Ostrich cast succeeds.
System.out.println(wildBirdZoo.getAnimals());
System.out.println(ostrichBirdZoo.getAnimals()); // Contains a sparrow...?
Что, кажется, полностью нарушает систему типов.
Короче говоря... это может не сработать так, как вы хотите.
Отличный ответ, спасибо!! it means, "a specific type that extends Animal that will be provided at runtime"
-- Я запомню этот способ мышления о параметрах ограниченного типа.
«конкретный тип, который расширяет Animal, который будет предоставлен в время выполнения». На самом деле вызов универсального типа — это процесс времени компиляции.
Зачем
BirdZoo
расширятьAnimal
?