Tipo de dependencia en Java, Parte 1

Comprender la compatibilidad de tipos es fundamental para escribir buenos programas en Java, pero la interacción de las variaciones entre los elementos del lenguaje Java puede parecer muy académica para los no iniciados. ¡Este artículo está dirigido a desarrolladores de software que estén preparados para afrontar el desafío! La Parte 1 revela las relaciones covariantes y contravariantes entre elementos más simples, como tipos de matriz y tipos genéricos, así como el elemento especial del lenguaje Java, el comodín. La parte 2 explora la dependencia y la varianza de tipos en ejemplos de API comunes y en expresiones lambda.

descargar Descargar la fuente Obtenga el código fuente de este artículo, "Tipo de dependencia en Java, Parte 1". Creado para JavaWorld por el Dr. Andreas Solymosi.

Conceptos y terminología

Antes de entrar en las relaciones de covarianza y contravarianza entre varios elementos del lenguaje Java, asegurémonos de tener un marco conceptual compartido.

Compatibilidad

En la programación orientada a objetos, la compatibilidad se refiere a una relación dirigida entre tipos, como se muestra en la Figura 1.

Andreas Solymosi

Decimos que dos tipos son compatibles en Java si es posible transferir datos entre variables de los tipos. La transferencia de datos es posible si el compilador la acepta y se realiza mediante asignación o paso de parámetros. Por ejemplo, shortes compatible con intporque la asignación intVariable = shortVariable;es posible. Pero booleanno es compatible con intporque la asignación intVariable = booleanVariable;no es posible; el compilador no lo aceptará.

Debido a que la compatibilidad es una relación dirigida, a veces es compatible pero no es compatible o no de la misma manera. Veremos esto más adelante cuando lleguemos a discutir la compatibilidad explícita o implícita.T1T2T2T1

Lo que importa es que la compatibilidad entre tipos de referencia solo es posible dentro de una jerarquía de tipos. Todos los tipos de clases son compatibles con Object, por ejemplo, porque todas las clases heredan implícitamente de Object. Integersin Floatembargo, no es compatible con porque Floatno es una superclase de Integer. Integeres compatible con Number, porque Numberes una superclase (abstracta) de Integer. Debido a que están ubicados en la misma jerarquía de tipos, el compilador acepta la asignación numberReference = integerReference;.

Hablamos de compatibilidad implícita o explícita , dependiendo de si la compatibilidad debe marcarse explícitamente o no. Por ejemplo, short es implícitamente compatible con int(como se muestra arriba) pero no al revés: la asignación shortVariable = intVariable;no es posible. Sin embargo, short es explícitamente compatible con int, porque la asignación shortVariable = (short)intVariable;es posible. Aquí debemos marcar la compatibilidad por casting , también conocida como conversión de tipo.

Del mismo modo, entre los tipos de referencia: integerReference = numberReference;no es aceptable, solo integerReference = (Integer) numberReference;se aceptaría. Por lo tanto, Integeres implícitamente compatible con, Numberpero Numbersolo explícitamente compatible con Integer.

Dependencia

Un tipo puede depender de otros tipos. Por ejemplo, el tipo de matriz int[]depende del tipo primitivo int. Del mismo modo, el tipo genérico ArrayListdepende del tipo Customer. Los métodos también pueden depender del tipo, según los tipos de sus parámetros. Por ejemplo, el método void increment(Integer i); depende del tipo Integer. Algunos métodos (como algunos tipos genéricos) dependen de más de un tipo, como los métodos que tienen más de un parámetro.

Covarianza y contravarianza

La covarianza y la contravarianza determinan la compatibilidad según los tipos. En cualquier caso, la varianza es una relación dirigida. La covarianza se puede traducir como "diferente en la misma dirección" o con-diferente , mientras que contravarianza significa "diferente en la dirección opuesta" o contra-diferente . Los tipos covariantes y contravariantes no son lo mismo, pero existe una correlación entre ellos. Los nombres implican la dirección de la correlación.

Entonces, la covarianza significa que la compatibilidad de dos tipos implica la compatibilidad de los tipos que dependen de ellos. Dada la compatibilidad de tipos, se supone que los tipos dependientes son covariantes, como se muestra en la Figura 2.

Andreas Solymosi

La compatibilidad de a implica la compatibilidad de ) a ). El tipo dependiente se llama covariante ; o más precisamente, ) es covariante de ).T1T2A(T1A(T2A(T)A(T1A(T2

Para otro ejemplo: debido a que la asignación numberArray = integerArray;es posible (en Java, al menos), los tipos de matriz Integer[]y Number[]son covariantes. Entonces, podemos decir que Integer[]es implícitamente covariante a Number[]. Y aunque lo contrario no es cierto, la asignación integerArray = numberArray;no es posible, la asignación con tipo casting ( integerArray = (Integer[])numberArray;) es posible; por lo tanto, decimos, Number[]es explícitamente covariante con Integer[].

Para resumir: Integeres implícitamente compatible con Number, por Integer[]lo tanto, es implícitamente covariante Number[]y Number[]explícitamente covariante con Integer[]. La figura 3 lo ilustra.

Andreas Solymosi

En términos generales, podemos decir que los tipos de matriz son covariantes en Java. Veremos ejemplos de covarianza entre tipos genéricos más adelante en este artículo.

Contravarianza

Como la covarianza, la contravarianza es una relación dirigida . Mientras que covarianza significa con-diferente , contravarianza significa contra-diferente . Como mencioné anteriormente, los nombres expresan la dirección de la correlación . También es importante señalar que la varianza no es un atributo de tipos en general, sino solo de tipos dependientes (como matrices y tipos genéricos, y también de métodos, que discutiré en la Parte 2).

Un tipo dependiente como A(T)se llama contravariante si la compatibilidad de to implica la compatibilidad de ) to ). La figura 4 lo ilustra.T1T2A(T2A(T1

Andreas Solymosi

Un elemento del lenguaje (tipo o método) que A(T)depende de Tes covariante si la compatibilidad de to implica la compatibilidad de ) to ). Si la compatibilidad de to implica la compatibilidad de ) to ), entonces el tipo es contravariante . Si la compatibilidad de entre no implica compatibilidad entre ) y ), entonces es invariante .T1T2A(T1A(T2T1T2A(T2A(T1A(T)T1T2A(T1A(T2A(T)

Los tipos de matriz en Java no son implícitamente contravariantes , pero pueden ser explícitamente contravariantes , al igual que los tipos genéricos. Ofreceré algunos ejemplos más adelante en el artículo.

Elementos dependientes del tipo: métodos y tipos

En Java, los métodos, tipos de matrices y tipos genéricos (parametrizados) son los elementos dependientes del tipo. Los métodos dependen de los tipos de sus parámetros. Un tipo de matriz, T[], depende de los tipos de sus elementos, T. Un tipo genérico Gdepende de su parámetro de tipo, T. La figura 5 lo ilustra.

Andreas Solymosi

En su mayoría, este artículo se centra en la compatibilidad de tipos, aunque abordaré la compatibilidad entre métodos hacia el final de la Parte 2.

Compatibilidad de tipos implícita y explícita

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

Tenga en cuenta que la compatibilidad implícita en la Figura 6 asume que la relación es transitiva : shortes compatible con long.

De manera similar a lo que ve en la Figura 6, siempre es posible asignar una referencia de un subtipo a intuna referencia de un supertipo. ClassCastExceptionSin embargo, tenga en cuenta que la misma asignación en la otra dirección podría generar un , por lo que el compilador de Java lo permite solo con conversión de tipos.

Covarianza y contravarianza para tipos de matriz

En Java, algunos tipos de matrices son covariantes y / o contravariantes. En el caso de la covarianza, esto significa que si Tes compatible con U, entonces T[]también es compatible con U[]. En el caso de contravarianza, significa que U[]es compatible con T[]. Las matrices de tipos primitivos son invariantes en Java:

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

Las matrices de tipos de referencia son implícitamente covariantes y explícitamente contravariantes , sin embargo:

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

Figura 7. Covarianza implícita para matrices

Lo que esto significa, prácticamente, es que una asignación de componentes de matriz podría arrojar ArrayStoreExceptionen tiempo de ejecución. Si una referencia de matriz de SuperTypereferencias a un objeto de matriz de SubType, y uno de sus componentes se asigna a un SuperTypeobjeto, entonces:

 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 

Debido a la covarianza, el compilador no puede verificar la exactitud de la última asignación a los elementos de la matriz; la JVM hace esto y tiene un costo significativo. Sin embargo, el compilador puede optimizar el gasto si no se utiliza la compatibilidad de tipos entre los tipos de arreglos.

Andreas Solymosi

Recuerde que en Java, para una variable de referencia de algún tipo, está prohibido hacer referencia a un objeto de su supertipo: las flechas en la Figura 8 no deben estar dirigidas hacia arriba.

Variaciones y comodines en tipos genéricos

Los tipos genéricos (parametrizados) son implícitamente invariantes en Java, lo que significa que las diferentes instancias de un tipo genérico no son compatibles entre sí. Incluso el tipo de casting no resultará en compatibilidad:

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

Los errores de tipo surgen aunque subGeneric.getClass() == superGeneric.getClass(). El problema es que el método getClass()determina el tipo sin formato, por eso un parámetro de tipo no pertenece a la firma de un método. Por tanto, las dos declaraciones de métodos

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

no deben aparecer juntos en una definición de interfaz (o clase abstracta).