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
классе.