Pildoritas de POO y Java: Polimorfismo

Llevo casi desde que comencé con las pildoritas de POO anunciando que “el polimorfismo va a llegaaaaar” y nada, que no llegaba…¡hasta hoy!

El polimorfismo va muy ligado a la herencia, y se apoya en ella para poder generalizar comportamientos de diferentes clases, como si de una se tratara, siendo para nosotros transparente su tipo, siempre y cuando éste herede de una clase Padre común que define la funcionalidad generalizada.

Polimorfismo

El Polimorfismo es el envío de mensajes a objetos de tipos diferentes utilizando la misma sintaxis. En lenguajes con un sistema de tipos dinámicos, es decir, que una variable puede contener todo tipo de valores y su tipo se setea en tiempo de ejecución. No es necesario el uso de herencia para aplicar el polimorfismo pero en lenguajes fuertemente tipados como Java es necesario utilizar una clase común padre de la cual heredarán todos los objetos polimórficos.

Por ejemplo, basándonos en el ejemplo de nuestra anterior pildorita de Herencia, las clases Profesor y Alumno heredan de Persona varios métodos comunes, como el método toString, que además reimplementan de forma específica cada clase hijo a través de la anotación @Override.

Ahora imaginemos que tenemos un método auxiliar en nuestra aplicación que se dedica a mostrar los datos por pantalla de cualquier persona vinculada a la Escuela, Universidad…¿Cómo lo haríamos de forma general utilizando polimorfismo?

public void mostrarDatos() {
		Profesor profe1 = new Profesor(1, "Antonio", "Brena", "1111111K", 52, "M");
		Profesor profe2 = new Profesor(2, "Alicia", "Hernandez", "2222222K", 45, "F");
		Alumno alumno1 = new Alumno(1, "Mar", "Millan", "44444444K", 29, "F");
		Persona trabajador1 = new Persona("Francisco", "García", "33333333K", 39, "M");
		
		List<Persona> lsPersonas = new ArrayList<Persona>();
		lsPersonas.add(profe1);
		lsPersonas.add(profe2);
		lsPersonas.add(alumno1);
		lsPersonas.add(trabajador1);

		for (Persona per : lsPersonas) {
			System.out.println(per.toString());
		}		
	}

El resultado de ejecutar este método es el siguiente:

DATOS DEL PROFESOR:
Nombre: Antonio
Apellidos: Brena
DNI: 1111111K
Edad: 52
Sexo: M

DATOS DEL PROFESOR:
Nombre: Alicia
Apellidos: Hernandez
DNI: 2222222K
Edad: 45
Sexo: F

DATOS DEL ALUMNO:
Nombre: Mar
Apellidos: Millan
DNI: 44444444K
Edad: 29
Sexo: F

DATOS DE LA PERSONA:
Nombre: Francisco
Apellidos: García
DNI: 33333333K
Edad: 39
Sexo: M

Como se puede observar, para nosotros es indiferente el tipo del objeto que estemos invocando, ya que sabemos que todos esos objetos al heredar de la clase padre Persona tendrán obligatoriamente el método toString. Así mismo, si uno de esos objetos tiene una implementación propia de dicho método, ejecutará la implementación especifica de forma transparente para nosotros.

Aquellos lenguajes que tengan interfaces (como es el caso de Java), pueden también implementar el polimorfismo utilizando este recurso, lo que se conoce como composición de objetos. Esta alternativa se considera mejor práctica a la hora de aplicar polimorfismo que el uso de herencia, ya que este solución sólo obliga a que las diferentes clases compartan el tipo y obligatoriedad de implementar los métodos del interfaz, pero no se comparten comportamientos ni atributos. Pero, ¿qué es una interfaz?

Interfaces y Clases Abstractas

Cuando queremos unificar atributos y métodos en una clase padre, como hemos hecho en la clase Persona, pero no queremos que ésta sea instanciable, crearíamos nuestra clase Persona como clase abstracta:

public abstract class Persona {
	 
    private String nombre;
    private String apellidos;
    private String dni;
    private Integer edad;
    private String sexo;
    
    Persona() {
    	super();
    }
    
    public Persona(String nombre, String apellidos, String dni, Integer edad, String sexo) {
    	super();
    	this.nombre = nombre;
    	this.apellidos = apellidos;
    	this.dni = dni;
    	this.edad = edad;
    	this.sexo = sexo;
    }
    
    public String getNombre() {
        return nombre;
    }
 
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
 
    public String getApellidos() {
        return apellidos;
    }
 
    public void setApellidos(String apellidos) {
        this.apellidos = apellidos;
    }
 
    public String getDni() {
        return dni;
    }
 
    public void setDni(String dni) {
        this.dni = dni;
    }
 
    public Integer getEdad() {
        return edad;
    }
 
    public void setEdad(Integer edad) {
        this.edad = edad;
    }
 
    public String getSexo() {
        return sexo;
    }
 
    public void setSexo(String sexo) {
        this.sexo = sexo;
    }
    
    @Override 
    public String toString() {
        String nuevalinea = System.getProperty("line.separator");
        String resultado = "DATOS DE LA PERSONA: " + nuevalinea + 
                "Nombre: " + this.getNombre() + nuevalinea + 
                "Apellidos: " + this.getApellidos() + nuevalinea + 
                "DNI: " + this.getDni() + nuevalinea +
                "Edad: " + this.getEdad() + nuevalinea +
                "Sexo: " + this.getSexo() + nuevalinea;
 
        return resultado;
    }
    
	@Override
	public boolean equals(Object object) {
		final Persona aux = (Persona) object;
		if (this == object)
	           return true;
		else if (object == null)
	           return false;
		else if (getClass() != object.getClass())
	           return false;
		else if (this.dni.equals(aux.getDni())) {
			return true;
		} else {
			return false;
		}
	}

     public abstract void metodoAbstracto();
}

Por tanto, en nuestro anterior ejemplo tendríamos un error al crear el objeto Persona trabajador1, ya que, como hemos dicho, las clases abstractas no pueden ser instanciadas:

También podemos añadir métodos abstractos. Un ejemplo de ello es el último método que hemos creado en el ejemplo de la clase abstracta Persona, el método metodoAbstracto(), que como se puede observar siquiera tiene cuerpo. Si creamos un método abstracto estaremos obligados a definir la implementación de dicho método en las clases hijas.

¿Y qué es una interfaz? Pues una clase completamente abstracta, es decir, una clase en la que no se implementan métodos, ni hay atributos, excepto aquellos que sean estáticos y/o constantes.

Los interfaces tienen como finalidad definir la forma y comportamiento de una clase.

Por ejemplo, podríamos tener una interfaz IMamifero, que implemente Persona:

public interface IMamifero {
	public void crecer();
	public void reproducirse();
	public void morir();
}

Para que nuestra clase implemente este interfaz, lo indicaremos a través de la palabra reservada implements.

Al hacer esto, como se puede ver en la anterior imagen, tendremos un error en la mayoría de entornos que nos indicará que tenemos que implementar forzosamente los métodos definidos en el interfaz IMamifero:

public class Persona implements IMamifero {
	 
    private String nombre;
    private String apellidos;
    private String dni;
    private Integer edad;
    private String sexo;
    
    Persona() {
    	super();
    }
    
    public Persona(String nombre, String apellidos, String dni, Integer edad, String sexo) {
    	super();
    	this.nombre = nombre;
    	this.apellidos = apellidos;
    	this.dni = dni;
    	this.edad = edad;
    	this.sexo = sexo;
    }
    
    public String getNombre() {
        return nombre;
    }
 
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
 
    public String getApellidos() {
        return apellidos;
    }
 
    public void setApellidos(String apellidos) {
        this.apellidos = apellidos;
    }
 
    public String getDni() {
        return dni;
    }
 
    public void setDni(String dni) {
        this.dni = dni;
    }
 
    public Integer getEdad() {
        return edad;
    }
 
    public void setEdad(Integer edad) {
        this.edad = edad;
    }
 
    public String getSexo() {
        return sexo;
    }
 
    public void setSexo(String sexo) {
        this.sexo = sexo;
    }
    
    @Override 
    public String toString() {
        String nuevalinea = System.getProperty("line.separator");
        String resultado = "DATOS DE LA PERSONA: " + nuevalinea + 
                "Nombre: " + this.getNombre() + nuevalinea + 
                "Apellidos: " + this.getApellidos() + nuevalinea + 
                "DNI: " + this.getDni() + nuevalinea +
                "Edad: " + this.getEdad() + nuevalinea +
                "Sexo: " + this.getSexo() + nuevalinea;
 
        return resultado;
    }
    
	@Override
	public boolean equals(Object object) {
		final Persona aux = (Persona) object;
		if (this == object)
	           return true;
		else if (object == null)
	           return false;
		else if (getClass() != object.getClass())
	           return false;
		else if (this.dni.equals(aux.getDni())) {
			return true;
		} else {
			return false;
		}
	}

	@Override
	public void crecer() {
		System.out.println("Crece");
		
	}

	@Override
	public void reproducirse() {
		System.out.println("Fornicia");
		
	}

	@Override
	public void morir() {
		System.out.println("R.I.P.");	
	}
}

Al hacer esto, ya sí que podremos ejecutar nuestra aplicación sin errores y, si queremos, hacer polimorfismo con los métodos definidos por la interfaz:

Profesor profe1 = new Profesor(1, "Antonio", "Brena", "1111111K", 52, "M");
		Profesor profe2 = new Profesor(2, "Alicia", "Hernandez", "2222222K", 45, "F");
		Alumno alumno1 = new Alumno(1, "Mar", "Millan", "44444444K", 29, "F");
		Persona trabajador1 = new Persona("Francisco", "García", "33333333K", 39, "M");
		
		List<Persona> lsPersonas = new ArrayList<Persona>();
		lsPersonas.add(profe1);
		lsPersonas.add(profe2);
		lsPersonas.add(alumno1);
		lsPersonas.add(trabajador1);

		for (Persona per : lsPersonas) {
			per.reproducirse();
		}

Consola:

Fornicia
Fornicia
Fornicia
Fornicia

Como veis, el Polimorfismo no es más que el uso más potente de la herencia, pero no es, ni mucho menos, complejo, al igual que tampoco lo son las ideas de clase abstracta e interfaz una vez se entiende el concepto general de Herencia.

Así que nada, si habéis seguido estas pildoritas al pie de la letra podemos ya afirmar (con extremada timidez, eso sí) que comprendéis y entendéis las bases de lo que es la Programación Orientada a Objetos. A partir de ahora, sólo necesitaréis tiempo y práctica para poder desarrollar cualquier aplicación que deseéis con este paradigma y, eso sí, teniendo claros los conceptos fundamentales que os harán desarollar como las personitas, y no como muchos: con los pies.

Y aunque ésta sí será nuestra última pildorita de POO, ni mucho menos será la última de Java. En las próximas semanas os hablaré de las excepciones (un concepto de Java que escapa a la POO pero fundamental a la hora de programar y realizar un correcto tratamiento de errores) y a partir de ahí, ya se verá. Tenía pensado hacer un programa básico como ejemplo resumen de todo lo que hemos visto, y tras esto, seguir entrando en chicha con conceptos de programación más avanzados.

Índice de Pildoritas

Share