Polimorfismo en Java

1. Información general

Todos los lenguajes de programación orientada a objetos (OOP) deben exhibir cuatro características básicas: abstracción, encapsulación, herencia y polimorfismo.

En este artículo, cubrimos dos tipos de núcleo de polimorfismo: estática o en tiempo de compilación de polimorfismo y dinámicos o de tiempo de ejecución de polimorfismo . El polimorfismo estático se aplica en tiempo de compilación, mientras que el polimorfismo dinámico se realiza en tiempo de ejecución.

2. Polimorfismo estático

Según Wikipedia, el polimorfismo estático es una imitación del polimorfismo que se resuelve en tiempo de compilación y, por lo tanto, elimina las búsquedas de tablas virtuales en tiempo de ejecución .

Por ejemplo, nuestra clase TextFile en una aplicación de administrador de archivos puede tener tres métodos con la misma firma del método read () :

public class TextFile extends GenericFile { //... public String read() { return this.getContent() .toString(); } public String read(int limit) { return this.getContent() .toString() .substring(0, limit); } public String read(int start, int stop) { return this.getContent() .toString() .substring(start, stop); } }

Durante la compilación del código, el compilador verifica que todas las invocaciones del método de lectura correspondan al menos a uno de los tres métodos definidos anteriormente.

3. Polimorfismo dinámico

Con polimorfismo dinámico, la máquina virtual Java (JVM) maneja la detección del método apropiado para ejecutar cuando se asigna una subclase a su formulario principal . Esto es necesario porque la subclase puede anular algunos o todos los métodos definidos en la clase principal.

En una aplicación hipotética de administración de archivos, definamos la clase principal para todos los archivos llamados GenericFile :

public class GenericFile { private String name; //... public String getFileInfo() { return "Generic File Impl"; } }

También podemos implementar una clase ImageFile que extiende el GenericFile pero anula el método getFileInfo () y agrega más información:

public class ImageFile extends GenericFile { private int height; private int width; //... getters and setters public String getFileInfo() { return "Image File Impl"; } }

Cuando creamos una instancia de ImageFile y la asignamos a una clase GenericFile , se realiza una conversión implícita. Sin embargo, la JVM mantiene una referencia a la forma real de ImageFile .

La construcción anterior es análoga a la invalidación del método. Podemos confirmar esto invocando el método getFileInfo () mediante:

public static void main(String[] args) { GenericFile genericFile = new ImageFile("SampleImageFile", 200, 100, new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB) .toString() .getBytes(), "v1.0.0"); logger.info("File Info: \n" + genericFile.getFileInfo()); }

Como se esperaba, genericFile.getFileInfo () activa el método getFileInfo () de la clase ImageFile como se ve en el resultado a continuación:

File Info: Image File Impl

4. Otras características polimórficas en Java

Además de estos dos tipos principales de polimorfismo en Java, existen otras características en el lenguaje de programación Java que exhiben polimorfismo. Analicemos algunas de estas características.

4.1. Coerción

La coerción polimórfica se ocupa de la conversión de tipo implícita realizada por el compilador para evitar errores de tipo. Un ejemplo típico se ve en una concatenación de enteros y cadenas:

String str = “string” + 2;

4.2. Sobrecarga del operador

La sobrecarga de operador o método se refiere a una característica polimórfica del mismo símbolo u operador que tiene diferentes significados (formas) según el contexto.

Por ejemplo, el símbolo más (+) se puede utilizar para la suma matemática, así como para la concatenación de cadenas . En cualquier caso, solo el contexto (es decir, los tipos de argumentos) determina la interpretación del símbolo:

String str = "2" + 2; int sum = 2 + 2; System.out.printf(" str = %s\n sum = %d\n", str, sum);

Salida:

str = 22 sum = 4

4.3. Parámetros polimórficos

El polimorfismo paramétrico permite asociar el nombre de un parámetro o método en una clase con diferentes tipos. Tenemos un ejemplo típico a continuación donde definimos el contenido como una cadena y luego como un entero :

public class TextFile extends GenericFile { private String content; public String setContentDelimiter() { int content = 100; this.content = this.content + content; } }

También es importante tener en cuenta que la declaración de parámetros polimórficos puede conducir a un problema conocido como ocultación de variables donde una declaración local de un parámetro siempre anula la declaración global de otro parámetro con el mismo nombre.

Para resolver este problema, a menudo se recomienda utilizar referencias globales como esta palabra clave para señalar variables globales dentro de un contexto local.

4.4. Subtipos polimórficos

El subtipo polimórfico convenientemente nos permite asignar múltiples subtipos a un tipo y esperar que todas las invocaciones en el tipo activen las definiciones disponibles en el subtipo.

Por ejemplo, si tenemos una colección de GenericFile sy invocamos el método getInfo () en cada uno de ellos, podemos esperar que la salida sea diferente según el subtipo del que se derivó cada elemento de la colección:

GenericFile [] files = {new ImageFile("SampleImageFile", 200, 100, new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB).toString() .getBytes(), "v1.0.0"), new TextFile("SampleTextFile", "This is a sample text content", "v1.0.0")}; for (int i = 0; i < files.length; i++) { files[i].getInfo(); }

El polimorfismo de subtipo es posible gracias a una combinación de conversión ascendente y unión tardía . Upcasting implica la conversión de la jerarquía de herencia de un supertipo a un subtipo:

ImageFile imageFile = new ImageFile(); GenericFile file = imageFile;

The resulting effect of the above is that ImageFile-specific methods cannot be invoked on the new upcast GenericFile. However, methods in the subtype override similar methods defined in the supertype.

To resolve the problem of not being able to invoke subtype-specific methods when upcasting to a supertype, we can do a downcasting of the inheritance from a supertype to a subtype. This is done by:

ImageFile imageFile = (ImageFile) file;

Late bindingstrategy helps the compiler to resolve whose method to trigger after upcasting. In the case of imageFile#getInfo vs file#getInfo in the above example, the compiler keeps a reference to ImageFile‘s getInfo method.

5. Problems With Polymorphism

Let's look at some ambiguities in polymorphism that could potentially lead to runtime errors if not properly checked.

5.1. Type Identification During Downcasting

Recall that we earlier lost access to some subtype-specific methods after performing an upcast. Although we were able to solve this with a downcast, this does not guarantee actual type checking.

For example, if we perform an upcast and subsequent downcast:

GenericFile file = new GenericFile(); ImageFile imageFile = (ImageFile) file; System.out.println(imageFile.getHeight());

We notice that the compiler allows a downcast of a GenericFile into an ImageFile, even though the class actually is a GenericFile and not an ImageFile.

Consequently, if we try to invoke the getHeight() method on the imageFile class, we get a ClassCastException as GenericFile does not define getHeight() method:

Exception in thread "main" java.lang.ClassCastException: GenericFile cannot be cast to ImageFile

To solve this problem, the JVM performs a Run-Time Type Information (RTTI) check. We can also attempt an explicit type identification by using the instanceof keyword just like this:

ImageFile imageFile; if (file instanceof ImageFile) { imageFile = file; }

The above helps to avoid a ClassCastException exception at runtime. Another option that may be used is wrapping the cast within a try and catch block and catching the ClassCastException.

It should be noted that RTTI check is expensive due to the time and resources needed to effectively verify that a type is correct. In addition, frequent use of the instanceof keyword almost always implies a bad design.

5.2. Fragile Base Class Problem

According to Wikipedia, base or superclasses are considered fragile if seemingly safe modifications to a base class may cause derived classes to malfunction.

Let's consider a declaration of a superclass called GenericFile and its subclass TextFile:

public class GenericFile { private String content; void writeContent(String content) { this.content = content; } void toString(String str) { str.toString(); } }
public class TextFile extends GenericFile { @Override void writeContent(String content) { toString(content); } }

When we modify the GenericFile class:

public class GenericFile { //... void toString(String str) { writeContent(str); } }

Observamos que la modificación anterior deja TextFile en una recursividad infinita en el método writeContent () , lo que eventualmente resulta en un desbordamiento de pila.

Para abordar un problema de clase base frágil, podemos usar la palabra clave final para evitar que las subclases anulen el método writeContent () . La documentación adecuada también puede ayudar. Y por último, pero no menos importante, la composición debería preferirse generalmente a la herencia.

6. Conclusión

En este artículo, discutimos el concepto fundamental de polimorfismo, centrándonos tanto en las ventajas como en las desventajas.

Como siempre, el código fuente de este artículo está disponible en GitHub.