Digitare dipendenza in Java, parte 1

Comprendere la compatibilità dei tipi è fondamentale per scrivere buoni programmi Java, ma l'interazione delle varianze tra gli elementi del linguaggio Java può sembrare altamente accademica per chi non lo sapesse. Questo articolo è per gli sviluppatori di software pronti ad affrontare la sfida! La parte 1 rivela le relazioni covarianti e controvarianti tra elementi più semplici come i tipi di array e i tipi generici, così come l'elemento speciale del linguaggio Java, il carattere jolly. La parte 2 esplora la dipendenza e la varianza del tipo negli esempi API comuni e nelle espressioni lambda.

download Scarica il codice sorgente Ottieni il codice sorgente per questo articolo, "Dipendenza dei tipi in Java, parte 1." Creato per JavaWorld dal Dr. Andreas Solymosi.

Concetti e terminologia

Prima di entrare nelle relazioni di covarianza e controvarianza tra i vari elementi del linguaggio Java, assicuriamoci di avere una struttura concettuale condivisa.

Compatibilità

Nella programmazione orientata agli oggetti, la compatibilità si riferisce a una relazione diretta tra i tipi, come mostrato nella Figura 1.

Andreas Solymosi

Diciamo che due tipi sono compatibili in Java se è possibile trasferire dati tra variabili dei tipi. Il trasferimento dei dati è possibile se il compilatore lo accetta e viene eseguito tramite assegnazione o passaggio di parametri. Ad esempio, shortè compatibile con intperché l'assegnazione intVariable = shortVariable;è possibile. Ma booleannon è compatibile con intperché l'assegnazione intVariable = booleanVariable;non è possibile; il compilatore non lo accetterà.

Poiché la compatibilità è una relazione diretta, a volte è compatibile con, ma non è compatibile , o non allo stesso modo. Lo vedremo ulteriormente quando arriveremo a discutere di compatibilità esplicita o implicita.T1T2T2T1

Ciò che conta è che la compatibilità tra i tipi di riferimento è possibile solo all'interno di una gerarchia di tipi. Tutti i tipi di classe sono compatibili Object, ad esempio, perché tutte le classi ereditano implicitamente da Object. Integernon è compatibile con Float, tuttavia, perché Floatnon è una superclasse di Integer. Integerè compatibile con Number, perché Numberè una superclasse (astratta) di Integer. Poiché si trovano nella stessa gerarchia di tipi, il compilatore accetta l'assegnazione numberReference = integerReference;.

Si parla di compatibilità implicita o esplicita , a seconda che la compatibilità debba essere contrassegnata esplicitamente o meno. Ad esempio, short è implicitamente compatibile con int(come mostrato sopra) ma non viceversa: l'assegnazione shortVariable = intVariable;non è possibile. Tuttavia, short è esplicitamente compatibile con int, perché l'assegnazione shortVariable = (short)intVariable;è possibile. Qui dobbiamo contrassegnare la compatibilità mediante il casting , noto anche come conversione del tipo.

Allo stesso modo, tra i tipi di riferimento: integerReference = numberReference;non è accettabile, integerReference = (Integer) numberReference;sarebbe solo accettato. Pertanto, Integerè implicitamente compatibile con, Numberma Numberè solo esplicitamente compatibile con Integer.

Dipendenza

Un tipo potrebbe dipendere da altri tipi. Ad esempio, il tipo di matrice int[]dipende dal tipo primitivo int. Allo stesso modo, il tipo generico ArrayListdipende dal tipo Customer. I metodi possono anche dipendere dal tipo, a seconda dei tipi dei loro parametri. Ad esempio, il metodo void increment(Integer i); dipende dal tipo Integer. Alcuni metodi (come alcuni tipi generici) dipendono da più di un tipo, come i metodi che hanno più di un parametro.

Covarianza e controvarianza

Covarianza e controvarianza determinano la compatibilità in base ai tipi. In entrambi i casi, la varianza è una relazione diretta. La covarianza può essere tradotta come "diverso nella stessa direzione" o con-diverso , mentre controvarianza significa "diverso nella direzione opposta" o contro-diverso . I tipi covariante e controvariante non sono la stessa cosa, ma esiste una correlazione tra loro. I nomi implicano la direzione della correlazione.

Quindi, covarianza significa che la compatibilità di due tipi implica la compatibilità dei tipi che dipendono da loro. Data la compatibilità dei tipi, si presume che i tipi dipendenti siano covarianti, come mostrato nella Figura 2.

Andreas Solymosi

La compatibilità di a implica la compatibilità di ) a ). Il tipo dipendente è chiamato covariante ; o più precisamente, ) è covariante a ).T1T2A(T1A(T2A(T)A(T1A(T2

Per un altro esempio: poiché l'assegnazione numberArray = integerArray;è possibile (almeno in Java), i tipi di array Integer[]e Number[]sono covarianti. Quindi, possiamo dire che Integer[]è implicitamente covariante a Number[]. E mentre non è vero il contrario - l'assegnazione integerArray = numberArray;non è possibile - l'assegnazione con tipo casting ( integerArray = (Integer[])numberArray;) è possibile; quindi, diciamo, Number[]è esplicitamente covariante a Integer[].

Riassumendo: Integerè implicitamente compatibile con Number, quindi Integer[]è implicitamente covariante con Number[]ed Number[]è esplicitamente covariante con Integer[]. La figura 3 illustra.

Andreas Solymosi

In generale, possiamo dire che i tipi di array sono covarianti in Java. Vedremo esempi di covarianza tra tipi generici più avanti nell'articolo.

Contravarianza

Come la covarianza, la controvarianza è una relazione diretta . Mentre covarianza significa con-diverso , controvarianza significa contro-diverso . Come ho accennato in precedenza, i nomi esprimono la direzione della correlazione . È anche importante notare che la varianza non è un attributo di tipi in generale, ma solo di tipi dipendenti (come array e tipi generici, e anche di metodi, di cui parlerò nella Parte 2).

Un tipo dipendente come A(T)è chiamato controvariante se la compatibilità di a implica la compatibilità di ) a ). La Figura 4 illustra.T1T2A(T2A(T1

Andreas Solymosi

Un elemento del linguaggio (tipo o metodo) A(T)dipendente da Tè covariante se la compatibilità di a implica la compatibilità di ) a ). Se la compatibilità di a implica la compatibilità di ) a ), il tipo è controvariante . Se la compatibilità di between non implica alcuna compatibilità tra ) e ), allora è invariante .T1T2A(T1A(T2T1T2A(T2A(T1A(T)T1T2A(T1A(T2A(T)

I tipi di array in Java non sono implicitamente controvarianti , ma possono essere esplicitamente controvarianti , proprio come i tipi generici. Offrirò alcuni esempi più avanti nell'articolo.

Elementi dipendenti dal tipo: metodi e tipi

In Java, metodi, tipi di array e tipi generici (parametrizzati) sono gli elementi dipendenti dal tipo. I metodi dipendono dai tipi dei loro parametri. Un tipo di matrice, T[], dipende dal tipo di elementi, T. Un tipo generico Gdipende dal suo parametro tipo T. La Figura 5 illustra.

Andreas Solymosi

Per lo più questo articolo si concentra sulla compatibilità dei tipi, anche se toccherò la compatibilità tra i metodi verso la fine della Parte 2.

Compatibilità dei tipi implicita ed esplicita

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.

Andreas Solymosi

Si noti che la compatibilità implicita nella Figura 6 presuppone che la relazione sia transitiva : shortè compatibile con long.

Simile a quello che vedi nella Figura 6, è sempre possibile assegnare un riferimento di un sottotipo a intun riferimento di un supertipo. Tieni presente che la stessa assegnazione nell'altra direzione potrebbe generare un ClassCastException, tuttavia, quindi il compilatore Java lo consente solo con il casting del tipo.

Covarianza e controvarianza per i tipi di matrice

In Java, alcuni tipi di array sono covarianti e / o controvarianti. Nel caso della covarianza, ciò significa che se Tè compatibile con U, allora T[]è anche compatibile con U[]. In caso di controvarianza, significa che U[]è compatibile con T[]. Gli array di tipi primitivi sono invarianti in Java:

 longArray = intArray; // type error shortArray = (short[])intArray; // type error 

Le matrici di tipi di riferimento sono implicitamente covarianti ed esplicitamente controvarianti , tuttavia:

 SuperType[] superArray; SubType[] subArray; ... superArray = subArray; // implicit covariant subArray = (SubType[])superArray; // explicit contravariant 
Andreas Solymosi

Figura 7. Covarianza implicita per gli array

Ciò significa, in pratica, che un'assegnazione di componenti di array potrebbe generare ArrayStoreExceptionin fase di esecuzione. Se un riferimento di matrice di fa SuperTyperiferimento a un oggetto di matrice di SubTypee uno dei suoi componenti viene quindi assegnato a un SuperTypeoggetto, allora:

 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 

A causa della covarianza, il compilatore non può verificare la correttezza dell'ultima assegnazione agli elementi dell'array: la JVM lo fa, e con un costo significativo. Tuttavia, il compilatore può ottimizzare la spesa, se non viene utilizzata la compatibilità dei tipi tra i tipi di array.

Andreas Solymosi

Ricorda che in Java, per una variabile di riferimento di qualche tipo che fa riferimento a un oggetto del suo supertipo è vietato: le frecce nella Figura 8 non devono essere dirette verso l'alto.

Varianze e caratteri jolly nei tipi generici

I tipi generici (parametrizzati) sono implicitamente invarianti in Java, il che significa che le diverse istanze di un tipo generico non sono compatibili tra loro. Anche il casting del tipo non risulterà in compatibilità:

 Generic superGeneric; Generic subGeneric; subGeneric = (Generic)superGeneric; // type error superGeneric = (Generic)subGeneric; // type error 

Gli errori di tipo si verificano anche se subGeneric.getClass() == superGeneric.getClass(). Il problema è che il metodo getClass()determina il tipo grezzo: questo è il motivo per cui un parametro di tipo non appartiene alla firma di un metodo. Pertanto, le due dichiarazioni di metodo

 void method(Generic p); void method(Generic p); 

non devono ricorrere insieme in una definizione di interfaccia (o classe astratta).