Métodos, Constructores e Interfaces Genéricos

Avanzado

La idea de Genéricos en Java es permitir que el tipo (Integer, String,etc. y tipos definidos por el usuario) sea un parámetro para métodos, clases e interfaces. Aprende con ejemplos sobre métodos, constructores e interfaces genéricos en Java.

Lectura Recomendada
Genéricos en Java

1. Métodos Genéricos

Como se ha mostrado en los ejemplos anteriores, los métodos dentro de una clase genérica pueden hacer uso de un parámetro de tipo de clase y, por lo tanto, son automáticamente genéricos en relación con el parámetro de tipo. Sin embargo, es posible declarar un método genérico que usa uno o más parámetros de tipo propios. Además, es posible crear un método genérico que se incluye dentro de una clase no genérica.

El siguiente programa declara una clase no genérica llamada DemoMetodoGenerico y un método genérico estático dentro de esa clase llamado igualArrays(). Este método determina si dos matrices contienen los mismos elementos, en el mismo orden. Se puede usar para comparar dos matrices siempre que las matrices sean del mismo tipo o compatibles y los elementos de la matriz sean, en sí mismos, comparables.

// Demostrar un simple método genérico
class DemoMetodoGenerico {
    //Determine si el contenido de dos matrices es el mismo.
    static <T extends Comparable<T>, V extends T> boolean
    igualArrays (T[] x, T[] y){
        //Si las longitudes de los array son diferentes,
        // entonces  los array son diferentes
        if (x.length != y.length) return false;

        for (int i=0; i<x.length;i++)
            if(!x[i].equals(y[i])) return false; //arrays diferentes
        return true; // Contenido de arrays son equivalentes
    }

    public static void main(String[] args) {
        Integer nums[]={1,2,3,4,5};
        Integer nums2[]={1,2,3,4,5};
        Integer nums3[]={1,2,7,4,5};
        Integer nums4[]={1,2,7,4,5,6};

        if (igualArrays(nums,nums))
            System.out.println("nums es igual a nums");
        if (igualArrays(nums,nums2))
            System.out.println("nums es igual a nums2");
        if (igualArrays(nums,nums3))
            System.out.println("nums es igual a num3");
        if (igualArrays(nums,nums4))
            System.out.println("nums es igual a nums4");

        // Crea un array de double
        Double dvals[]={1.1,2.2,3.3,4.4,5.5};
        //Esto no compilará porque
        // nums y dvals no son del mismo tipo.
        //if(igualArrays(nums,dvals))
        //   System.out.println("nums es igual a dvals");
    }
}

Salida:

nums es igual a nums
nums es igual a nums2

1.1. Explicación del código

Examinemos igualArrays() de cerca. Primero, observe cómo se declara por esta línea:

static <T extends Comparable<T>, V extends T> boolean
igualArrays (T[] x, T[] y){

Los parámetros de tipo se declaran antes del tipo de devolución del método. También tenga en cuenta que T se extiende de Comparable <T>. Comparable es una interfaz declarada en java.lang. Una clase que implementa Comparable define objetos que se pueden ordenar. Por lo tanto, requerir un límite superior de Comparable garantiza que igualArrays() solo se pueda usar con objetos que puedan compararse.

Comparable es genérico y su parámetro de tipo especifica el tipo de objetos que compara. (En breve, verá cómo crear una interfaz genérica.) A continuación, observe que el tipo V está delimitado por T. Por lo tanto, V debe ser el mismo que el tipo T o una subclase de T. Esta relación refuerza el hecho de que igualArrays() sólo puede llamarse con argumentos que sean comparables entre sí. Observe también que igualArrays() es estático, permitiendo que sea llamado independientemente de cualquier objeto. Entienda, sin embargo, que los métodos genéricos pueden ser estáticos o no estáticos. No hay ninguna restricción a este respecto.

Ahora, observe cómo se llama a igualArrays() dentro de main() mediante el uso de la sintaxis de llamada normal, sin la necesidad de especificar argumentos de tipo. Esto se debe a que los tipos de argumentos se disciernen automáticamente, y los tipos de T y V se ajustan en consecuencia. Por ejemplo, en la primera llamada:

if(igualArrays(nums, nums))

el tipo de elemento del primer argumento es Integer, lo que hace que Integer se sustituya por T. El tipo de elemento del segundo argumento también es Integer, lo que hace que Integer también sea un sustituto de V. Por lo tanto, la llamada a igualArrays() es legal, y las dos matrices se pueden comparar.

1.2. Delimitación de método genérico

Ahora, observe el código comentado, que se muestra aquí:

// if(igualArrays(nums,dvals))
// System.out.println("nums es igual a dvals");

Si elimina los comentarios y luego intenta compilar el programa, recibirá un error. La razón es que el parámetro tipo V está limitado por T en la cláusula extends en la declaración de V. Esto significa que V debe ser de tipo T o una subclase de T. En este caso, el primer argumento es de tipo Integer, lo que convierte a T en Integer, pero el segundo argumento es de tipo Double, que no es una subclase de Integer. Esto hace que llamar a igualArrays() sea ilegal y da como resultado un error de discrepancia de tipo en tiempo de compilación.

La sintaxis utilizada para crear igualArrays() puede generalizarse. Aquí está la sintaxis de un método genérico:

<lista-parametro-tipo> ret-tipo nombre-metodo(lista-parametros) { // ...

En todos los casos, lista-parametro-tipo es una lista de parámetros de tipo separados por comas. Tenga en cuenta que para un método genérico, la lista de parámetros de tipo precede al tipo de devolución.

2. Constructores genéricos

Un constructor puede ser genérico, incluso si su clase no lo es. Por ejemplo, en el siguiente programa, la clase SumaN no es genérica, pero su constructor sí lo es.

class SumaN{
    private int suma;

    <T extends Number> SumaN(T arg){
        suma=0;
        for (int i=0; i<=arg.intValue();i++)
            suma+=i;
    }

    int getSuma(){
        return suma;
    }
}

class DemoConstGen{
    public static void main(String[] args) {
        SumaN ob=new SumaN(4.0);
        System.out.println(ob.getSuma());
    }
}

La clase SumaN calcula y encapsula la suma del valor numérico pasado a su constructor. Recuerde que la suma de N es la suma de todos los números enteros entre 0 y N. Debido a que SumaN especifica un parámetro de tipo que está limitado por Number, un objeto SumaN puede construirse usando cualquier tipo numérico, incluyendo Integer, Float o Double. No importa qué tipo numérico se use, su valor se convierte a Integer llamando a intValue(), y se calcula la suma. Por lo tanto, no es necesario que la clase SumaN sea genérica; sólo se necesita un constructor genérico.

3. Interfaces genéricas

Como viste en el programa DemoMetodoGenerico presentado anteriormente, una interfaz puede ser genérica. En ese ejemplo, la interfaz estándar Comparable<T> se usó para asegurar que los elementos de dos matrices se pudieran comparar. Por supuesto, también puedes definir tu propia interfaz genérica.

Las interfaces genéricas se especifican al igual que las clases genéricas. Aquí hay un ejemplo. Crea una interfaz llamada Contenedor, que puede ser implementada por clases que almacenan uno o más valores. Declara un método llamado contenido() que determina si un valor especificado está contenido por el objeto invocado.

// Ejemplo de interface genérica

interface Contenedor<T>{
    // El método contenido() verifica que un elemento específico
    // está contenido dentro de un objeto que implementa Contenedor.
    boolean contenido(T o);
}

//Implementa Contenedor usando una matriz
// para contener los valores.

class MiClase<T> implements Contenedor<T>{
    T[] array;

    MiClase(T[] o){
        array=o;
    }

    //Implementación de contenido()
    public boolean contenido(T o){
        for (T x: array)
            if (x.equals(o)) return true;
        return false;
    }
}

class DemoIFGen{
    public static void main(String[] args) {
        Integer x[]={1,2,3};

        MiClase<Integer> ob= new MiClase<Integer>(x);

        if (ob.contenido(2))
            System.out.println("2 está en ob");
        else
            System.out.println("2 NO está en ob");

        if (ob.contenido(5))
            System.out.println("5 está en ob");
        else
            System.out.println("5 NO está en ob");

        // Lo siguiente es ilegal porque ob es un Contenedor de Integer
        // y 9.25 es un valor Double.
        // if (ob.contenido(9.25))
        //   System.out.println("9.25 está en ob");

    }
}

Salida:

2 está en ob
5 NO está en ob

Aunque la mayoría de los aspectos de este programa deben ser fáciles de entender, es necesario establecer un par de puntos clave. Primero, observe que Contenedor se declara así:

interface Contenedor<T> {

En general, una interfaz genérica se declara de la misma manera que una clase genérica. En este caso, el parámetro de tipo T especifica el tipo de objetos que están contenidos. A continuación, MiClase implementa Contenedor. Observe la declaración de MiClase, que se muestra aquí:

class MiClase<T> implements Contenedor<T>{

En general, si una clase implementa una interfaz genérica, esa clase también debe ser genérica, al menos en la medida en que tome un parámetro de tipo que se pase a la interfaz. Por ejemplo, el siguiente intento de declarar MiClase es erróneo:

class MiClase implements Contenedor<T> { // Error!

Esta declaración es incorrecta porque MiClase no declara un parámetro de tipo, lo que significa que no hay forma de pasar uno a Contenedor. En este caso, el identificador T es simplemente desconocido y el compilador informa un error. Por supuesto, si una clase implementa un tipo específico de interfaz genérica, como se muestra aquí:

class MiClase implements Contenedor<Double> { // Ok!

entonces la clase implementadora no necesita ser genérica.

3.1. Explicación del código

Como era de esperar, los parámetros de tipo especificados por una interfaz genérica pueden ser acotados. Esto le permite limitar el tipo de datos para los cuales se puede implementar la interfaz. Por ejemplo, si desea limitar la contención a tipos numéricos, puede declararlo así:

interface Contenedor<T extends Number> {

Ahora, cualquier clase implementadora debe pasar a Contenedor un argumento de tipo que también tenga el mismo límite. Por ejemplo, ahora MiClase se debe declarar como se muestra aquí:

class MiClase<T extends Number> implements Contenedor<T> {

Preste especial atención a la forma en que MiClase declara el parámetro de tipo T y luego pasa a Contenedor. Como Contenedor ahora requiere un tipo que extienda Number, la clase implementadora (MiClase en este caso) debe especificar el mismo límite. Además, una vez que se ha establecido este límite, no hay necesidad de especificarlo nuevamente en la cláusula de implementaciones. De hecho, sería un error hacerlo. Por ejemplo, esta declaración es incorrecta y no se compilará:

class MiClase<T extends Number>
implements Contenedor<T extends Number>{
//Error!
}

Una vez que se ha establecido el parámetro de tipo, simplemente se pasa a la interfaz sin más modificaciones.

Aquí está la sintaxis generalizada para una interfaz genérica:

interface nombre-interfaz<lista-parametros-tipo> { // ...

Aquí, lista-parametros-tipo es una lista de parámetros de tipo separados por comas. Cuando se implementa una interfaz genérica, debe especificar los argumentos de tipo, como se muestra aquí:

class nombre-clase<lista-parametros-tipo>
  implements nombre-interfaz<lista-parametros-tipo> {

¡Hasta la próxima!

Genéricos en Java
  • Métodos, Constructores e Interfaces

Sobre el Autor:

Hey hola! Yo soy Alex Walton y tengo el placer de compartir conocimientos hacía ti sobre el tema de Programación en Java, desde cero, Online y Gratis.

Deja una Respuesta

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.