INSTITUTO TECNOLÓGIGO SUPERIOR DE COMALCALCO ALUMNA: Hernández Romero Ángeles Fernanda MATRICULA: TE170573 MATERIA: Programación avanzada MAESTRO: Elías Rodríguez Rodríguez SEMESTRE: 7mo. GRUPO: “C” ACTIVIDAD: Investigación de conceptos CARRERA: Ingeniería mecatrónica Comalcalco, Tabasco 26 de Octubre del 2020 Polimorfismo en JAVA Método de Polimorfismo Java En programación orientada a objetos, polimorfismo es la capacidad que tienen los objetos de una clase en ofrecer respuesta distinta e independiente en función de los parámetros (diferentes implementaciones) utilizados durante su invocación. Dicho de otro modo, el objeto como entidad puede contener valores de diferentes tipos durante la ejecución del programa. En JAVA el término polimorfismo también suele definirse como ‘Sobrecarga de parámetros’, que así de pronto no suena tan divertido, pero como veremos más adelante induce a cierta confusión. En realidad, suele confundirse con el tipo de polimorfismo más común, pero no es del todo exacto usar esta denominación. 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 Polimorfismo puro Sobrecarga Polimorfismo de inclusión 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 sobrescribe el método movimiento. Esto es el polimorfismo de inclusión. Un error común de un desarrollador es pensar que el siguiente 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á sobrescribiéndolo, porque para usar el polimorfismo de inclusión debemos usar el mismo identificador y la misma lista de parámetros que en la superclase. El método de la superclase usa dos bytes, mientras que el de la subclase usa dos ints. Ejemplo de Polimorfismo Un ejemplo clásico de polimorfismo es el siguiente. Podemos crear dos clases distintas: Gato y Perro, que heredan de la superclase Animal. La clase Animal tiene el método abstracto makesound() que se implementa de forma distinta en cada una de las subclases (gatos y perros suenan de forma distinta). Entonces, un tercer objeto puede enviar el mensaje de hacer sonido a un grupo de objetos Gato y Perro por medio de una variable de referencia de clase Animal, haciendo así un uso polimórfico de dichos objetos respecto del mensaje mover. class Animal { public void makeSound() { System.out.println("Grr..."); } } class Cat extends Animal { public void makeSound() { System.out.println("Meow"); } } class Dog extends Animal { public void makeSound() { System.out.println("Woof"); } } Como todos los objetos Gato y Perro son objetos Animales, podemos hacer lo siguiente: public static void main(String[ ] args) { Animal a = new Dog(); Animal b = new Cat(); } Creamos dos variables de referencia de tipo Animal y las apuntamos a los objetos Gato y Perro. Ahora, podemos llamar a los métodos makeSound(). a.makeSound(); //Outputs "Woof" b.makeSound(); //Outputs "Meow" Como decía el polimorfismo, que se refiere a la idea de "tener muchas formas", ocurre cuando hay una jerarquía de clases relacionadas entre sí a través de la herencia y este es un buen ejemplo. Excepciones en JAVA En Java los errores en tiempo de ejecución (cuando se está ejecutando el programa) se denominan excepciones, y esto ocurre cuando se produce un error en alguna de las instrucciones de nuestro programa, como por ejemplo cuando se hace una división entre cero, cuando un objeto es 'null' y no puede serlo, cuando no se abre correctamente un fichero, etc. Cuando se produce una excepción se muestra en la pantalla un mensaje de error y finaliza la ejecución del programa. En Java (al igual que en otros lenguajes de programación), existen mucho tipos de excepciones y enumerar cada uno de ellos seria casi una labor infinita. En lo referente a las excepciones hay que decir que se aprenden a base experiencia, de encontrarte con ellas y de saber solucionarlas. Cuando en Java se produce una excepción se crear un objeto de una determina clase (dependiendo del tipo de error que se haya producido), que mantendrá la información sobre el error producido y nos proporcionará los métodos necesarios para obtener dicha información. Estas clases tienen como clase padre la clase Throwable, por tanto, se mantiene una jerarquía en las excepciones. A continuación, mostramos algunas de las clases para que nos hagamos una idea de la jerarquía que siguen las excepciones, pero existen muchísimas más excepciones que las que mostramos: A continuación, vamos a mostrar un ejemplo de cómo al hacer una división entre cero, se produce una excepción. Veamos la siguiente imagen en el que podemos ver un fragmento de código y el resultado de la ejecución del código: Como vemos en nuestro programa tenemos 3 instrucciones. La primera debe de imprimir por pantalla el mensaje "ANTES DE HACER LA DIVISIÓN", la segunda debe de hacer la división y la última debe de imprimir por pantalla el mensaje "DESPUES DE HACER LA DIVISIÓN". La primera instrucción la ejecuta perfectamente, pero al llegar a la segunda se produce una "ArithmeticException" (excepción de la clase ArithmeticException) y se detiene la ejecución del programa ya que estamos dividiendo un número entre '0'. Por suerte Java nos permite hacer un control de las excepciones para que nuestro programa no se pare inesperadamente y aunque se produzca una excepción, nuestro programa siga su ejecución. Para ello tenemos la estructura "try – catch – finally" que la mostramos a continuación: try { // Instrucciones cuando no hay una excepción } catch (TypeException ex) { // Instrucciones cuando se produce una excepcion } finally { // Instruciones que se ejecutan, tanto si hay como sino hay excepciones } Respecto a la estructura "try – catch – finally", se ha de decir que primero se ejecuta el bloque "try", si se produce una excepción se ejecuta el bloque "catch" y por último el bloque "finally". En esta estructura se puede omitir el bloque "catch" o el bloque "finally", pero no ambos. Sabiendo esta estructura, podemos reescribir nuestro programa para que se ejecuten las tres instrucciones, aunque se produzca una excepción. Previamente debemos de saber cuál va a ser la clase de la excepción que puede aparecer que sería la "ArithmeticException" para definirla en la parte del "catch". Nuestro programa quedaría de la siguiente forma y se ejecutaría sin problema obteniendo también la información de la excepción: Como vemos capturamos la excepción en un objeto "ex" de la clase "ArithmeticException" y podemos obtener el mensaje de error que nos da la excepción. Vemos también que el programa termina su ejecución, aunque se haya producido una excepción. Dentro de una misma estructura podemos definir todas las excepciones que queramos. En el caso anterior hemos definido solo la excepción "ArithmeticException"; pero, por ejemplo, podemos definir también la excepción "NullPointerException", por si nos viene un valor a 'null' al hacer la división: En resumen, hemos puesto en esta entrada un ejemplo muy sencillo para controlar un par de excepciones bastante obvias como la división entre '0' y un 'null', que perfectamente lo podríamos haber controlado con una sentencia de control "if" mirando el contenido de los atributos, pero la finalidad de esta entrada era ver cómo controlar las excepciones con la estructura "try – catch – finally", que si lo sabemos utilizar nuestro programa deberá seguir funcionando aunque se produzcan excepciones. Decir también que es casi imposible aprenderse todas las excepciones que hay en Java (así que no os pongáis a empollarlas una a una porque igual no utilizareis ni el 10% de las que hay) ya que estas las iréis aprendiendo según os las vayáis encontrando en vuestros desarrollos. Tipos de excepciones: NOMBRE DESCRIPCION FileNotFoundException Lanza una excepción cuando el fichero no se encuentra. ClassNotFoundException Lanza una excepción cuando no existe la clase. EOFException Lanza una excepción cuando llega al final del fichero. ArrayIndexOutOfBoundsException Lanza una excepción cuando se accede a una posición de un array que no exista. NumberFormatException Lanza una excepción cuando se procesa un numero pero este es un dato alfanumérico. NullPointerException Lanza una excepción cuando intentando acceder a un miembro de un objeto para el que todavía no hemos reservado memoria. IOException Generaliza muchas excepciones anteriores. La ventaja es que no necesitamos controlar cada una de las excepciones. Excepcion Es la clase padre de IOException y de otras clases. Tiene la misma ventaja que IOException. Declaración de nuevas excepciones Java cuenta con muchas excepciones predefinidas para determinadas situaciones, como por ejemplo IOException para errores producidos en operaciones de entrada/salida, como es el caso de la lectura de ficheros. Para nuestros propios programas, a veces es útil que creemos nuestra excepción a medida. Para ello, tenemos que declarar una nueva clase que herede de Exception. Por ejemplo: public class BadPostCodeException extends Exception { public BadPostCodeException() { super(); } public BadPostCodeException(String message) { super(message); } } Normalmente, no necesitaremos declarar nuevos atributos ni métodos, aparte de los constructores. Cómo se lanza una excepción Cuando en un método necesitamos lanzar una excepción, utilizaremos la palabra clave throw, seguida de una instancia de la excepción a lanzar. Por ejemplo: if (postCode.length() != 5) { throw new BadPostCodeException("Postcodes must have 5 digits"); } for (int i = 0; i < postCode.length(); i++) { if (!Character.isDigit(postCode.charAt(i))) { throw new BadPostCodeException("Postcodes can only contain digits"); } } City city = lookupPostCode(postCode); if (city == null) { throw new BadPostCodeException("Postcode not in database: " + postCode); } Salvo en el caso de un grupo especial de excepciones, en general un método que pueda potencialmente lanzar una excepción debe indicarlo explícitamente mediante la palabra clave throws (nótese la "s" final, no confundir con throw) seguida del nombre de la clase de la excepción que puede lanzar: public class PostCodeManager { (...) public String town(String postCode) throws BadPostCodeException { if (postCode.length() != 5) { throw new BadPostCodeException("Postcodes must have 5 digits"); } for (int i = 0; i < postCode.length(); i++) { if (!Character.isDigit(postCode.charAt(i))) { throw new BadPostCodeException("Postcodes can only contain digits"); } } City city = lookupPostCode(postCode); if (city == null) { throw new BadPostCodeException("Postcode not in database: " + postCode); } (...) } (...) } Si el método puede lanzar más de una clase de excepción, se ponen los nombres de todas las clases separados por comas tras un único throws. Qué ocurre cuando se lanza una excepción Cuando se produce una excepción, la máquina virtual interrumpe la ejecución normal del programa y busca un bloque de código adecuado para tratar la situación. Si no encuentra este código en el método actual, la excepción se propaga hacia el método que lo haya invocado y se busca allí el código que la trate. Si tampoco ese método dispone del código adecuado, se propagará a su vez al que lo haya invocado, y así sucesivamente.En este apartado se explica el caso en que un método no dispone de código adecuado para tratar una excepción. Como se ha explicado, en este caso la excepción se propaga al método que lo haya invocado. Un método que no proporcione código para tratar la excepción debe declarar que puede lanzar la excepción con la palabra clave throws al igual que se explica en el apartado anterior: public class Location { (...) private String postCode; public String townAsString() throws BadPostCodeException { PostCodeManager manager = new PostCodeManager(); String town = manager.town(postCode); return postCode + " " + town; } (...) } En el ejemplo, la invocación al método town puede potencialmente lanzar una excepción. El método townAsString no está proporcionando código adecuado para tratarla. Por tanto, la excepción se propagaría a través de él, y entonces también debe declarar que puede lanzarla, como se ve en el ejemplo.Si en la llamada a town surge una excepción, esta se propaga al método que haya invocado a townAsString. La excepción continuará propagándose por la pila de invocaciones hasta que en algún método sea tratada. Si se alcanza el método main, y este tampoco proporciona ningún código para tratarla, la máquina virtual cortará la ejecución del programa y mostrará al usuario, normalmente en pantalla, el mensaje de la excepción y la ubicación del programa en que se haya producido. Cómo se trata una excepción Un método puede decidir capturar una excepción si tiene sentido colocar en ese método el código que la trata adecuadamente. Para capturar una excepción se utiliza un bloque try/catch: try { // código en que podría surgir la excepción } catch (ClaseDeLaExcepción e) { // código que trata la situación de la excepción } En el momento en que surja la excepción en el bloque try, se corta su ejecución y se pasa a ejecutar el código del bloque catch. Una vez finaliza el bloque catch, se continúa con la ejecución del resto del código que siga a try/catch. También se continúa normalmente en ese punto si el bloque try finaliza sin que surjan excepciones. A continuación, se muestra una implementación alternativa de townAsString en que se captura la excepción: public class Location { (...) private String postCode; public String townAsString() { String result; PostCodeManager manager = new PostCodeManager(); try { String town = manager.town(postCode); result = postCode + " " + town; } catch (BadPostCodeException e) { result = "Unknown location at postcode " + postCode; } return result; } (...) } Si en el método town surge una excepción, se corta la ejecución del bloque try, y por tanto ya no se ejecuta la instrucción result = postCode + " " + town, sino que el programa salta al interior del bloque catch. La instrucción return result se ejecuta siempre, surja o no alguna excepción, porque está a continuación del bloque try/catch. Nótese que el método townAsString no declara en este caso que se lance la excepción. El motivo es que, al capturar la excepción, está ya no puede propagarse a través del método. Referencias bibliográficas Mazón Olivo, B. E., Cartuche Calva, J. J., Chimarro Chipantiza, V. L., & Rivas Asanza, W. B. (2015). Fundamentos de programación orientada a objetos en JAVA. Deitel, P. J., & Deitel, H. M. (2008). Java: como programar. Pearson educacion. Tomás Gironés, J. (2012). El polimorfismo en Java. Moltó, G. Tema 1-Conceptos de Java para Estructuras de Datos. Fernández, O. B. (2005). Introducción al lenguaje de programación Java. Una guía básica, 9. Martínez, M. Y. M. (2018). Excepciones.