Статьи

Опасности коррелирующего полиморфизма подтипа с общим полиморфизмом

Java 5 ввел общий полиморфизм в экосистему Java. Это было отличным дополнением к языку Java , даже если мы все знаем о многочисленных предостережениях из-за стирания универсального типа и его последствий. Общий полиморфизм (также известный как параметрический полиморфизм ) обычно поддерживается ортогонально, возможно, к ранее существовавшему полиморфизму подтипа . Простой пример этого — API коллекций.

1
List<? extends Number> c = new ArrayList<Integer>();

В приведенном выше примере подтип ArrayList назначается переменной List супертипа List . В то же время ArrayList параметризован с типом Integer , который может быть назначен для совместимого параметра supertype ? extends Number ? extends Number . Такое использование полиморфизма подтипа в контексте общего полиморфизма также называется ковариацией , хотя, конечно, ковариантность также может быть достигнута и в неуниверсальном контексте.

Ковариация с общим полиморфизмом

Ковариантность важна для дженериков. Это позволяет создавать системы сложного типа. Простые примеры включают использование ковариации с общими методами:

1
2
<E extends Serializable> void serialize(
    Collection<E> collection) {}

Приведенный выше пример принимает любой тип Collection , который может быть подтипирован на сайте вызова с такими типами, как List , ArrayList , Set и многими другими. В то же время аргумент универсального типа на сайте вызова должен быть только подтипом Serializable . Т.е. это может быть List<Integer> или ArrayList<String> и т. Д.

Корреляция полиморфизма подтипа с общим полиморфизмом

Люди часто заманивают в корреляцию двух ортогональных типов полиморфизма. Простым примером такой корреляции может быть специализация IntegerList или StringSet как таковая:

1
2
class IntegerList extends ArrayList<Integer> {}
class StringSet extends HashSet<String> {}

Легко видеть, что число явных типов взорвется, если вы начнете охватывать декартово произведение иерархий подтипов и обобщенных типов, желая более точно специализироваться, создавая такие вещи, как IntegerArrayList , IntegerAbstractList , IntegerLinkedList и т. Д.

Делая корреляцию родовой

Как видно выше, такие корреляции часто удаляют универсальность из иерархии типов, хотя они не обязаны это делать. Это можно увидеть в следующем, более общем примере:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// AnyContainer can contain AnyObject
class AnyContainer<E extends AnyObject> {}
class AnyObject {}
 
// PhysicalContainer contains only PhysicalObjects
class PhysicalContainer<E extends PhysicalObject>
  extends AnyContainer<E> {}
class PhysicalObject extends AnyObject {}
 
// FruitContainer contains only Fruit,
// which in turn are PhysicalObjects
class FruitContainer<E extends Fruit>
  extends PhysicalContainer<E> {}
class Fruit extends PhysicalObject {}

Приведенный выше пример является типичным примером, когда дизайнер API заманивал в корреляцию полиморфизма подтипа ( Fruit extends PhysicalObject extends AnyObject ) с универсальным полиморфизмом ( <E> ), сохраняя его универсальным, что позволяет добавлять дополнительные подтипы ниже FruitContainer . Это становится более интересным, когда AnyObject должен вообще знать свой собственный подтип. Это может быть достигнуто с помощью рекурсивного универсального параметра . Давайте исправим предыдущий пример

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// AnyContainer can contain AnyObject
class AnyContainer<E extends AnyObject<E>> {}
class AnyObject<O extends AnyObject<O>> {}
 
// PhysicalContainer contains only PhysicalObjects
class PhysicalContainer<E extends PhysicalObject<E>>
  extends AnyContainer<E> {}
class PhysicalObject<O extends PhysicalObject<O>>
  extends AnyObject<O> {}
 
// FruitContainer contains only Fruit,
// which in turn are PhysicalObjects
class FruitContainer<E extends Fruit<E>>
  extends PhysicalContainer<E> {}
class Fruit<O extends Fruit<O>>
  extends PhysicalObject<O> {}

Интересной частью здесь являются уже не контейнеры, а AnyObject типов AnyObject , которая соотносит полиморфизм подтипа с универсальным полиморфизмом своего собственного типа! Это также делается с помощью java.lang.Enum :

01
02
03
04
05
06
07
08
09
10
public class Enum<E extends Enum<E>>
implements Comparable<E> {
  public final int compareTo(E other) { ... }
  public final Class<E> getDeclaringClass() { ... }
}
 
enum MyEnum {}
 
// Which is syntactic sugar for:
final class MyEnum extends Enum<MyEnum> {}

Где лежит опасность?

Тонкое различие между перечислениями и нашей пользовательской иерархией AnyObject заключается в том, что MyEnum прекращает рекурсивную самокорреляцию двух методов ортогональной типизации, будучи final ! AnyObject другой стороны, подтипы AnyObject не должны удалять параметр универсального типа, если только они не сделаны окончательными. Пример:

1
2
3
4
5
// "Dangerous"
class Apple extends Fruit<Apple> {}
 
// "Safe"
final class Apple extends Fruit<Apple> {}

Почему final так важен, или, другими словами, почему подтипы AnyObject должны быть осторожны при прекращении рекурсивной самокорреляции, как это делала Apple раньше? Это просто. Давайте предположим следующее дополнение:

1
2
3
4
5
6
7
class AnyObject<O extends AnyObject<O>>
  implements Comparable<O> {
 
  @Override
  public int compareTo(O other) { ... }
  public AnyContainer<O> container() { ... }
}

Вышеупомянутый контракт на AnyObject.compareTo() подразумевает, что любой подтип AnyObject может только когда-либо сравниваться с тем же подтипом. Следующее невозможно:

1
2
3
4
5
Fruit<?> fruit = // ...
Vegetable<?> vegetable = // ...
 
// Compilation error!
fruit.compareTo(vegetable);

Единственный в настоящее время сопоставимый тип в иерархии — это Apple:

1
2
3
4
Apple a1 = new Apple();
Apple a2 = new Apple();
 
a1.compareTo(a2);

Но что, если мы хотим добавить GoldenDelicious и Gala ?

1
2
class GoldenDelicious extends Apple {}
class Gala extends Apple {}

Теперь мы можем сравнить их!

1
2
3
4
GoldenDelicious g1 = new GoldenDelicious();
Gala g2 = new Gala();
 
g1.compareTo(g2);

Это не было намерением автора AnyObject !

То же самое относится и к методу container() . Подтипам разрешено ковариантно специализировать тип AnyContainer , что хорошо:

1
2
3
4
5
6
class Fruit<O extends Fruit<O>>
  extends PhysicalObject<O> {
 
  @Override
  public FruitContainer<O> container() { ... }
}

Но что происходит с методом container() в GoldenDelicious и Gala ?

1
2
GoldenDelicious g = new GoldenDelicious();
FruitContainer<Apple> c = g.container();

Да, он вернет контейнер Apple , а не контейнер GoldenDelicious как задумано дизайнером AnyObject .

Полиморфизм подтипа и общий полиморфизм охватывают оси ортогонального типа. Создание их корреляции может быть запахом дизайна в вашей системе типов. Заставлять их соотноситься с одним и тем же типом опасно, так как трудно понять, как правильно. Пользователи будут пытаться завершить определение рекурсивного универсального типа для подтипа вашего базового типа. Причиной такого завершения является тот факт, что базовые типы с рекурсивными самоограничениями трудно использовать. Но завершение часто идет не так, как должно быть сделано только в final классах, а не в обычных классах или интерфейсах.

Другими словами, если вы считаете, что вам нужно рекурсивное определение универсального типа для общего базового типа, подумайте еще раз очень внимательно, действительно ли оно вам нужно и могут ли пользователи вашего типа правильно завершить определение рекурсивного универсального типа в final классе.