En PC Resumen, hablaremos del concepto de como funcionan los Comodines que tienen los genéricos en Java.

Antes de ver los comodines, considérese un ejemplo que nos ayude a motivar su uso. Supongamos que queremos implementar un método genérico llamado sumar que obtenga el total de números de una colección, como en un objeto ArrayList. Nos gustaría poder obtener el total de todos los números sin importar su tipo. Por esta razón, declaramos el objeto con el argumento de tipo Number, el cual es superclase de todos los números. Por lo tanto el método sumar recibirá un parámetro de tipo ArrayList<Number> y obtendrá el total de sus elementos.

import java.util.ArrayList;

public class TotalNumeros {
    
    public static double sumar (ArrayList<Number> lista) {
        double total = 0;
        for (Number elemento: lista) {
            total += element.doubleValue();
        }
        return total;
    }

    public static void main(String args[]) {
        Number[] numeros = {1, 2.4, 3, 4.1};
        ArrayList<Number> listaNumeros = new ArrayList<Number>();
        for (Number elemento: numeros) {
            listaNumeros.add (elemento);
        }
        System.out.printf("listaNumeros contiene:%s \n", listaNumeros);
        System.out.printf("Total de los elementos a listaNumeros: %.1f \n", sumar(llistaNumeros));
    }
}

El método utiliza valores double para realizar los cálculos y devuelve el resultado como un double.

En PC Resumen vamos a hablar de los genéricos crudos en Java. Los programas de prueba anteriores que hemos visto en el artículo Clases genéricas en Java, crean instancias de objetos Pila con los argumentos de tipo Double y Integer. También es posible instanciar la clase genérica Pila sin especificar un argumento de tipo:

Pila pila = new Pila(5);

En este caso, se dice que pila tiene un tipo crudo, lo que significa que el compilador utiliza de forma implícita el tipo Object en la clase genérica para cada argumento de tipo. Así, la instrucción anterior crea una Pila que puede almacenar objetos de cualquier tipo. Esto es importante para la compatibilidad con versiones anteriores de Java.

A una variable Pila de crudo se le puede asignar una Pila que especifique un argumento de tipo:

Pila pilaTipoCrudo2 = new Pila<Double>(5);

Debido a que el tipo Double es una subclase de Object. La asignación se permite ya que los elementos son objetos.

De forma similar, a una variable Pila que especifica un argumento de tipo se le puede asignar un objeto Pila de crudo:

Pila<Integer> pilaInteger = new Pila(10);

Aunque esta asignación se permite no es segura debido a que una Pila de crudo podría almacenar elementos diferentes a Integer. En este caso, el compilador genera un mensaje de advertencia, el cual indica asignación insegura.

El programa de prueba siguiente utiliza la noción de tipo crudo. Si compilamos con la opción Xlint: Unchecked saldrán varios mensajes de advertencia ya que el compilador no puede garantizar que los tipos manipulados por las pilas sean correctos.

public class PruebaTipoCrudo {
    private Double[] elementosDouble = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6};
    private Integer[] elementosInteger = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
    
    public void probarPilas() {
        Pila pilaTipoCrudo1 = new Pila(5);
        Pila pilaTipoCrudo2 = new Pila<Double>(5);
        Pila<Integer> pilaInteger = new Pila(10);
        probarApilar("pilaTipoCrudo1", pilaTipoCrudo1, elementsDouble);
        probarDesapilar("pilaTipoCrudo1", pilaTipoCrudo1 );
        probarApilar("pilaTipoCrudo2", pilaTipoCrudo2, elementsDouble);
        probarDesapilar("pilaTipoCrudo2", pilaTipoCrudo2);
        probarApilar("pilaInteger", pilaInteger, elementsInteger);
        probarDesapilar("pilaInteger", pilaInteger);
    }

    public <T> void probarApilar(String nombre, Pila<T> pila, T[] elementos) {
        try {
            System.out.printf("\n Apilar elementos en %s \n", nombre);
            for (T elemento: elementos) {
                System.out.printf("%s", elemento);
                pila.apilar(elemento);
            }
        } Catch (ExcepcioPilaLlena excepcioPilaLlena) {
            System.out.println();
            excepcioPilaLlena.printStackTrace();
        }
    }

    public <T> void probarDesapilar(String nombre, Pila<T> pila) {
        try {
            System.out.printf("\n Desapilando elementos de %s \n", nombre);
            T valorAQuitar;
            while (true) {
                valorAQuitar = pila.desapilar ();
                System.out.printf("%s", valorAQuitar);
            }
        } Catch (ExcepcioPilaVacia excepcioPilaVacia) {
            System.out.println();
            excepcioPilaVacia.printStackTrace();
        }
    }

    public static void main(String args[]) {
        PruebaTipoCrudo aplicacion = new PruebaTipoCrudo();
        aplicacion.probarPiles ();
    }
}

En PC Resumen hablaremos de que son las Clases genéricas en Java. El concepto de una estructura de datos, como por ejemplo una pila, se puede entender independientemente del tipo de elemento que manipula. Las clases genéricas proporcionan los medios para describir el concepto de pila (o cualquier otra clase) en forma independiente de su tipo. Así podemos crear instancias de objetos con tipos específicos de la clase genérica. Esto nos permite una gran oportunidad de reutilizar el software.

Una vez que tenemos una clase genérica, podemos utilizar una notación concisa para indicar el tipo actual a utilizarse en lugar del parámetro de tipo de la clase. En tiempo de compilación, el compilador Java asegura la seguridad de los tipos de nuestro código y utiliza las técnicas de borrado para permitir que nuestro código interactúe con la clase genérica.

En el código siguiente vemos la declaración de una clase genérica Pila. La declaración de la clase es similar a la de una clase no genérica, salvo que el nombre de la clase va seguido de una sección de parámetros de tipo. En este caso, el parámetro de tipo E, que será el que manipulará la pila. Al igual que con los métodos genéricos, la sección de parámetros de tipo puede tener uno o más parámetros separados por comas. El parámetro de tipo E se utiliza en la declaración de la clase Pila para representar el tipo del elemento.

En PC Resumen vamos a explicar la implementación de los genéricos en Java. Si las operaciones realizadas por los métodos sobrecargados son idénticas para cada tipo de argumento, los métodos sobrecargados pueden codificarse de forma más compacta y conveniente utilizando genéricos.

A continuación veremos un programa que nos imprime tres arrays diferentes.

public class PruebaMetodoGenerico {
    public static<E> void imprimirArray (E[] arrayEntrada) {
        for (E elemento: arrayEntrada) {
            System.out.printf ("% s", elemento);
        }
        System.out.println();
    }
    public static void main (String args[]) {
        Integer[] arrayInteger = {1, 2, 3, 4, 5, 6};
        Double[] arrayDouble = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7};
        Character[] arrayCharacter = {'H', 'O', 'L', 'A'};
        System.out.println("El array arrayInteger contiene:");
        imprimirArray(arrayInteger);
        System.out.println("El array arrayDouble contiene:");
        imprimirArray(arrayDouble);
        System.out.println("El array arrayCharacter contiene:");
        imprimirArray(arrayCharacter);
    }
}

En PC Resumen os vamos a explicar que motiva usar los métodos genéricos en Java. A menudo se utilizan métodos sobrecargados para realizar operaciones similares en diferentes tipos de datos. Cuando el compilador encuentra una llamada a un método, siempre trata de localizar la declaración de un método que tenga el mismo nombre y parámetros que coincidan con los tipos de los argumentos en la llamada al método.

El problema es que si queremos hacer tres métodos con el mismo nombre para, por ejemplo, imprimir un array de enteros, uno de decimales y uno de caracteres debemos repetir prácticamente el mismo código tres veces. Si podemos reemplazar los tipos de elementos con un nombre genérico (por convención será E para representar el tipo "elemento"), entonces podemos declarar un método imprimirArray que pueda imprimir los elementos de un array que tenga objetos con un código similar a este:

public static void imprimirArray (E[] arrayEntrada) {
    for (E elemento: arrayEntrada) {
        System.out.printf("%s", elemento);
    }
    System.out.println();
}

Aunque no lo hemos visto, el método printf funciona prácticamente igual que en C. En Java podemos especificar el formato %s para imprimir la representación de cadena de cualquier objeto ya que se hace un llamamiento implícito al método toString del objeto.