El polimorfismo es uno de los principios básicos de la programación orientada a objetos. Le permite aprovechar el poder de la escritura segura de Java y escribir código utilizable y mantenible. Se ha dicho mucho sobre él, pero espero que todos puedan sacar algo nuevo de esta reseña.
Por ejemplo, al declarar un método método público nulo (Objeto o), la firma será el nombre del método y el tipo del parámetro Objeto. El tipo de devolución NO está incluido en la firma. ¡Es importante! A continuación, compilemos nuestro código fuente. Como sabemos, para ello se debe guardar el código en un archivo con el nombre de la clase y la extensión java. El código Java se compila utilizando el compilador " javac " en algún formato intermedio que puede ser ejecutado por la Máquina Virtual Java (JVM). Este formato intermedio se llama código de bytes y está contenido en archivos con la extensión .class. Ejecutemos el comando para compilar:
En el código de bytes podemos ver que la llamada a un método a través de un objeto cuyo tipo se especificó la clase se realiza usando
Polimorfismo predominante o dinámico. Entonces, comencemos guardando el archivo HelloWorld.java con el siguiente contenido:
Como puede ver, en el código de bytes de las líneas con una llamada a un método, se indica la misma referencia al método a llamar
La redefinición también está asociada al concepto de “ covarianza ”. Veamos un ejemplo:
Es decir, durante la sobrecarga, el compilador comprueba la corrección. Es importante. Pero, ¿cómo determina realmente el compilador que es necesario llamar a un determinado método? Utiliza la regla "el método más específico" descrita en la especificación del lenguaje Java: " 15.12.2.5. Elección del método más específico ". Para demostrar cómo funciona, tomemos un ejemplo del programador Java profesional certificado de Oracle:
Resulta que durante la compilación, la información sobre los tipos y la cantidad de argumentos (que está disponible en el momento de la compilación) se utilizará para determinar la firma del método. Si el método es uno de los métodos del objeto (es decir, un método de instancia), la llamada al método real se determinará en tiempo de ejecución mediante la búsqueda de métodos dinámicos (es decir, enlace dinámico). Para que quede más claro, tomemos un ejemplo similar al comentado anteriormente:
Como se indicó, el compilador ha determinado que en el futuro se llamará algún método virtual. Es decir, el cuerpo del método se definirá en tiempo de ejecución. Pero en el momento de la compilación, de los tres métodos, el compilador eligió el más adecuado, por lo que indicó el número:
¿Qué tipo de método es este? Este es un enlace al método. En términos generales, esta es una pista mediante la cual, en tiempo de ejecución, la máquina virtual Java puede determinar qué método buscar para ejecutar. Se pueden encontrar más detalles en el súper artículo: "¿ Cómo maneja JVM la sobrecarga y anulación de métodos internamente ?".
Introducción
Creo que todos sabemos que el lenguaje de programación Java pertenece a Oracle. Por tanto, nuestro camino comienza con el sitio: www.oracle.com . Hay un "Menú" en la página principal. En él, en el apartado “Documentación” hay un subapartado “Java”. Todo lo relacionado con las funciones básicas del lenguaje pertenece a la "documentación Java SE", por lo que seleccionamos esta sección. Se abrirá la sección de documentación para la última versión, pero por ahora aparecerá el mensaje "¿Busca una versión diferente?". Elijamos la opción: JDK8. En la página veremos muchas opciones diferentes. Pero nos interesa aprender el idioma: " Rutas de aprendizaje de tutoriales de Java ". En esta página encontraremos otro apartado: " Aprendizaje del Lenguaje Java ". Este es el lugar más sagrado de los santos, un tutorial sobre los conceptos básicos de Java de Oracle. Java es un lenguaje de programación orientado a objetos (OOP), por lo que aprender el lenguaje incluso en el sitio web de Oracle comienza con una discusión de los conceptos básicos de " Conceptos de programación orientada a objetos ". Por el nombre mismo se desprende claramente que Java se centra en trabajar con objetos. De la subsección "¿ Qué es un objeto? ", queda claro que los objetos en Java constan de estado y comportamiento. Imaginemos que tenemos una cuenta bancaria. La cantidad de dinero en la cuenta es un estado y los métodos para trabajar con este estado son el comportamiento. Los objetos necesitan ser descritos de alguna manera (decir qué estado y comportamiento pueden tener) y esta descripción es la clase . Cuando creamos un objeto de alguna clase, especificamos esta clase y esto se llama " tipo de objeto ". De ahí que se diga que Java es un lenguaje fuertemente tipado, como se indica en la especificación del lenguaje Java en la sección " Capítulo 4. Tipos, Valores y Variables ". El lenguaje Java sigue conceptos de programación orientada a objetos y admite la herencia mediante la palabra clave extends. ¿Por qué expansión? Porque con la herencia, una clase hija hereda el comportamiento y el estado de la clase padre y puede complementarlos, es decir. ampliar la funcionalidad de la clase base. También se puede especificar una interfaz en la descripción de la clase utilizando la palabra clave implements. Cuando una clase implementa una interfaz, significa que la clase se ajusta a algún contrato: una declaración del programador al resto del entorno de que la clase tiene un determinado comportamiento. Por ejemplo, el reproductor tiene varios botones. Estos botones son una interfaz para controlar el comportamiento del reproductor, y el comportamiento cambiará el estado interno del reproductor (por ejemplo, el volumen). En este caso, el estado y el comportamiento como descripción darán una clase. Si una clase implementa una interfaz, entonces un objeto creado por esta clase puede describirse mediante un tipo no solo por la clase, sino también por la interfaz. Veamos un ejemplo:public class MusicPlayer {
public static interface Device {
public void turnOn();
public void turnOff();
}
public static class Mp3Player implements Device {
public void turnOn() {
System.out.println("On. Ready for mp3.");
}
public void turnOff() {
System.out.println("Off");
}
}
public static class Mp4Player extends Mp3Player {
@Override
public void turnOn() {
System.out.println("On. Ready for mp3/mp4.");
}
}
public static void main(String []args) throws Exception{
// Какое-то устройство (Тип = Device)
Device mp3Player = new Mp3Player();
mp3Player.turnOn();
// У нас есть mp4 проигрыватель, но нам от него нужно только mp3
// Пользуемся им Cómo mp3 проигрывателем (Тип = Mp3Player)
Mp3Player mp4Player = new Mp4Player();
mp4Player.turnOn();
}
}
El tipo es una descripción muy importante. Indica cómo vamos a trabajar con el objeto, es decir. qué comportamiento esperamos del objeto. Los comportamientos son métodos. Por tanto, comprendamos los métodos. En el sitio web de Oracle, los métodos tienen su propia sección en el Tutorial de Oracle: " Definición de métodos ". Lo primero que debemos aprender del artículo: la firma de un método es el nombre del método y los tipos de parámetros :
javac MusicPlayer.java
una vez compilado el código Java, podemos ejecutarlo. Usando la utilidad " java " para comenzar, se iniciará el proceso de la máquina virtual Java para ejecutar el código de bytes pasado en el archivo de clase. Ejecutemos el comando para iniciar la aplicación: java MusicPlayer
. Veremos en pantalla el texto especificado en el parámetro de entrada del método println. Curiosamente, al tener el bytecode en un archivo con extensión .class, podemos verlo usando la utilidad " javap ". Ejecutemos el comando <ocde>javap -c MusicPlayer:
invokevirtual
y el compilador ha calculado qué firma de método se debe usar. Por qué invokevirtual
? Porque hay una llamada (invocar se traduce como llamada) de un método virtual. ¿Qué es un método virtual? Este es un método cuyo cuerpo se puede anular durante la ejecución del programa. Simplemente imagine que tiene una lista de correspondencia entre una determinada clave (firma del método) y el cuerpo (código) del método. Y esta correspondencia entre la clave y el cuerpo del método puede cambiar durante la ejecución del programa. Por tanto el método es virtual. De forma predeterminada, en Java, los métodos que NO son estáticos, NO finales y NO privados son virtuales. Gracias a esto, Java admite el principio de polimorfismo de programación orientada a objetos. Como ya habrás comprendido, de esto se trata nuestra revisión de hoy.
Polimorfismo
En el sitio web de Oracle, en su Tutorial oficial, hay una sección separada: " Polimorfismo ". Usemos el compilador en línea de Java para ver cómo funciona el polimorfismo en Java. Por ejemplo, tenemos una clase abstracta Número que representa un número en Java. ¿Qué permite? Tiene algunas técnicas básicas que todos los herederos tendrán. Cualquiera que herede de Número dice literalmente: "Soy un número, puedes trabajar conmigo como un número". Por ejemplo, para cualquier sucesor puede utilizar el método intValue() para obtener su valor entero. Si observa la API de Java para Number, puede ver que el método es abstracto, es decir, cada sucesor de Number debe implementar este método por sí mismo. ¿Pero qué nos aporta esto? Veamos un ejemplo:public class HelloWorld {
public static int summ(Number first, Number second) {
return first.intValue() + second.intValue();
}
public static void main(String []args){
System.out.println(summ(1, 2));
System.out.println(summ(1L, 4L));
System.out.println(summ(1L, 5));
System.out.println(summ(1.0, 3));
}
}
Como puede verse en el ejemplo, gracias al polimorfismo, podemos escribir un método que aceptará argumentos de cualquier tipo como entrada, que será un descendiente de Número (no podemos obtener Número, porque es una clase abstracta). Como fue el caso con el ejemplo del reproductor, en este caso estamos diciendo que queremos trabajar con algo, como Número. Sabemos que cualquiera que sea un Número debe poder proporcionar su valor entero. Y eso es suficiente para nosotros. No queremos entrar en detalles sobre la implementación de un objeto específico y queremos trabajar con este objeto mediante métodos comunes a todos los descendientes de Number. La lista de métodos que estarán disponibles para nosotros estará determinada por el tipo en el momento de la compilación (como vimos anteriormente en el código de bytes). En este caso, nuestro tipo será Número. Como puede ver en el ejemplo, estamos pasando diferentes números de diferentes tipos, es decir, el método de suma recibirá Integer, Long y Double como entrada. Pero lo que todos tienen en común es que son descendientes del Número abstracto y, por lo tanto, anulan su comportamiento en el método intValue, porque cada tipo específico sabe cómo convertir ese tipo a Integer. Este polimorfismo se implementa mediante el llamado overriding, en inglés Overriding.
public class HelloWorld {
public static class Parent {
public void method() {
System.out.println("Parent");
}
}
public static class Child extends Parent {
public void method() {
System.out.println("Child");
}
}
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
parent.method();
child.method();
}
}
Hagamos javac HelloWorld.java
y javap -c HelloWorld
:
invokevirtual (#6)
. Vamos a hacerlo java HelloWorld
. Como podemos ver, las variables padre e hijo se declaran con el tipo Padre, pero la implementación en sí se llama según qué objeto se asignó a la variable (es decir, qué tipo de objeto). Durante la ejecución del programa (también dicen en tiempo de ejecución), la JVM, dependiendo del objeto, al llamar a métodos usando la misma firma, ejecutaba diferentes métodos. Es decir, usando la clave de la firma correspondiente, primero recibimos el cuerpo de un método y luego recibimos otro. Dependiendo de qué objeto esté en la variable. Esta determinación en el momento de la ejecución del programa de qué método se llamará también se denomina enlace tardío o enlace dinámico. Es decir, la coincidencia entre la firma y el cuerpo del método se realiza dinámicamente, dependiendo del objeto sobre el que se llama el método. Naturalmente, no se pueden anular los miembros estáticos de una clase (miembro de la clase), así como los miembros de la clase con tipo de acceso privado o final. Las anotaciones @Override también ayudan a los desarrolladores. Ayuda al compilador a comprender que en este punto vamos a anular el comportamiento de un método ancestro. Si cometimos un error en la firma del método, el compilador nos lo informará inmediatamente. Por ejemplo:
public static class Parent {
public void method() {
System.out.println("parent");
}
}
public static class Child extends Parent {
@Override
public void method(String text) {
System.out.println("child");
}
}
No se compila con error: error: el método no anula ni implementa un método de un supertipo
public class HelloWorld {
public static class Parent {
public Number method() {
return 1;
}
}
public static class Child extends Parent {
@Override
public Integer method() {
return 2;
}
}
public static void main(String[] args) {
System.out.println(new Child().method());
}
}
A pesar de lo aparentemente abstruso, el significado se reduce al hecho de que al anular, podemos devolver no solo el tipo que se especificó en el antepasado, sino también un tipo más específico. Por ejemplo, el antepasado devolvió Número y podemos devolver Integer, el descendiente de Número. Lo mismo se aplica a las excepciones declaradas en los lanzamientos del método. Los herederos pueden anular el método y refinar la excepción lanzada. Pero no pueden expandirse. Es decir, si el padre lanza una IOException, entonces podemos lanzar la EOFException más precisa, pero no podemos lanzar una excepción. Del mismo modo, no se puede limitar el alcance ni imponer restricciones adicionales. Por ejemplo, no puedes agregar estática.
Ocultación
También existe el “ ocultamiento ”. Ejemplo:public class HelloWorld {
public static class Parent {
public static void method() {
System.out.println("Parent");
}
}
public static class Child extends Parent {
public static void method() {
System.out.println("Child");
}
}
public static void main(String[] args) {
Parent parent = new Parent();
Parent child = new Child();
parent.method();
child.method();
}
}
Esto es algo bastante obvio si lo piensas. Los miembros estáticos de una clase pertenecen a la clase, es decir al tipo de variable. Por lo tanto, es lógico que si el niño es del tipo Padre, entonces el método se llamará en el Padre y no en el niño. Si miramos el código de bytes, como hicimos antes, veremos que el método estático se llama usando invokestatic. Esto le explica a la JVM que necesita mirar el tipo y no la tabla de métodos, como lo hizo invokevirtual o invokeinterface.
Métodos de sobrecarga
¿Qué más vemos en el Tutorial de Java Oracle? En la sección " Definición de métodos " estudiada anteriormente hay algo sobre la sobrecarga. ¿Lo que es? En ruso, esto es "sobrecarga de métodos", y estos métodos se denominan "sobrecargados". Entonces, sobrecarga de métodos. A primera vista, todo es sencillo. Abramos un compilador de Java en línea, por ejemplo el compilador de Java en línea tutorialspoint .public class HelloWorld {
public static void main(String []args){
HelloWorld hw = new HelloWorld();
hw.say(1);
hw.say("1");
}
public static void say(Integer number) {
System.out.println("Integer " + number);
}
public static void say(String number) {
System.out.println("String " + number);
}
}
Así que aquí todo parece sencillo. Como se indica en el tutorial de Oracle, los métodos sobrecargados (en este caso, el método say) difieren en la cantidad y el tipo de argumentos pasados al método. No se puede declarar el mismo nombre y el mismo número de tipos idénticos de argumentos, porque el compilador no podrá distinguirlos entre sí. Vale la pena señalar algo muy importante de inmediato:
public class Overload{
public void method(Object o) {
System.out.println("Object");
}
public void method(java.io.FileNotFoundException f) {
System.out.println("FileNotFoundException");
}
public void method(java.io.IOException i) {
System.out.println("IOException");
}
public static void main(String args[]) {
Overload test = new Overload();
test.method(null);
}
}
Tome un ejemplo de aquí: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... Como puede ver, estamos pasando nulo al método. El compilador intenta determinar el tipo más específico. El objeto no es adecuado porque todo se hereda de él. Adelante. Hay 2 clases de excepciones. Miremos java.io.IOException y veamos que hay una FileNotFoundException en "Subclases conocidas directamente". Es decir, resulta que FileNotFoundException es el tipo más específico. Por lo tanto, el resultado será la salida de la cadena "FileNotFoundException". Pero si reemplazamos IOException con EOFException, resulta que tenemos dos métodos en el mismo nivel de jerarquía en el árbol de tipos, es decir, para ambos, IOException es el padre. El compilador no podrá elegir qué método llamar y arrojará un error de compilación: reference to method is ambiguous
. Un ejemplo más:
public class Overload{
public static void method(int... array) {
System.out.println("1");
}
public static void main(String args[]) {
method(1, 2);
}
}
El resultado será 1. No hay preguntas aquí. El tipo int... es un vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html y en realidad no es más que "azúcar sintáctico" y en realidad es un int. .. la matriz se puede leer como matriz int[]. Si ahora agregamos un método:
public static void method(long a, long b) {
System.out.println("2");
}
Entonces mostrará no 1, sino 2, porque Estamos pasando 2 números y 2 argumentos coinciden mejor que una matriz. Si agregamos un método:
public static void method(Integer a, Integer b) {
System.out.println("3");
}
Entonces todavía veremos 2. Porque en este caso las primitivas coinciden más exactamente que el boxeo en Integer. Sin embargo, si ejecutamos, method(new Integer(1), new Integer(2));
imprimirá 3. Los constructores en Java son similares a los métodos y, dado que también se pueden usar para obtener una firma, se les aplican las mismas reglas de "resolución de sobrecarga" que los métodos sobrecargados. La especificación del lenguaje Java nos lo dice en " 8.8.8. Sobrecarga de constructores ". Sobrecarga de métodos = Enlace anticipado (también conocido como Enlace estático) A menudo se puede oír hablar de enlace temprano y tardío, también conocido como enlace estático o enlace dinámico. La diferencia entre ellos es muy simple. Temprano es la compilación, tarde es el momento en que se ejecuta el programa. Por lo tanto, la vinculación temprana (vinculación estática) es la determinación de qué método se llamará y quién en el momento de la compilación. Bueno, el enlace tardío (enlace dinámico) es la determinación de qué método llamar directamente en el momento de la ejecución del programa. Como vimos anteriormente (cuando cambiamos IOException a EOFException), si sobrecargamos los métodos de modo que el compilador no pueda entender dónde realizar qué llamada, obtendremos un error en tiempo de compilación: la referencia al método es ambigua. La palabra ambigua traducida del inglés significa ambigua o incierta, imprecisa. Resulta que la sobrecarga es vinculante anticipada, porque la verificación se realiza en tiempo de compilación. Para confirmar nuestras conclusiones, abramos la Especificación del lenguaje Java en el capítulo " 8.4.9. Sobrecarga ":
public class HelloWorld {
public void method(int intNumber) {
System.out.println("intNumber");
}
public void method(Integer intNumber) {
System.out.println("Integer");
}
public void method(String intNumber) {
System.out.println("Number is: " + intNumber);
}
public static void main(String args[]) {
HelloWorld test = new HelloWorld();
test.method(2);
}
}
Guardemos este código en el archivo HelloWorld.java y compilémoslo usando javac HelloWorld.java
Ahora veamos qué escribió nuestro compilador en el código de bytes ejecutando el comando: javap -verbose HelloWorld
.
"invokevirtual #13"
resumiendo
Entonces, descubrimos que Java, como lenguaje orientado a objetos, admite polimorfismo. El polimorfismo puede ser estático (Static Binding) o dinámico (Dynamic Binding). Con el polimorfismo estático, también conocido como enlace temprano, el compilador determina qué método se debe llamar y dónde. Esto permite el uso de un mecanismo como la sobrecarga. Con el polimorfismo dinámico, también conocido como enlace tardío, basado en la firma previamente calculada de un método, un método se calculará en tiempo de ejecución en función del objeto que se utilice (es decir, el método de qué objeto se llama). El funcionamiento de estos mecanismos se puede ver mediante el código de bytes. La sobrecarga analiza las firmas de los métodos y, al resolver la sobrecarga, se elige la opción más específica (más precisa). La anulación analiza el tipo para determinar qué métodos están disponibles y los métodos mismos se llaman en función del objeto. Además de materiales sobre el tema:- ¿Cómo maneja JVM la sobrecarga y anulación de métodos internamente?
- Introducción a la construcción de compiladores en un mundo Java
- Diferencias entre enlace temprano y tardío en Java
- Enlace estático vs dinámico en Java
- Pregunta de OCP: sobrecarga
- Fundamentos del código de bytes de Java: uso de objetos y métodos de llamada
GO TO FULL VERSION