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?
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:
Animal
), it's up to typed subclass to enforce typing of elementsAnother 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.