Параметры типа времени компиляции с несколькими границами

Могу ли я использовать параметры типа с несколькими границами, чтобы во время компиляции гарантировать, что содержимое контейнера соответствует определенным характеристикам?

Это, вероятно, лучше всего выражается в коде:

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 принимал и страуса, и воробья, и отвергал сурикатов и буйволов. Есть ли другой способ построить такой контейнер?

Зачем BirdZoo расширять Animal?

Nicholas Kurian 11.06.2019 06:23

@NicholasK нет, но есть параметр типа. BirdZoo сам расширяет Zoo, а его параметр типа расширяет Animal, потому что все Zoos должны содержать Animals.

romacafe 11.06.2019 06:31

@Derefactoro Да, видел. Вопрос в том, используете ли вы это для создания контейнера с гарантиями времени компиляции, что его содержимое соответствует всем границам?

romacafe 11.06.2019 06:46
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
3
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В принципе, нет, вы не можете. Концептуальная причина заключается в том, как связаны универсальные типы. Когда вы создаете такой класс, как 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 при следующем.

Очевидно, что у этого есть некоторые недостатки по сравнению с желаемым дизайном:

  1. Базовое хранилище является «сырым» (в данном случае просто Animal), типизированный подкласс обеспечивает типизацию элементов.
  2. Точно так же извлекать элементы из хранилища и сохранять известный тип сложно/запутанно.
  3. Требуется шаблонный метод в каждом подклассе + доступ к помощнику для кодирования типов в методах.

Другой вариант, который работает только частично:

  // 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" -- Я запомню этот способ мышления о параметрах ограниченного типа.

romacafe 11.06.2019 07:40

«конкретный тип, который расширяет Animal, который будет предоставлен в время выполнения». На самом деле вызов универсального типа — это процесс времени компиляции.

SDJ 11.06.2019 07:52

Другие вопросы по теме