Las preguntas relacionadas con la programación orientada a objetos son una parte integral de la entrevista técnica para un puesto de desarrollador de Java en una empresa de TI. En este artículo, hablaremos sobre un principio de OOP: el polimorfismo. Nos centraremos en los aspectos que a menudo se preguntan durante las entrevistas y también daremos algunos ejemplos para mayor claridad.

¿Qué es el polimorfismo en Java?

El polimorfismo es la capacidad de un programa para tratar objetos con la misma interfaz de la misma manera, sin información sobre el tipo específico del objeto. Si responde una pregunta sobre qué es el polimorfismo, lo más probable es que se le pida que explique lo que quiso decir. Sin desencadenar un montón de preguntas adicionales, expóngalo todo al entrevistador una vez más. Tiempo de entrevista: polimorfismo en Java - 1Puede comenzar con el hecho de que el enfoque OOP implica construir un programa Java basado en la interacción entre objetos, que se basan en clases. Las clases son planos (plantillas) previamente escritos que se utilizan para crear objetos en el programa. Además, una clase siempre tiene un tipo específico que, con un buen estilo de programación, tiene un nombre que sugiere su propósito. Además, se puede observar que dado que Java está fuertemente tipado, el código del programa siempre debe especificar un tipo de objeto cuando se declaran las variables. Añádase a esto el hecho de que la tipificación estricta mejora la seguridad y la fiabilidad del código y permite, incluso en la compilación, evitar errores debido a tipos de incompatibilidad (por ejemplo, intentar dividir una cadena por un número). Naturalmente, el compilador debe "saber" el tipo declarado: puede ser una clase del JDK o una que creamos nosotros mismos. Señale al entrevistador que nuestro código puede usar no solo los objetos del tipo indicado en la declaración sino también sus descendientes.Este es un punto importante: podemos trabajar con muchos tipos diferentes como un solo tipo (siempre que estos tipos se deriven de un tipo base). Esto también significa que si declaramos una variable cuyo tipo es una superclase, podemos asignar una instancia de uno de sus descendientes a esa variable. Al entrevistador le gustará que le des un ejemplo. Seleccione alguna clase que pueda ser compartida por (una clase base para) varias clases y haga que un par de ellas la hereden. Clase base:

public class Dancer {
    private String name;
    private int age;

    public Dancer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void dance() {
        System.out.println(toString() + " I dance like everyone else.");
    }

    @Override
    public String toString() {
        Return "I'm " + name + ". I'm " + age + " years old.";
    }
}
En las subclases, invalide el método de la clase base:

public class ElectricBoogieDancer extends Dancer {
    public ElectricBoogieDancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString () + " I dance the electric boogie!");
    }
}

public class Breakdancer extends Dancer {

    public Breakdancer(String name, int age) {
        super(name, age);
    }
// Override the method of the base class
    @Override
    public void dance() {
        System.out.println(toString() + " I breakdance!");
    }
}
Un ejemplo de polimorfismo y cómo se pueden usar estos objetos en un programa:

public class Main {

    public static void main(String[] args) {
        Dancer dancer = new Dancer("Fred", 18);

        Dancer breakdancer = new Breakdancer("Jay", 19); // Widening conversion to the base type 
        Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20); // Widening conversion to the base type

        List<dancer> disco = Arrays.asList(dancer, breakdancer, electricBoogieDancer);
        for (Dancer d : disco) {
            d.dance(); // Call the polymorphic method
        }
    }
}
En el método principal , demuestre que las rectas

Dancer breakdancer = new Breakdancer("Jay", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20);
declarar una variable de una superclase y asignarle un objeto que sea una instancia de uno de sus descendientes. Lo más probable es que le pregunten por qué el compilador no falla ante la inconsistencia de los tipos declarados en los lados izquierdo y derecho del operador de asignación; después de todo, Java está fuertemente tipado. Explique que aquí funciona una conversión de tipo de ampliación: una referencia a un objeto se trata como una referencia a su clase base. Además, al encontrar una construcción de este tipo en el código, el compilador realiza la conversión automática e implícitamente. El código de muestra muestra que el tipo declarado en el lado izquierdo del operador de asignación ( Dancer ) tiene varias formas (tipos), que se declaran en el lado derecho ( Breakdancer , ElectricBoogieDancer). Cada formulario puede tener su propio comportamiento único con respecto a la funcionalidad general definida en la superclase (el método de baile ). Es decir, un método declarado en una superclase puede implementarse de manera diferente en sus descendientes. En este caso, estamos tratando con la anulación de métodos, que es exactamente lo que crea múltiples formas (comportamientos). Esto se puede ver ejecutando el código en el método principal: Salida del programa: Soy Fred. Tengo 18 años. Yo bailo como todos los demás. soy jay Tengo 19 años. ¡Hago breakdance! soy marcia Tengo 20 años. ¡Yo bailo el boogie eléctrico! Si no anulamos el método en las subclases, no obtendremos un comportamiento diferente. Por ejemplo,Clases de ElectricBoogieDancer , entonces el resultado del programa será este: Soy Fred. Tengo 18 años. Yo bailo como todos los demás. soy jay Tengo 19 años. Yo bailo como todos los demás. soy marcia Tengo 20 años. Yo bailo como todos los demás. Y esto significa que simplemente no tiene sentido crear las clases Breakdancer y ElectricBoogieDancer . ¿Dónde se manifiesta específicamente el principio del polimorfismo? ¿Dónde se usa un objeto en el programa sin conocimiento de su tipo específico? En nuestro ejemplo, sucede cuando se llama al método dance() en el objeto Dancer d . En Java, el polimorfismo significa que el programa no necesita saber si el objeto es unBreakdancer o ElectricBoogieDancer . Lo importante es que es descendiente de la clase Dancer . Y si menciona descendientes, debe tener en cuenta que la herencia en Java no solo se extiende , sino que también implementa. Ahora es el momento de mencionar que Java no admite la herencia múltiple: cada tipo puede tener un padre (superclase) y un número ilimitado de descendientes (subclases). En consecuencia, las interfaces se utilizan para agregar varios conjuntos de funciones a las clases. En comparación con las subclases (herencia), las interfaces están menos acopladas con la clase principal. Se utilizan muy ampliamente. En Java, una interfaz es un tipo de referencia, por lo que el programa puede declarar una variable del tipo de interfaz. Ahora es el momento de dar un ejemplo. Crear una interfaz:

public interface CanSwim {
    void swim();
}
Para mayor claridad, tomaremos varias clases no relacionadas y haremos que implementen la interfaz:

public class Human implements CanSwim {
    private String name;
    private int age;

    public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void swim() {
        System.out.println(toString()+" I swim with an inflated tube.");
    }

    @Override
    public String toString() {
        return "I'm " + name + ". I'm " + age + " years old.";
    }

}
 
public class Fish implements CanSwim {
    private String name;

    public Fish(String name) {
        this.name = name;
    }

    @Override
    public void swim() {
        System.out.println("I'm a fish. My name is " + name + ". I swim by moving my fins.");

    }

public class UBoat implements CanSwim {

    private int speed;

    public UBoat(int speed) {
        this.speed = speed;
    }

    @Override
    public void swim() {
        System.out.println("I'm a submarine that swims through the water by rotating screw propellers. My speed is " + speed + " knots.");
    }
}
método principal :

public class Main {

    public static void main(String[] args) {
        CanSwim human = new Human("John", 6);
        CanSwim fish = new Fish("Whale");
        CanSwim boat = new UBoat(25);

        List<swim> swimmers = Arrays.asList(human, fish, boat);
        for (Swim s : swimmers) {
            s.swim();
        }
    }
}
Los resultados llamando a un método polimórfico definido en una interfaz nos muestran las diferencias en el comportamiento de los tipos que implementan esta interfaz. En nuestro caso, estas son las diferentes cadenas que muestra el método de natación . Después de estudiar nuestro ejemplo, el entrevistador puede preguntar por qué ejecutar este código en el método principal

for (Swim s : swimmers) {
            s.swim();        
}
hace que se llame a los métodos anulados definidos en nuestras subclases? ¿Cómo se selecciona la implementación deseada del método mientras se ejecuta el programa? Para responder a estas preguntas, debe explicar el enlace tardío (dinámico). Vincular significa establecer un mapeo entre una llamada de método y su implementación de clase específica. En esencia, el código determina cuál de los tres métodos definidos en las clases se ejecutará. Java usa el enlace tardío por defecto, es decir, el enlace ocurre en tiempo de ejecución y no en tiempo de compilación como es el caso del enlace temprano. Esto significa que cuando el compilador compila este código

for (Swim s : swimmers) {
            s.swim();        
}
no sabe qué clase ( humano , pez o submarino ) tiene el código que se ejecutará cuando nade .se llama el método. Esto se determina solo cuando se ejecuta el programa, gracias al mecanismo de enlace dinámico (comprobando el tipo de un objeto en tiempo de ejecución y seleccionando la implementación correcta para este tipo). Si te preguntan cómo se implementa esto, puedes responder que al cargar e inicializar objetos, la JVM construye tablas en memoria y vincula variables con sus valores y objetos con sus métodos. Al hacerlo, si una clase se hereda o implementa una interfaz, la primera orden del día es verificar la presencia de métodos anulados. Si los hay, están vinculados a este tipo. De lo contrario, la búsqueda de un método coincidente se mueve a la clase que está un paso más arriba (el padre) y así sucesivamente hasta la raíz en una jerarquía de varios niveles. Cuando se trata de polimorfismo en programación orientada a objetos y su implementación en el código, observamos que es una buena práctica usar clases e interfaces abstractas para proporcionar definiciones abstractas de clases base. Esta práctica se deriva del principio de abstracción: identificar el comportamiento y las propiedades comunes y colocarlos en una clase abstracta, o identificar solo el comportamiento común y colocarlo en una interfaz. Se requiere diseñar y crear una jerarquía de objetos basada en interfaces y herencia de clases para implementar el polimorfismo. Con respecto al polimorfismo y las innovaciones en Java, notamos que a partir de Java 8, al crear clases abstractas e interfaces es posible utilizar el o identificando solo el comportamiento común y poniéndolo en una interfaz. Se requiere diseñar y crear una jerarquía de objetos basada en interfaces y herencia de clases para implementar el polimorfismo. Con respecto al polimorfismo y las innovaciones en Java, notamos que a partir de Java 8, al crear clases abstractas e interfaces es posible utilizar el o identificando solo el comportamiento común y poniéndolo en una interfaz. Se requiere diseñar y crear una jerarquía de objetos basada en interfaces y herencia de clases para implementar el polimorfismo. Con respecto al polimorfismo y las innovaciones en Java, notamos que a partir de Java 8, al crear clases abstractas e interfaces es posible utilizar elpalabra clave predeterminada para escribir una implementación predeterminada para métodos abstractos en clases base. Por ejemplo:

public interface CanSwim {
    default void swim() {
        System.out.println("I just swim");
    }
}
A veces, los entrevistadores preguntan cómo se deben declarar los métodos en las clases base para que no se viole el principio del polimorfismo. La respuesta es simple: estos métodos no deben ser estáticos , privados ni definitivos . Private hace que un método esté disponible solo dentro de una clase, por lo que no podrá anularlo en una subclase. Static asocia un método con la clase en lugar de cualquier objeto, por lo que siempre se llamará al método de la superclase. Y final hace que un método sea inmutable y oculto de las subclases.

¿Qué nos da el polimorfismo?

También es probable que te pregunten cómo nos beneficia el polimorfismo. Puede responder esto brevemente sin perderse en los detalles peludos:
  1. Hace posible reemplazar implementaciones de clase. Las pruebas se basan en ello.
  2. Facilita la extensibilidad, lo que hace que sea mucho más fácil crear una base sobre la que se pueda construir en el futuro. Agregar nuevos tipos basados ​​en los existentes es la forma más común de expandir la funcionalidad de los programas OOP.
  3. Le permite combinar objetos que comparten un tipo o comportamiento común en una colección o matriz y manejarlos de manera uniforme (como en nuestros ejemplos, donde obligamos a todos a bailar() o nadar() :)
  4. Flexibilidad en la creación de nuevos tipos: puede optar por la implementación de un método del padre o anularlo en una subclase.

Algunas palabras de despedida

El polimorfismo es un tema muy importante y extenso. Es el tema de casi la mitad de este artículo sobre programación orientada a objetos en Java y forma una buena parte de la base del lenguaje. No podrás evitar definir este principio en una entrevista. Si no lo sabe o no lo entiende, la entrevista probablemente llegará a su fin. Así que no seas holgazán: evalúa tus conocimientos antes de la entrevista y actualízalos si es necesario.