El polimorfismo se refiere a la capacidad de algunas entidades para presentarse en diferentes formas. Está representado popularmente por la mariposa, que se transforma de larva a pupa e imago. El polimorfismo también existe en los lenguajes de programación, como una técnica de modelado que le permite crear una única interfaz para varios operandos, argumentos y objetos. El polimorfismo de Java da como resultado un código más conciso y más fácil de mantener.
Si bien este tutorial se centra en el polimorfismo de subtipo, hay varios otros tipos que debe conocer. Comenzaremos con una descripción general de los cuatro tipos de polimorfismo.
descargar Obtener el código Descargar el código fuente, por ejemplo, las aplicaciones de este tutorial. Creado por Jeff Friesen para JavaWorld.Tipos de polimorfismo en Java
Hay cuatro tipos de polimorfismo en Java:
- La coerción es una operación que sirve a varios tipos a través de la conversión de tipo implícito. Por ejemplo, divide un número entero por otro número entero o un valor de punto flotante por otro valor de punto flotante. Si un operando es un número entero y el otro operando es un valor de punto flotante, el compilador coacciona (convierte implícitamente) el número entero a un valor de punto flotante para evitar un error de tipo. (No existe una operación de división que admita un operando entero y un operando de punto flotante). Otro ejemplo es pasar una referencia de objeto de subclase al parámetro de superclase de un método. El compilador coacciona el tipo de subclase al tipo de superclase para restringir las operaciones a las de la superclase.
- La sobrecarga se refiere al uso del mismo símbolo de operador o nombre de método en diferentes contextos. Por ejemplo, puede utilizar
+
para realizar una suma de enteros, una suma de punto flotante o una concatenación de cadenas, según los tipos de sus operandos. Además, varios métodos que tienen el mismo nombre pueden aparecer en una clase (mediante declaración y / o herencia). - El polimorfismo paramétrico estipula que dentro de una declaración de clase, un nombre de campo se puede asociar con diferentes tipos y un nombre de método se puede asociar con diferentes parámetros y tipos de retorno. El campo y el método pueden adoptar diferentes tipos en cada instancia de clase (objeto). Por ejemplo, un campo puede ser de tipo
Double
(un miembro de la biblioteca de clases estándar de Java que envuelve undouble
valor) y un método puede devolver aDouble
en un objeto, y el mismo campo puede ser de tipoString
y el mismo método puede devolver aString
en otro objeto . Java admite polimorfismo paramétrico a través de genéricos, que discutiré en un artículo futuro. - Subtipo significa que un tipo puede servir como subtipo de otro tipo. Cuando una instancia de subtipo aparece en un contexto de supertipo, la ejecución de una operación de supertipo en la instancia de subtipo da como resultado la ejecución de la versión del subtipo de esa operación. Por ejemplo, considere un fragmento de código que dibuja formas arbitrarias. Puede expresar este código de dibujo de forma más concisa introduciendo una
Shape
clase con undraw()
método; al introducirCircle
,Rectangle
y otras subclases que anulandraw()
; introduciendo una matriz de tipoShape
cuyos elementos almacenan referencias aShape
instancias de subclase; y llamandoShape
aldraw()
método de en cada instancia. Cuando llamasdraw()
, son lasCircle
's,Rectangle
' s u otraShape
instanciadraw()
método al que se llama. Decimos que hay muchas formas deShape
'sdraw()
método.
Este tutorial presenta el polimorfismo de subtipo. Aprenderá sobre upcasting y enlace tardío, clases abstractas (que no se pueden instanciar) y métodos abstractos (que no se pueden llamar). También aprenderá sobre la identificación del tipo de tiempo de ejecución y el downcasting, y obtendrá un primer vistazo a los tipos de retorno covariantes. Guardaré el polimorfismo paramétrico para un tutorial futuro.
Polimorfismo ad-hoc vs universal
Como muchos desarrolladores, clasifico la coerción y la sobrecarga como polimorfismo ad-hoc, y paramétrico y subtipo como polimorfismo universal. Si bien son técnicas valiosas, no creo que la coerción y la sobrecarga sean un verdadero polimorfismo; son más como conversiones de tipos y azúcar sintáctico.
Polimorfismo de subtipo: Upcasting y late binding
El polimorfismo de subtipo se basa en la conversión ascendente y la unión tardía. La conversión ascendente es una forma de conversión en la que se eleva la jerarquía de herencia de un subtipo a un supertipo. No interviene ningún operador de conversión porque el subtipo es una especialización del supertipo. Por ejemplo, Shape s = new Circle();
upcasts de Circle
a Shape
. Esto tiene sentido porque un círculo es una especie de forma.
Después de convertir Circle
a Shape
, no puede llamar a Circle
métodos específicos, como un getRadius()
método que devuelve el radio del círculo, porque los Circle
métodos específicos no forman parte de Shape
la interfaz de. Perder el acceso a las características del subtipo después de reducir una subclase a su superclase parece inútil, pero es necesario para lograr el polimorfismo del subtipo.
Supongamos que Shape
declara un draw()
método, su Circle
subclase anula este método, Shape s = new Circle();
se acaba de ejecutar y la siguiente línea especifica s.draw();
. ¿Qué draw()
método se llama: Shape
's draw()
método o Circle
' s draw()
método? El compilador no sabe a qué draw()
método llamar. Todo lo que puede hacer es verificar que exista un método en la superclase y verificar que la lista de argumentos y el tipo de retorno de la llamada al método coincidan con la declaración del método de la superclase. Sin embargo, el compilador también inserta una instrucción en el código compilado que, en tiempo de ejecución, busca y usa cualquier referencia s
para llamar al draw()
método correcto . Esta tarea se conoce como enlace tardío .
Enlace tardío vs enlace temprano
El enlace tardío se utiliza para llamadas a final
métodos que no son de instancia. Para todas las demás llamadas a métodos, el compilador sabe a qué método llamar. Inserta una instrucción en el código compilado que llama al método asociado con el tipo de variable y no a su valor. Esta técnica se conoce como enlace temprano .
He creado una aplicación que demuestra polimorfismo de subtipo en términos de upcasting y enlace tardío. Esta aplicación consiste en Shape
, Circle
, Rectangle
, y Shapes
clases, donde cada clase se almacena en su propio archivo de origen. El Listado 1 presenta las tres primeras clases.
Listado 1. Declarar una jerarquía de formas
class Shape { void draw() { } } class Circle extends Shape { private int x, y, r; Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; } // For brevity, I've omitted getX(), getY(), and getRadius() methods. @Override void draw() { System.out.println("Drawing circle (" + x + ", "+ y + ", " + r + ")"); } } class Rectangle extends Shape { private int x, y, w, h; Rectangle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } // For brevity, I've omitted getX(), getY(), getWidth(), and getHeight() // methods. @Override void draw() { System.out.println("Drawing rectangle (" + x + ", "+ y + ", " + w + "," + h + ")"); } }
El Listado 2 presenta la Shapes
clase de aplicación cuyo main()
método impulsa la aplicación.
Listado 2. Upcasting y enlace tardío en polimorfismo de subtipo
class Shapes { public static void main(String[] args) { Shape[] shapes = { new Circle(10, 20, 30), new Rectangle(20, 30, 40, 50) }; for (int i = 0; i < shapes.length; i++) shapes[i].draw(); } }
La declaración de la shapes
matriz demuestra upcasting. Las referencias Circle
y Rectangle
se almacenan en shapes[0]
y shapes[1]
y se actualizan para escribir Shape
. Cada uno de shapes[0]
y shapes[1]
se considera una Shape
instancia: shapes[0]
no se considera un Circle
; shapes[1]
no se considera un Rectangle
.
La unión tardía se demuestra mediante la shapes[i].draw();
expresión. Cuando i
es igual 0
, las causas de instrucción generados por el compilador Circle
's draw()
método que se llama. Cuando i
es igual 1
, sin embargo, esto hace que la instrucción Rectangle
's draw()
método para ser llamados. Ésta es la esencia del polimorfismo de subtipo.
Suponiendo que los cuatro archivos de origen ( Shapes.java
, Shape.java
, Rectangle.java
, y Circle.java
) se encuentran en el directorio actual, compilarlas ya sea a través de las siguientes líneas de comandos:
javac *.java javac Shapes.java
Ejecute la aplicación resultante:
java Shapes
Debe observar el siguiente resultado:
Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)
Métodos y clases abstractas
Al diseñar jerarquías de clases, encontrará que las clases más cercanas a la parte superior de estas jerarquías son más genéricas que las clases que están más abajo. Por ejemplo, una Vehicle
superclase es más genérica que una Truck
subclase. De manera similar, una Shape
superclase es más genérica que una Circle
o una Rectangle
subclase.
It doesn't make sense to instantiate a generic class. After all, what would a Vehicle
object describe? Similarly, what kind of shape is represented by a Shape
object? Rather than code an empty draw()
method in Shape
, we can prevent this method from being called and this class from being instantiated by declaring both entities to be abstract.
Java provides the abstract
reserved word to declare a class that cannot be instantiated. The compiler reports an error when you try to instantiate this class. abstract
is also used to declare a method without a body. The draw()
method doesn't need a body because it is unable to draw an abstract shape. Listing 3 demonstrates.
Listing 3. Abstracting the Shape class and its draw() method
abstract class Shape { abstract void draw(); // semicolon is required }
Abstract cautions
The compiler reports an error when you attempt to declare a class abstract
and final
. For example, the compiler complains about abstract final class Shape
because an abstract class cannot be instantiated and a final class cannot be extended. The compiler also reports an error when you declare a method abstract
but don't declare its class abstract
. Removing abstract
from the Shape
class's header in Listing 3 would result in an error, for instance. This would be an error because a non-abstract (concrete) class cannot be instantiated when it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all of the abstract methods, or else the extending class must itself be declared to be abstract; otherwise, the compiler will report an error.
An abstract class can declare fields, constructors, and non-abstract methods in addition to or instead of abstract methods. For example, an abstract Vehicle
class might declare fields describing its make, model, and year. Also, it might declare a constructor to initialize these fields and concrete methods to return their values. Check out Listing 4.
Listing 4. Abstracting a vehicle
abstract class Vehicle { private String make, model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } abstract void move(); }
You'll note that Vehicle
declares an abstract move()
method to describe the movement of a vehicle. For example, a car rolls down the road, a boat sails across the water, and a plane flies through the air. Vehicle
's subclasses would override move()
and provide an appropriate description. They would also inherit the methods and their constructors would call Vehicle
's constructor.
Downcasting and RTTI
Moving up the class hierarchy, via upcasting, entails losing access to subtype features. For example, assigning a Circle
object to Shape
variable s
means that you cannot use s
to call Circle
's getRadius()
method. However, it's possible to once again access Circle
's getRadius()
method by performing an explicit cast operation like this one: Circle c = (Circle) s;
.
This assignment is known as downcasting because you are casting down the inheritance hierarchy from a supertype to a subtype (from the Shape
superclass to the Circle
subclass). Although an upcast is always safe (the superclass's interface is a subset of the subclass's interface), a downcast isn't always safe. Listing 5 shows what kind of trouble could ensue if you use downcasting incorrectly.
Listing 5. The problem with downcasting
class Superclass { } class Subclass extends Superclass { void method() { } } public class BadDowncast { public static void main(String[] args) { Superclass superclass = new Superclass(); Subclass subclass = (Subclass) superclass; subclass.method(); } }
Listing 5 presents a class hierarchy consisting of Superclass
and Subclass
, which extends Superclass
. Furthermore, Subclass
declares method()
. A third class named BadDowncast
provides a main()
method that instantiates Superclass
. BadDowncast
then tries to downcast this object to Subclass
and assign the result to variable subclass
.
En este caso, el compilador no se quejará porque la conversión de una superclase a una subclase en la misma jerarquía de tipos es legal. Dicho esto, si se permitiera la asignación, la aplicación fallaría cuando intentara ejecutarse subclass.method();
. En este caso, la JVM intentaría llamar a un método inexistente, porque Superclass
no declara method()
. Afortunadamente, la JVM verifica que una conversión sea legal antes de realizar una operación de conversión. Al detectar que Superclass
no declara method()
, lanzaría un ClassCastException
objeto. (Discutiré las excepciones en un artículo futuro).
Compile el Listado 5 de la siguiente manera:
javac BadDowncast.java
Ejecute la aplicación resultante:
java BadDowncast