Compile-time Type Parameters with Multiple Bounds

romacafe :

Can I use Type Parameters with multiple bounds to guarantee at compile-time that a container's contents conform to certain traits?

This is probably best expressed in code:

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
    }
}

What I want is for BirdZoo to accept both Ostrich and Sparrow, and reject Meerkat and Buffalo. Is there any other way to construct such a container?

Mshnik :

Basically, no you can't. The conceptual reason is in how generic types are bound. When you create a class like Zoo<T extends Animal>, T doesn't mean "any type that extends Animal", it means, "a specific type that extends Animal that will be provided at runtime". Normally this lets you do what you want, but your case appears to be testing the bounds (ba-dum-tiss) of this system.

I think the more nitty-gritty answer will have to go into the wildcard (?) binding system - something about ? extends A & B means that it can't prove that a type C that extends A & B & D actually matches.

A (worse) design that accomplishes your goal looks like this:

  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

With the type parameter encoded in the method signature, the type system is free to pick a new T with each method call, which allows it to bind to Ostrich on one call and Sparrow on the next.

Obviously, this has some downsides compared to your desired design:

  1. Underlying storage is "raw" (in this case just Animal), it's up to typed subclass to enforce typing of elements
  2. Similarly, getting elements out of storage and keeping known type is hard / messy.
  3. Requires boilerplate method in each subclass + access to helper to encode type in methods.

Another option that only works part of the way:

  // 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

The fact that it complains about casting X to T sort of indicates the issue we're dancing around. For example, when the instance is constructed with BirdZoo<?>, we're mostly ok. On the other hand, imagine the zoo was constructed using BirdZoo<Ostrich> and ostrichBirdZoo.addAnimal(new Sparrow()); is called. Then we have T=Ostrich and X=Sparrow. Both T and X extend both Animal and Bird, but T != X, BUT the static nor type checker isn't smart enough to tell!

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...?

Which seems to break the type system entirely.


So long story short... this may not work the way you want.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=156185&siteId=1
Recommended