Зависимост на типа в Java, част 1

Разбирането на съвместимостта на типа е от основно значение за писането на добри Java програми, но взаимодействието на различията между езиковите елементи на Java може да изглежда изключително академично за непосветените. Тази статия е за разработчици на софтуер, готови да се справят с предизвикателството! Част 1 разкрива ковариантните и контравариантните връзки между по-прости елементи като типове масиви и родови типове, както и специалния елемент на езика Java, заместващия знак. Част 2 изследва зависимостта и вариацията на типа в общи примери за API и в ламбда изрази.

изтегляне Изтегляне на източника Вземете изходния код за тази статия, „Зависимост на типа в Java, част 1.“ Създаден за JavaWorld от д-р Андреас Солимоси.

Понятия и терминология

Преди да влезем във взаимоотношенията на ковариация и контравариация между различни езикови елементи на Java, нека бъдем сигурни, че имаме споделена концептуална рамка.

Съвместимост

В обектно-ориентираното програмиране съвместимостта се отнася до насочена връзка между типовете, както е показано на фигура 1.

Андреас Солимоси

Казваме, че два типа са съвместими в Java, ако е възможно да се прехвърлят данни между променливи на типовете. Прехвърлянето на данни е възможно, ако компилаторът го приеме и се извършва чрез присвояване или предаване на параметри. Като пример shortе съвместим с, intзащото възлагането intVariable = shortVariable;е възможно. Но booleanне е съвместим с, intзащото заданието intVariable = booleanVariable;не е възможно; компилаторът няма да го приеме.

Тъй като съвместимостта е насочена връзка, понякога е съвместима с, но не е съвместима с или не по същия начин. Ще видим това по-нататък, когато стигнем до обсъждане на явна или неявна съвместимост.T1T2T2T1

Важното е, че съвместимостта между референтните типове е възможна само в рамките на типова йерархия. Всички типове класове са съвместими 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.

Андреас Солимоси

Съвместимостта на до предполага съвместимостта на ) до ). Зависимият тип се нарича ковариант ; или по-точно, ) е ковариант на ).T1T2A(T1A(T2A(T)A(T1A(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 илюстрира.T1T2A(T2A(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); 

не трябва да се срещат заедно в дефиниция на интерфейс (или абстрактен клас).