Héctor Luaces

Polimorfismo en Java

Tenía pensado escribir sobre el enlace dinámico y estático en los Tipos de Java, pero creo que para que un programador pueda entender correctamente estos conceptos, primero ha de estar familiarizado con el concepto de polimorfismo en Java y en POO.

En el artículo voy a hablar tanto en términos generales (aplicables a cualquier lenguaje POO) como específicos. No todos los lenguajes tienen los mismos tipos de polimorfismo que Java, pero creo que leyendo todo se conseguirá obtener un nivel de comprensión de este concepto mucho mayor, que a veces es un tema que es conocido, pero que no se entiende completamente.

 

Polimorfismo en Java

El polimorfismo es la habilidad de una función, método, variable u objeto de poseer varias formas distintas. Podríamos decir que un mismo identificador comparte varios significados diferentes.

El propósito del polimorfismo es implementar un estilo de programación llamado envío de mensajes en el que los objetos interactúan entre ellos mediante estos mensajes, que no son más que llamadas a distintas funciones.

Java tiene 4 grandes formas de polimorfismo (aunque conceptualmente, muchas más):

 

Polimorfismo de asignación

El polimorfismo de asignación es el que está más relacionado con el enlace dinámico.

En java, una misma variable referenciada (Clases, interfaces…) puede hacer referencia a más de un tipo de Clase. El conjunto de las que pueden ser referenciadas está restringido por la herencia o la implementación.

Esto significa, que una variable A declarada como un tipo, puede hacer referencia a otros tipos de variables siempre y cuando haya una relación de herencia o implementación entre A y el nuevo tipo. Podemos decir que un tipo A y un tipo B son compatibles si el tipo B es una subclase o implementación del tipo A.

Supongamos este ejemplo:

abstract class vehiculo
{
    abstract public void iniciar();
}
class Coche extends Vehiculo
{
    @Override
    public void iniciar()
    {
    }
}

En él tenemos una clase que hereda de otra. La forma normal de instanciar una clase de tipo Coche sería esta…

Coche j = new Coche();

Sin embargo, el polimorfismo de asignación permite a una variable declarada como otro tipo usar otra forma, siempre y cuando haya una relación de herencia o implementación. Sabiendo esto, este fragmento demuestra el polimorfismo de asignación:

Vehiculo j = new Coche();

En el vemos como una variable inicializada como tipo Vehiculo puede usar el polimorfismo de asignación para hacer referencia a una clase de tipo Coche. Podemos decir que el tipo estático de la variable j es Vehiculo, mientras que su tipo dinámico es Coche, pero de esto hablaré en otro momento…

Esto también puede hacerse con el nombre de interfaces implementadas.

interface Comprable
{
    public void comprar();
}

class Casa implements Comprable
{
    @Override
    public void comprar()
    {
    }
}

class Coche extends Vehiculo implements comprable
{
   @Override
   public void iniciar()
   {
   }

   @Override
   public void comprar()
   {
   }
}

Teniendo las anteriores clases iniciales, el siguiente código es una clara muestra del polimorfismo de asignación en Java.

Comprable a = new Casa();

a = new Coche();

Polimorfismo Puro

El polimorfismo puro se usa para nombrar a una función o método que puede recibir varios tipos de argumentos en tiempo de ejecución. Esto no lo debemos confundir con la sobrecarga, que es otro tipo de polimorfismo en tiempo de compilación. Conociendo el polimorfismo de asignación, podemos hacer una función que acepte varios tipos de objetos distintos en tiempo de ejecución. Veamos un ejemplo, usando las clases anteriormente mencionadas:

class PolimorfismoPuroTest
{
    public function funcionPolimorfica(Comprable ob)
    {
        // La función acepta cualquier "comprable", es decir, cualquier objeto que implemente esa interfaz
        // El tipo de objeto se determina en tiempo de ejecución. En nuestros ejemplos, puede ser una casa o coche.
    }
}

En el ejemplo se ve como el método funcionPolimorfica es capaz de trabajar con varios objetos gracias al polimorfismo de asignación. Esto es lo que se conoce como polimorfismo puro y cada lenguaje lo implementa de una forma u otra.

 

Polimorfismo de sobrecarga

Muy similar al anterior, pero este se realiza en tiempo de compilación.

En el polimorfismo de sobrecarga, dos o más funciones comparten el mismo identificador, pero distinta lista de argumentos. Al contrario que el polimorfismo puro, el tipado de los argumentos se especifica en tiempo de compilación.

Es muy habitual ver esto en las clases envolventes (Integer, Float, etc…) y por eso mismo voy a mostrar un ejemplo de sobrecarga de la clase String de Java:

public final class String
    implements java.io.Serializable, Comparable, CharSequence 
{
    ...

    public static String valueOf(Object obj) 
    {
        return (obj == null) ? "null" : obj.toString();
    }

    public static String valueOf(char data[]) 
    {
        return new String(data);
    }

    public static String valueOf(char data[], int offset, int count) 
    {
        return new String(data, offset, count);
    }

    ...
}

En el ejemplo anterior vemos una misma función (valueOf) con diferentes listas de argumentos. En función de los argumentos especificados en el mensaje, la clase String utilizará uno u otro para adecuarse al contexto. Fijaros que la primera función admite un Objeto, la segunda un array de Chars y la tercera Un array de Chars y dos integers. Esto es el polimorfismo de sobrecarga, de definición similar al polimorfismo puro, pero de implementación muy distinta.

Polimorfismo de inclusión

La habilidad para redefinir por completo el método de una superclase en una subclase es lo que se conoce como polimorfismo de inclusión (o redefinición).

En él, una subclase define un método que existe en una superclase con una lista de argumentos (si se define otra lista de argumentos, estaríamos haciendo sobrecarga y no redefinición).

Un ejemplo muy básico:

abstract class Pieza
{
    public abstract void movimiento(byte X, byte Y);
}

class Alfil extends Pieza
{
    @Override
    public void movimiento(byte X, byte Y)
    {
    }
}

En el ejemplo vemos como la clase Alfil sobreescribe el método movimiento. Esto es el polimorfismo de inclusión. Un error común de un desarrollador es pensar que el siguente ejemplo es otra muestra de este tipo de polimorfismo:

class Caballo extends Pieza
{
    public void movimiento(int X, int Y)
    {
    }
}

El ejemplo de la clase Caballo está sobrecargando un método definido en su superclase. No está sobreescribiendolo, porque para usar el polimorfismo de inclusión debemos usar el mismo identificador y la misma lista de parámetros que en la superclase. Fijáos que el método de la superclase usa dos bytes, mientras que el de la subclase usa dos ints.

Con esto termino esta pequeña introducción al polimorfismo en Java. Tras esto os hablaré del enlace dinámico y estático. Os añado unos cuantos enlaces donde encontraréis más información:

  • Polimorfismo [en / es]
  • Envío de mensajes [en]
  • Polymorphism in Java [Oracle]

8 Comentarios

  1. Cuál es la diferencia entre polimorfismo de asignación e inclusión? En los 2 estás sobreescribiendo de la clase que heredan con los mismo tipos, argumentos y todo

    • Hola, X:

      El polimorfismo de inclusión permite redefinir el método de una clase, es decir, permite que un mismo nombre pueda tomar varias formas distintas en función de qué clase final la implemente.

      El polimorfismo de asignación permite que una variable tipada (fíjate ya no hablamos de métodos) pueda tomar un valor que no sea exactamente el mismo tipo que declara, si no uno que mantenga una relación de herencia o implementación. Es decir, una variable tipo «Coche» o «Furgoneta» (siendo en este ejemplo ambas clases clases hijas de «Vehiculo») no es necesario que sea de ese tipo, también podría ser «Vehiculo». Es decir, que el tipo de esa variable puede tener varias formas.

      Así pues, como ves, son bastante distintos, pues uno es para métodos y otro para variables.

      Gracias por comentar. Si no te queda algo claro me dices.

      Salud.

  2. En el caso de que la clase Coche herede de Vehículo se puede realizar:

    Vechiculo j = new Choche();

    Por qué no se puede realizar el proceso inverso?:

    Coche j = new Vechiculo();

    Gracias de antemano por la respuesta

    • Hola, Daniel:

      Sencillamente porque vehículo es una especialización de coche, no al revés.

      De forma informal podemos decir que todos los coches son vehículos, pero no todos los vehículos son coches (p.ej.: tractores, aviones, furgonetas, camiones, etc.).

      Si quieres una explicación algo más académica, te recomiendo que le eches una lectura a este otro post que habla del enlace dinámico en Java:

      https://www.luaces-novo.es/enlace-dinamico-en-java/

      Un saludo.

  3. Muchas gracias por la respuesta. Sin embargo, me gustaría discutir el tema bajo otra perspectiva. (Leí el post de enlace dinámico, muy bueno por cierto 😀 me gustan mucho tus artículos)

    Para que se realice una Herencia óptima, la relación debe cumplir con la propiedad de Subtipo, donde la asignación Vehiculo v = new Coche(); [Asumiendo que Coche hereda de Vehiculo] tiene mucho sentido, debido a que si existen métodos que contengan como parámetro objetos del tipo Vehículo, se podrían ingresar Coches sin ningún problema debido a que los Coches podrán responder correctamente al comportamiento de los Vehículos. Sin embargo el proceso inverso no es válido por lo mismo que mencionas en tu respuesta, ya que si existe un método que requiere la utilización de Coches, ingresar un Vehículo no es suficiente debido a que Vehículo no responde a las características particulares de Coche.

    Por otro lado, mi inquietud es la siguiente:

    La asignación Vehiculo v = new Coche(); se compone de dos partes:

    1.- «v» es una referencia a Vehiculo, por lo que los métodos que utiliza «v» son correspondientes a la clase Vehiculo.

    2.- A «v» se le asigna una instancia de la clase Coche, lo que implica que las implementaciones de los métodos son de la clase Coche. Esto significa que si Coche contiene una redefinición de los métodos de Vehiculo, la ejecución será de los métodos de Coche, esto se conoce como Ligado Dinámico y en tu artículo de Enlace Dinámico se ve muy bien eso.

    Mi duda es: al hacer la asignación Coche c = new Vehiculo(); representa que una referencia de la clase Coche se le asocia una instancia de la clase Vehiculo, que nosotros sabemos que es incorrecto pero hablándolo del punto de vista más técnico de lo que realmente está ocurriendo, ¿qué es lo que ocurre realmente al hacer esta asignación y por qué conduce a un error de compilación? Mi profesor explicaba que el error recae en que de esta forma «c» no podría responder correctamente a sus métodos, y eso es básico para cuando se crean objetos. Esa es mi duda, espero se haya entendido, muchísimas gracias por la ayuda!!

  4. Hola:

    en este contexto son clases distintas y por lo tanto no pueden asignarse, es tan fácil como eso.

    El ejemplo inverso funciona porque las reglas establecidas de herencia e implementación permiten al compilador inferir qué métodos comunes va a tener la referencia «v» en base a sus superclases e interfaces implementadas.

    Al revés este conjunto de reglas no existe ya que «Vehiculo» puede tener una cantidad arbitraria de métodos y variables que no estén definidas en «Coche», por lo que es imposible que el compilador pueda asignarlas gracias al tipado estricto de Java que impide que hagamos ese tipo de cosas para garantizar que todo funciona como debería.

    De no ser así, podríamos declarar una firma de método que tome como parámetro un Vehiculo pero podría llegarle un coche, cosa que causaría un error en tiempo de ejecución. Java se creó con unos principios que existen para evitar este tipo de errores.

    Un saludo.

Deja una respuesta