Параметры типа времени компиляции с несколькими границами - PullRequest
2 голосов
/ 11 июня 2019

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

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

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

1 Ответ

1 голос
/ 11 июня 2019

В принципе, нет, вы не можете.Концептуальная причина в том, как родовые типы связаны.Когда вы создаете класс, такой как Zoo<T extends Animal>, T не означает , что означает «любой тип, который расширяет Animal», это означает, что «определенный тип, который расширяет Animal, будет предоставлен вво время выполнения».Обычно это позволяет вам делать то, что вы хотите, но ваш случай, по-видимому, проверяет границы (ba-dum-ткань) этой системы.

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

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

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

Еще одна опция, которая работает только частично:

  // 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=SparrowT, и 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...?

Что, похоже, полностью нарушает систему типов.


Короче говоря ... это может работать не так, как вы хотите.

...