Un pequeño problema en Java
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.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
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 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
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
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
Lista
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
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
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());
}
} p >
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
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
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
public void drawAll(List extends Shape> formas) { //...}< / p>
Aquí hay una pequeña pero importante diferencia: reemplazamos el tipo List
cualquier Lista que sea una subclase de Shape, por lo que podemos llamar a List
Lista extiende Forma> 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 extends Shape> 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
clase pública Censo {
public static void addRegistry(Map
}
clase pública Censo {
public static void addRegistry(Map
}
Map
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
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
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
fromArrayToCollection(oa, co ); // T representa el objeto
String[] sa = new String[100];
Colección
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
// ¡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
}
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 } 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 extiende Forma> 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. >();