Red de conocimiento informático - Aprendizaje de programación - Un pequeño problema en Java

Un pequeño problema en Java

Este tutorial le presentará los genéricos en Java. Es posible que esté familiarizado con los genéricos en otros lenguajes, especialmente las plantillas en C++. Si es así, descubrirá rápidamente las similitudes y diferencias importantes entre los dos. Es incluso mejor si no estás familiarizado con estructuras gramaticales similares, puedes empezar desde cero sin olvidar malentendidos.

Los genéricos permiten la abstracción de tipos. El ejemplo más común es el tipo de contenedor, que es cualquier clase del árbol de colección. .iterator().next();// 3

La conversión de tipos en la línea 3 es un poco molesta. Normalmente, el programador conoce los tipos de datos de una lista particular. Sin embargo, esta conversión de tipo es fundamental

. El compilador solo puede garantizar que el iterador devuelva el tipo de objeto. La conversión de tipos es necesaria para garantizar la seguridad de los tipos al asignar valores a variables de tipo Integer.

Por supuesto, esta conversión de tipos no sólo es confusa, sino que también puede generar errores en tiempo de ejecución porque los programadores pueden cometer errores. ¿Cómo pueden los programadores expresar explícitamente su intención de restringir el contenido de una lista a un tipo de datos específico? Ésta es la idea central de los genéricos. Aquí hay una versión genérica del fragmento de programa anterior:

List myIntList = new LinkedList() // 1

myIntList.add(new Integer(0); ) ); // 2

Integer x = myIntList.iterator().next(); // 3

Preste atención a la declaración de tipo de la variable myIntList. Muestra que esta no es una Lista arbitraria, sino una Lista de números enteros, escrita de la siguiente manera: Lista.

Diremos que List es una interfaz general que acepta un parámetro de tipo, en este caso Integer.

Otra cosa a tener en cuenta es que a la línea 3 le falta una conversión de tipo. Ahora bien, podría pensar que hemos simplificado con éxito el proceso. Reemplazamos la conversión de tipo en la línea 3 con el parámetro de tipo en la línea 1. Sin embargo, aquí hay una gran diferencia. El compilador ahora puede comprobar la corrección de los programas en el momento de la compilación. Cuando decimos que myIntList se declara de tipo List, esto nos dice que siempre y donde sea que se use la variable myIntList, el compilador garantiza que los elementos que contiene son del tipo correcto. Por el contrario, una conversión de tipo indica cuál cree el programador que debería ser el tipo en ese punto del código. El efecto neto de esto es una mejor legibilidad y solidez, especialmente en programas grandes.

2. Definir tipos genéricos simples

Los siguientes fragmentos están extraídos de las definiciones de las interfaces List e Iterator en el paquete java.util:

1. Definiciones de la interfaz de lista e iterador del paquete util:

lista de interfaz pública {

void add(E x);

Iterador iterador() ;

}

Iterador de interfaz pública {

E next();

booleano hasNext();

}

Todo esto debería resultarle familiar, excepto la parte entre corchetes angulares, que es la declaración de los parámetros de tipo formal de las interfaces Lista e Iterador. Estos parámetros de tipo se pueden usar en toda la declaración de la clase, y otros tipos comunes se pueden usar casi en cualquier lugar (pero existen algunas restricciones importantes, consulte la Parte 7)

En el capítulo introductorio, analizamos Comes a la llamada a una Lista de declaración de tipo genérico, como Lista.

En una llamada de este tipo (a menudo denominada tipo parametrizado), todas las apariciones de un parámetro de tipo formal (E en este caso) se reemplazan por parámetros de tipo real (en este caso, un sustituto). entero). Como puedes imaginar, List representa una versión que reemplaza todas las E con Integer:

interfaz pública IntegerList {

void add(Integer x)

Iterator< Integer> iterator();

}

Esta intuición puede ser útil, pero también puede dar lugar a malentendidos. Ayuda porque List tiene un método en su declaración para sustituciones como esta. Puede dar lugar a malentendidos porque la declaración genérica nunca se reemplaza así. No existen múltiples copias del código, tanto en el código fuente como en el código binario, en disco y en memoria. Si eres programador de C++, sabrás que esto es muy diferente a las plantillas de C++.

Una declaración de un tipo genérico se compila solo una vez y obtiene un archivo de clase, como una declaración normal de una clase o interfaz. Los parámetros de tipo son como parámetros ordinarios en un método o constructor. Así como los métodos tienen parámetros de valor formales que describen los tipos de parámetros con los que operan, las declaraciones genéricas tienen parámetros de tipo formal. Cuando se llama al método, los parámetros reales reemplazan a los parámetros formales y se ejecuta el cuerpo del método. Cuando se llama a una declaración genérica, los parámetros de tipo reales reemplazan a los parámetros de tipo formal.

Convención de nomenclatura: recomendamos utilizar nombres concisos (caracteres únicos cuando sea posible) para los parámetros de tipo formal. Es mejor no utilizar letras minúsculas, ya que esto hace que sea más fácil distinguirlo de otros parámetros formales comunes. Muchos tipos de contenedores utilizan E como tipo de sus elementos, como en el ejemplo anterior. Hay algunas otras convenciones de nomenclatura en el siguiente ejemplo.

3. Genéricos y herencia de subclases

Pongamos a prueba nuestra comprensión de los genéricos. ¿Es legal el siguiente fragmento de código?

Lista ls = new ArrayList(); //1

Lista lo = ls //2

No La línea 1 es ciertamente legal, pero la parte complicada es la línea 2. Esto plantea la pregunta: ¿una lista de cadenas es una lista de objetos? La respuesta instintiva de la mayoría de las personas es: "¡Por supuesto!" La respuesta instintiva de la mayoría de las personas es "¡Por supuesto!". Entonces, mire las siguientes líneas:

lo.add(new Object()); // 3

String s = ls.get(0); Asignar objeto a String

Aquí usamos lo para apuntar a ls. Podemos insertar cualquier objeto en él. El resultado es que lo que está almacenado en ls ya no es una cadena y obtenemos resultados inesperados cuando intentamos eliminar elementos de ella. Por supuesto, el compilador de Java evita que esto suceda. La línea 2 provoca un error de compilación.

En resumen, si Foo es un subtipo (subclase o subinterfaz) de Bar, y G es algún tipo de declaración genérica, entonces G es G!!!! ¡No aguantes!

Esta es probablemente la parte más difícil de entender al aprender genéricos porque va en contra de tu intuición. El problema de esta intuición es que supone que el conjunto no cambia. Nuestras suposiciones intuitivas

Ninguna de estas cosas está escrita en piedra. Por ejemplo, esto parecería razonable si el Departamento de Transporte (DMV) proporcionara una lista de conductores a la Oficina del Censo. Supongamos que Lista es Lista y que Conductor es un subtipo de Persona. Efectivamente, le pasamos una copia del registro de Conductor.

Sin embargo,

La Oficina del Censo puede agregar otras personas a la lista de conductores, corrompiendo los registros del Departamento de Transporte. Para manejar esta situación, es útil pensar en algunos tipos genéricos más flexibles.

Las normas que hemos visto hasta ahora son bastante restrictivas.

4. Comodines

Considere escribir una rutina que imprima todos los elementos de una colección. Esto es lo que podrías escribir usando un lenguaje antiguo:

void printCollection(Collection c) {

Iterador i = c.iterator();

for ( int k = 0; k < c.tamaño(); k++) {

System.outi.next());

}

}

Aquí hay un intento ingenuo de usar genéricos (usando la nueva sintaxis de bucle):

void printCollection(Collection< Object> c) {

for (Object e : c) {< / p>

System.out.println(e);

}

}

El problema es que la nueva versión es mucho menos útil que la anterior. Una versión antigua. La versión anterior del código puede usar cualquier tipo de colección como parámetro, mientras que la nueva versión solo puede usar Collection, que, como acabamos de explicar, no es la clase principal de todos los tipos de colecciones. Entonces, ¿cuál es la clase principal de varias colecciones? Dice:

Colección(léase: "colección desconocida")

Es una colección cuyos tipos de elementos pueden coincidir con cualquier tipo. Aparentemente, esto se llama comodín. Podemos escribir así:

void printCollection(Collection c) {

for (Object e : c) {

System.out.println ( e);

}

}

Ahora podemos llamarlo con cualquier tipo de colección. Tenga en cuenta que todavía podemos leer los elementos en c, cuyo tipo es objeto. Esto siempre es seguro,

porque no importa cuál sea el verdadero tipo de colección, ésta contiene objetos. Pero agregarle elementos arbitrarios no es seguro para los tipos:

Collection();

c.add(new Object()); / Error en tiempo de compilación

Debido a que no conocemos el tipo de elementos en c, no podemos agregarle objetos. El parámetro de tipo E del método add es el tipo de elemento de la colección. Cualquier argumento que pasemos para agregar debe ser una subclase del tipo desconocido. Como no sabemos de qué tipo es, no podemos pasar ningún parámetro. La única excepción es nula, que es miembro de todos los tipos. Por otro lado, podemos llamar al método get() y usar su valor de retorno. Se desconoce el tipo de valor de retorno, pero sabemos que siempre es un objeto, por lo que es seguro asignar el valor de retorno de get a un objeto de tipo objeto, o colocarlo en cualquier lugar donde esperaríamos que fuera de tipo objeto.

4.1. Comodines acotados

Considere un programa de dibujo simple que se puede utilizar para dibujar varias formas, como rectángulos y círculos.

Para representar estas formas en su programa, puede definir la siguiente estructura de herencia de clases:

clase abstracta pública Forma {

dibujo vacío abstracto público (Canvas c);

}

clase pública Círculo extiende Forma {

private int x, y, radio;

public void draw(Canvas c) { //.. }

}

Clase pública Rectángulo extiende Forma {

private int x, y, width, height;

public void draw. ( Canvas c) { // ...}

}

Estas clases pueden dibujar en el lienzo:

publicclass Canvas {

publicvoid draw(Shape s) {

s.draw(this);

}

}

Todas las formas normalmente Todas tienen muchas formas. Suponiendo que están representados por una lista, sería más fácil usar un método en Canvas para dibujar todas las formas:

public void drawAll(List formas) {

for (Forma s : formas) {

s.draw (this);

}

}

Ahora las reglas de tipo causan drawAll () Sólo se puede llamar desde una lista de formas. Por ejemplo, no se puede llamar desde Lista. Esto es desafortunado porque todo lo que hace este método es leer la forma de la lista, por lo que también debería poder invocarse desde List. Lo que realmente queremos es que este método pueda leer cualquier tipo de forma: Desarrollo de software www.mscto.com

public void drawAll(List formas) { //...}< / p>

Aquí hay una pequeña pero importante diferencia: reemplazamos el tipo List con List. Ahora, drawAll() puede aceptar

cualquier Lista que sea una subclase de Shape, por lo que podemos llamar a List .

Lista es un ejemplo de comodín restringido. Aquí, "..." representa un tipo desconocido, al igual que el comodín que vimos antes.

Pero aquí sabemos que este tipo desconocido es en realidad una subclase de Shape (puede ser Shape en sí o puede ser una subclase de Shape). Podemos decir que Shape es el límite superior de este comodín. Como siempre, la flexibilidad de utilizar comodines tiene un precio. La desventaja es que escribir en una Forma ahora es ilegal. Por ejemplo, el siguiente código no está permitido:

public void addRectangle(List formas) {

formas.add(0, new Rectángulo()); / ¡Error al compilar!

}

Deberías poder indicar por qué el código anterior no está permitido.

Esto se debe a que el segundo argumento de formas.add es de tipo ? extiende Forma, una subclase desconocida de Forma. Por lo tanto, no sabemos cuál es su tipo, o si es una clase principal de Rectángulo; puede que lo sea o no, por lo que no es seguro pasar un

Rectangle aquí. Los comodines restringidos son exactamente lo que necesitamos para resolver el ejemplo del DMV que transmite una lista a la Oficina del Censo. Nuestro ejemplo supone que los datos están representados por un mapa desde nombres (cadenas) hasta personas (representadas por Persona o sus subclases, como Conductor). map es un ejemplo de un tipo genérico donde los dos parámetros de tipo representan la clave y el valor del mapa.

clase pública Censo {

public static void addRegistry(Map registro) { ...}

}

clase pública Censo {

public static void addRegistry(Map registro) { ...}....

}

Map allDrivers = ... ;

Census.addRegistry(allDrivers);

5. Método general

Considere escribir un Método, toma la matriz de objetos y la colección como parámetros y completa la función de colocar todos los objetos de la matriz en la colección.

Aquí está el primer intento:

static void fromArrayToCollection(Object[] a, Collection c) {

for(Object o : a ) {

c.add(o); // error en tiempo de compilación

}

}

En este punto, deberías poder aprender ¡Evite el error de principiante de intentar utilizar Collection como tipo de parámetro de colección! Quizás te hayas dado cuenta de que usar

Collection tampoco funciona; La solución a este problema es utilizar métodos genéricos. Al igual que las declaraciones de tipo, las declaraciones de método también pueden ser genéricas, es decir, utilizar uno o más parámetros de tipo.

static void fromArrayToCollection(T[] a, Collection c){

for (T o : a) {

c. add(o); // correcto

}

}

Podemos llamar a este método con cualquier colección, siempre que el tipo de sus elementos sea el tipo padre del tipo de elemento de matriz.

Objeto[] oa = nuevo Objeto[100];

Colección co = nuevo ArrayList();

fromArrayToCollection(oa, co ); // T representa el objeto

String[] sa = new String[100];

Colección cs = new ArrayList();

fromArrayToCollection(sa, cs);// Se infiere que T es Cadena

fromArrayToCollection(sa, co);// Se infiere que T es Objeto

Entero[ ] ia = nuevo Entero[100];

Float[] fa = nuevo Float[100];

Número[] na = nuevo Número[100];

Collection< Number> cn = new ArrayList<.Number>();

fromArrayToCollection(ia, cn);// Se infiere que T es Número

fromArrayToCollection(fa, cn );// Se infiere que T es Número

fromArrayToCollection(na, cn); // Se infiere que T es Número

fromArrayToCollection(na, co); ser Objeto

fromArrayToCollection(na, cs); // Error en el tiempo de compilación

Tenga en cuenta que no pasamos los parámetros de tipo reales al método genérico. El compilador inferirá el valor del parámetro de tipo en función de los parámetros reales.

El compilador normalmente inferirá los parámetros de tipo más específicos, haciendo que el tipo de llamada sea correcto.

Ahora una pregunta: ¿cuándo deberíamos usar métodos genéricos y cuándo deberíamos usar tipos comodín? Para comprender la respuesta, primero veamos algunos métodos en la biblioteca de la colección.

Colección de interfaz pública {

boolean containsAll(Collection c);

boolean addAll(Collection c);

}

También podemos usar métodos genéricos:

public interface Collection {

boolean containsAll( Collection c);

boolean addAll(Collection c);

// ¡Oye, las variables de tipo también pueden tener límites!

}

Sin embargo, en containsAll y addAll, el parámetro de tipo T se usa solo una vez. El tipo del valor de retorno no depende del parámetro de tipo ni de ningún otro parámetro del método (en este caso, un parámetro simple). Esto nos dice que los parámetros de tipo se utilizan como polimorfismo, cuyo único propósito es permitir que se utilicen múltiples tipos de parámetros reales en diferentes sitios de llamada. Si este es el caso, se deben utilizar comodines.

Los comodines están destinados a admitir subclases flexibles, que es lo que queremos enfatizar aquí.

Las funciones genéricas permiten el uso de parámetros de tipo para expresar dependencias entre uno o más parámetros de un método, o entre parámetros y sus valores de retorno. Si no existe tal dependencia, no se deben utilizar métodos genéricos.

(Texto original: Los métodos genéricos permiten el uso de parámetros de tipo para expresar dependencias entre los tipos de uno o más parámetros del método y/o su tipo de retorno. Si no están presentes Para esta dependencia, los métodos genéricos deben no debe usarse)

También puede usar métodos genéricos y comodines al mismo tiempo. El siguiente es el método Collections.copy():

classCollections {

public static void copy(List dest, List src){ .. .}

}

Tenga en cuenta la dependencia entre los dos tipos de parámetros. Cualquier objeto copiado de la lista de origen debe poder especificarse como el tipo de elementos de la lista de destino (destino): tipo T. El tipo de elemento de la lista de destinos (destino) es el tipo de elemento de la lista de destinos. Por lo tanto, el tipo de elemento del tipo fuente puede ser cualquier subtipo de T. La firma del método de copia indica la dependencia del tipo mediante un parámetro de tipo, pero utiliza un comodín para el tipo de elemento del segundo parámetro. También podríamos escribir la firma de esta función de otra forma, sin utilizar comodines en absoluto:

class Collectassions {

public static void copy(List< T > dest, List

}

Esto funciona, pero el primer parámetro de tipo se usa tanto en el tipo de dst como en el usado. límite superior del parámetro de tipo S para el segundo parámetro, mientras que S en sí solo se usa una vez en el tipo de src y ningún otro parámetro depende de él.

Esto significa que podemos usar un carácter comodín en lugar de S.

Usar comodines es más claro

y más preciso que usar parámetros de tipo explícitos, por lo que es mejor utilizar comodines cuando sea posible. Otra ventaja de los comodines es que se pueden utilizar fuera de las firmas de métodos, como en tipos de campos, variables locales y matrices. He aquí un ejemplo.

Volviendo al problema del dibujo, digamos que queremos mantener un historial de solicitudes de dibujo. Podemos guardar el registro histórico en la variable miembro estática de la clase Shape:

Al llamar a drawAll(), guarde los parámetros entrantes en el registro histórico:

Lista estática< Lista< ? extiende Forma>> historial = new ArrayList>();

public void drawAll(List formas) {

historial .addLast (formas);

for(Forma s: formas) {

s.draw(this);

}

}

Finalmente, hablemos de la convención de nomenclatura de los parámetros de tipo. Usamos T para representar tipos siempre que no haya un tipo más específico para distinguirlos. Esto suele ocurrir en métodos genéricos. Si una función genérica está en una clase genérica, para evitar confusiones es mejor usar el mismo nombre para los parámetros de tipo del método que para los parámetros de tipo de la clase. Lo mismo ocurre con las clases internas.