Polimorfismo de Java y sus tipos

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:

  1. 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.
  2. 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).
  3. 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 un doublevalor) y un método puede devolver a Doubleen un objeto, y el mismo campo puede ser de tipo Stringy el mismo método puede devolver a Stringen otro objeto . Java admite polimorfismo paramétrico a través de genéricos, que discutiré en un artículo futuro.
  4. 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 Shapeclase con un draw()método; al introducir Circle, Rectangley otras subclases que anulan draw(); introduciendo una matriz de tipo Shapecuyos elementos almacenan referencias a Shapeinstancias de subclase; y llamando Shapeal draw()método de en cada instancia. Cuando llamas draw(), son las Circle's, Rectangle' s u otra Shapeinstanciadraw()método al que se llama. Decimos que hay muchas formas de Shape's draw()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 Circlea Shape. Esto tiene sentido porque un círculo es una especie de forma.

Después de convertir Circlea Shape, no puede llamar a Circlemétodos específicos, como un getRadius()método que devuelve el radio del círculo, porque los Circlemétodos específicos no forman parte de Shapela 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 Shapedeclara un draw()método, su Circlesubclase 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 spara 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 finalmé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 Shapesclases, 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 Shapesclase 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 shapesmatriz demuestra upcasting. Las referencias Circley Rectanglese almacenan en shapes[0]y shapes[1]y se actualizan para escribir Shape. Cada uno de shapes[0]y shapes[1]se considera una Shapeinstancia: 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 ies igual 0, las causas de instrucción generados por el compilador Circle's draw()método que se llama. Cuando ies 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 Vehiclesuperclase es más genérica que una Trucksubclase. De manera similar, una Shapesuperclase es más genérica que una Circleo una Rectanglesubclase.

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 Superclassno declara method(). Afortunadamente, la JVM verifica que una conversión sea legal antes de realizar una operación de conversión. Al detectar que Superclassno declara method(), lanzaría un ClassCastExceptionobjeto. (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