Разбирането на съвместимостта на типа е от основно значение за писането на добри Java програми, но взаимодействието на различията между езиковите елементи на Java може да изглежда изключително академично за непосветените. Тази статия е за разработчици на софтуер, готови да се справят с предизвикателството! Част 1 разкрива ковариантните и контравариантните връзки между по-прости елементи като типове масиви и родови типове, както и специалния елемент на езика Java, заместващия знак. Част 2 изследва зависимостта и вариацията на типа в общи примери за API и в ламбда изрази.
изтегляне Изтегляне на източника Вземете изходния код за тази статия, „Зависимост на типа в Java, част 1.“ Създаден за JavaWorld от д-р Андреас Солимоси.Понятия и терминология
Преди да влезем във взаимоотношенията на ковариация и контравариация между различни езикови елементи на Java, нека бъдем сигурни, че имаме споделена концептуална рамка.
Съвместимост
В обектно-ориентираното програмиране съвместимостта се отнася до насочена връзка между типовете, както е показано на фигура 1.
Андреас СолимосиКазваме, че два типа са съвместими в Java, ако е възможно да се прехвърлят данни между променливи на типовете. Прехвърлянето на данни е възможно, ако компилаторът го приеме и се извършва чрез присвояване или предаване на параметри. Като пример short
е съвместим с, int
защото възлагането intVariable = shortVariable;
е възможно. Но boolean
не е съвместим с, int
защото заданието intVariable = booleanVariable;
не е възможно; компилаторът няма да го приеме.
Тъй като съвместимостта е насочена връзка, понякога е съвместима с, но не е съвместима с или не по същия начин. Ще видим това по-нататък, когато стигнем до обсъждане на явна или неявна съвместимост.T1
T2
T2
T1
Важното е, че съвместимостта между референтните типове е възможна само в рамките на типова йерархия. Всички типове класове са съвместими Object
например, защото всички класове наследяват имплицитно от Object
. Integer
не е съвместим с Float
, тъй като Float
не е суперклас на Integer
. Integer
е съвместим с Number
, защото Number
е (абстрактен) суперклас на Integer
. Тъй като те се намират в еднотипна йерархия, компилаторът приема заданието numberReference = integerReference;
.
Говорим за неявна или явна съвместимост, в зависимост от това дали съвместимостта трябва да бъде маркирана изрично или не. Например, краткото е имплицитно съвместимо с int
(както е показано по-горе), но не и обратното: присвояването shortVariable = intVariable;
не е възможно. Краткото обаче е изрично съвместимо с int
, тъй като възлагането shortVariable = (short)intVariable;
е възможно. Тук трябва да отбележим съвместимостта чрез кастинг , известен също като преобразуване на типа.
По същия начин, сред референтните типове: integerReference = numberReference;
не е приемливо, само integerReference = (Integer) numberReference;
ще бъде прието. Следователно Integer
е имплицитно съвместим с, Number
но Number
е само изрично съвместим с Integer
.
Зависимост
Един тип може да зависи от други типове. Например типът масив int[]
зависи от примитивния тип int
. По същия начин родовият тип ArrayList
зависи от типа Customer
. Методите също могат да зависят от типа, в зависимост от типовете на техните параметри. Например методът void increment(Integer i)
; зависи от вида Integer
. Някои методи (като някои родови типове) зависят от повече от един тип - например методи с повече от един параметър.
Ковариация и контравариация
Ковариацията и контравариацията определят съвместимостта въз основа на типовете. И в двата случая дисперсията е насочена връзка. Ковариацията може да се преведе като „различна в една и съща посока“ или с-различна , докато контравариацията означава „различна в обратната посока“ или срещу различна . Ковариантните и контравариантните типове не са едно и също, но има връзка между тях. Имената предполагат посоката на корелацията.
И така, ковариацията означава, че съвместимостта на два типа предполага съвместимостта на типовете, зависими от тях. При дадена съвместимост на типа се приема, че зависимите типове са ковариантни, както е показано на фигура 2.
Андреас СолимосиСъвместимостта на до предполага съвместимостта на ) до ). Зависимият тип се нарича ковариант ; или по-точно, ) е ковариант на ).T1
T2
A(T1
A(T2
A(T)
A(T1
A(T2
За друг пример: тъй като присвояването numberArray = integerArray;
е възможно (поне в Java), типовете масиви Integer[]
и Number[]
са ковариантни. Така че, можем да кажем, че Integer[]
е имплицитно ковариантно на Number[]
. И докато обратното не е вярно - заданието integerArray = numberArray;
не е възможно - възлагането с тип casting ( integerArray = (Integer[])numberArray;
) е възможно; следователно, казваме, Number[]
е изрично ковариантно на Integer[]
.
Да обобщим: Integer
е имплицитно съвместим с Number
, следователно Integer[]
е имплицитно ковариант на Number[]
и Number[]
е изрично ковариант на Integer[]
. Фигура 3 илюстрира.
Най-общо казано, можем да кажем, че типовете масиви са ковариантни в Java. Ще разгледаме примери за ковариация сред родови типове по-нататък в статията.
Контравариантност
Подобно на ковариацията, контравариацията е насочена връзка. Докато ковариацията означава с-различно , контравариацията означава срещу-различно . Както вече споменах, имената изразяват посоката на корелацията . Също така е важно да се отбележи, че отклонението не е атрибут на типовете като цяло, а само на зависимите типове (като масиви и родови типове, а също и на методи, които ще обсъдя в част 2).
А зависим тип като A(T)
се нарича contravariant ако съвместимостта да предполага съвместимостта ) до ). Фигура 4 илюстрира.T1
T2
A(T2
A(T1
A language element (type or method) A(T)
depending on T
is covariant if the compatibility of T1
to T2
implies the compatibility of A(T1
) to A(T2
). If the compatibility of T1
to T2
implies the compatibility of A(T2
) to A(T1
), then the type A(T)
is contravariant. If the compatibility of T1
between T2
does not imply any compatibility between A(T1
) and A(T2
), then A(T)
is invariant.
Array types in Java are not implicitly contravariant, but they can be explicitly contravariant , just like generic types. I'll offer some examples later in the article.
Типозависими елементи: Методи и типове
В Java методите, типовете масиви и общите (параметризирани) типове са зависимите от типа елементи. Методите зависят от видовете техни параметри. Тип масив,, T[]
зависи от типовете на неговите елементи T
,. Общият тип G
зависи от параметъра на неговия тип T
,. Фигура 5 илюстрира.
Тази статия се фокусира предимно върху съвместимостта на типовете, въпреки че ще засегна съвместимостта между методите към края на част 2.
Неявна и явна съвместимост на типа
Earlier, you saw the type T1
being implicitly (or explicitly) compatible to T2
. This is only true if the assignment of a variable of type T1
to a variable of type T2
is allowed without (or with) tagging. Type casting is the most frequent way to tag explicit compatibility:
variableOfTypeT2 = variableOfTypeT1; // implicit compatible variableOfTypeT2 = (T2)variableOfTypeT1; // explicit compatible
For example, int
is implicitly compatible to long
and explicitly compatible to short
:
int intVariable = 5; long longVariable = intVariable; // implicit compatible short shortVariable = (short)intVariable; // explicit compatible
Implicit and explicit compatibility exists not only in assignments, but also in passing parameters from a method call to a method definition and back. Together with input parameters, this means also passing a function result, which you would do as an output parameter.
Note that boolean
isn't compatible to any other type, nor can a primitive and a reference type ever be compatible.
Method parameters
We say, a method reads input parameters and writes output parameters. Parameters of primitive types are always input parameters. A return value of a function is always an output parameter. Parameters of reference types can be both: if the method changes the reference (or a primitive parameter), the change remains within the method (meaning it isn't visible outside the method after the call--this is known as call by value). If the method changes the referred object, however, the change remains after being returned from the method--this is known as call by reference.
A (reference) subtype is implicitly compatible to its supertype, and a supertype is explicitly compatible to its subtype. This means that reference types are compatible only within their hierarchy branch--upward implicitly and downward explicitly:
referenceOfSuperType = referenceOfSubType; // implicit compatible referenceOfSubType = (SubType)referenceOfSuperType; // explicit compatible
The Java compiler typically allows implicit compatibility for an assignment only if there is no danger of losing information at runtime between the different types. (Note, however, that this rule isn't valid for losing precision, such as in an assignment from int
to float.) For example, int
is implicitly compatible to long
because a long
variable holds every int
value. In contrast, a short
variable does not hold any int
values; thus, only explicit compatibility is allowed between these elements.
Имайте предвид, че неявната съвместимост на фигура 6 приема, че връзката е преходна : short
е съвместима с long
.
Подобно на това, което виждате на фигура 6, винаги е възможно да се присвои референция на подтип int
референция на супертип. Имайте предвид, че една и съща задача в другата посока може да хвърли a ClassCastException
, така че Java компилаторът го позволява само с леене на тип.
Ковариация и контравариация за типове масиви
В Java някои типове масиви са ковариантни и / или контравариантни. В случай на ковариация, това означава, че ако T
е съвместим с U
, тогава T[]
е съвместим и с U[]
. В случай на контравариация, това означава, че U[]
е съвместимо с T[]
. Масивите от примитивни типове са инвариантни в Java:
longArray = intArray; // type error shortArray = (short[])intArray; // type error
Масивите от референтни типове са имплицитно ковариантни и изрично контравариантни , но:
SuperType[] superArray; SubType[] subArray; ... superArray = subArray; // implicit covariant subArray = (SubType[])superArray; // explicit contravariant
Андреас Солимоси
Фигура 7. Неявна ковариация за масиви
Това на практика означава, че присвояването на компоненти на масива може да хвърли ArrayStoreException
по време на изпълнение. Ако SuperType
препратка към масив препраща към обект на масив от SubType
и един от неговите компоненти след това се присвоява на SuperType
обект, тогава:
superArray[1] = new SuperType(); // throws ArrayStoreException
This is sometimes called the covariance problem. The true problem is not so much the exception (which could be avoided with programming discipline), but that the virtual machine must check every assignment in an array element at runtime. This puts Java at an efficiency disadvantage against languages without covariance (where a compatible assignment for array references is prohibited) or languages like Scala, where covariance can be switched off.
An example for covariance
In a simple example, the array reference is of type Object[]
but the array object and the elements are of different classes:
Object[] objectArray; // array reference objectArray = new String[3]; // array object; compatible assignment objectArray[0] = new Integer(5); // throws ArrayStoreException
Поради ковариантността, компилаторът не може да провери правилността на последното присвояване на елементите на масива - JVM прави това, и то със значителни разходи. Компилаторът обаче може да оптимизира разходите, ако не се използва съвместимост на типа между типовете масиви.
Андреас СолимосиНе забравяйте, че в Java за референтна променлива от някакъв тип препращането към обект от нейния супертип е забранено: стрелките на фигура 8 не трябва да са насочени нагоре.
Вариации и заместващи символи в родови типове
Общите (параметризирани) типове са имплицитно инвариантни в Java, което означава, че различни инстанции на родов тип не са съвместими помежду си. Дори леенето на типове няма да доведе до съвместимост:
Generic superGeneric; Generic subGeneric; subGeneric = (Generic)superGeneric; // type error superGeneric = (Generic)subGeneric; // type error
Грешките в типа възникват, въпреки че subGeneric.getClass() == superGeneric.getClass()
. Проблемът е, че методът getClass()
определя суровия тип - ето защо параметър тип не принадлежи към сигнатурата на метод. По този начин двете декларации на методите
void method(Generic p); void method(Generic p);
не трябва да се срещат заедно в дефиниция на интерфейс (или абстрактен клас).