CodeGym /Blogue Java /Random-PT /Polimorfismo Java
John Squirrels
Nível 41
San Francisco

Polimorfismo Java

Publicado no grupo Random-PT
As perguntas relacionadas ao OOP são parte integrante da entrevista técnica para um cargo de desenvolvedor Java em uma empresa de TI. Neste artigo, falaremos sobre um princípio da POO – o polimorfismo. Vamos nos concentrar nos aspectos que costumam ser questionados durante as entrevistas e também daremos alguns exemplos para maior clareza.

O que é polimorfismo em Java?

Polimorfismo é a capacidade de um programa tratar objetos com a mesma interface da mesma maneira, sem informações sobre o tipo específico do objeto. Se você responder a uma pergunta sobre o que é polimorfismo, provavelmente será solicitado a explicar o que quis dizer. Sem desencadear um monte de perguntas adicionais, exponha tudo para o entrevistador mais uma vez. Hora da entrevista: polimorfismo em Java - 1Você pode começar com o fato de que a abordagem OOP envolve a construção de um programa Java baseado na interação entre objetos, que são baseados em classes. Classes são esquemas previamente escritos (templates) usados ​​para criar objetos no programa. Além disso, uma classe sempre possui um tipo específico, que, com um bom estilo de programação, possui um nome que sugere sua finalidade. Além disso, pode-se notar que, como Java é fortemente tipado, o código do programa deve sempre especificar um tipo de objeto quando as variáveis ​​são declaradas. Adicione a isso o fato de que a tipagem estrita melhora a segurança e a confiabilidade do código e permite, mesmo na compilação, evitar erros devido a tipos de incompatibilidade (por exemplo, tentar dividir uma string por um número). Naturalmente, o compilador deve "saber" o tipo declarado – pode ser uma classe do JDK ou uma que nós mesmos criamos. Ressalte ao entrevistador que nosso código pode usar não apenas os objetos do tipo indicado na declaração, mas também seus descendentes.Este é um ponto importante: podemos trabalhar com muitos tipos diferentes como um único tipo (desde que esses tipos sejam derivados de um tipo base). Isso também significa que, se declararmos uma variável cujo tipo é uma superclasse, podemos atribuir uma instância de um de seus descendentes a essa variável. O entrevistador vai gostar se você der um exemplo. Selecione alguma classe que possa ser compartilhada por (uma classe base para) várias classes e faça com que algumas delas a herdem. Classe 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.";
    }
}
Nas subclasses, substitua o método da classe 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!");
    }
}
Um exemplo de polimorfismo e como esses objetos podem ser usados ​​em um 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
        }
    }
}
No método principal , mostre que as linhas

Dancer breakdancer = new Breakdancer("Jay", 19);
Dancer electricBoogieDancer = new ElectricBoogieDancer("Marcia", 20);
declare uma variável de uma superclasse e atribua a ela um objeto que é uma instância de um de seus descendentes. Você provavelmente será questionado por que o compilador não muda com a inconsistência dos tipos declarados nos lados esquerdo e direito do operador de atribuição — afinal, Java é fortemente tipado. Explique que uma conversão de tipo de ampliação está funcionando aqui — uma referência a um objeto é tratada como uma referência à sua classe base. Além do mais, tendo encontrado tal construção no código, o compilador executa a conversão automática e implicitamente. O código de exemplo mostra que o tipo declarado no lado esquerdo do operador de atribuição ( Dancer ) possui vários formulários (tipos), que são declarados no lado direito ( Breakdancer , ElectricBoogieDancer). Cada formulário pode ter seu próprio comportamento exclusivo com relação à funcionalidade geral definida na superclasse (o método de dança ). Ou seja, um método declarado em uma superclasse pode ser implementado de forma diferente em seus descendentes. Neste caso, estamos lidando com a sobreposição de métodos, que é exatamente o que cria múltiplos formulários (comportamentos). Isso pode ser visto executando o código no método principal: Saída do programa: Sou Fred. Eu tenho 18 anos. Eu danço como todo mundo. Eu sou Jay. Tenho 19 anos. Eu danço break! Eu sou a Márcia. Eu tenho 20 anos. Eu danço o boogie elétrico! Se não substituirmos o método nas subclasses, não obteremos um comportamento diferente. Por exemplo,aulas ElectricBoogieDancer , então a saída do programa será esta: I'm Fred. Eu tenho 18 anos. Eu danço como todo mundo. Eu sou Jay. Tenho 19 anos. Eu danço como todo mundo. Eu sou a Márcia. Eu tenho 20 anos. Eu danço como todo mundo. E isso significa que simplesmente não faz sentido criar as classes Breakdancer e ElectricBoogieDancer . Onde especificamente o princípio do polimorfismo se manifesta? Onde um objeto é usado no programa sem conhecimento de seu tipo específico? Em nosso exemplo, isso acontece quando o método dance() é chamado no objeto Dancer d . Em Java, polimorfismo significa que o programa não precisa saber se o objeto é umBreakdancer ou ElectricBoogieDancer . O importante é que é descendente da classe Dancer . E se você mencionar descendentes, observe que a herança em Java não é apenas extends , mas também implementa. Agora é a hora de mencionar que Java não suporta herança múltipla — cada tipo pode ter um pai (superclasse) e um número ilimitado de descendentes (subclasses). Da mesma forma, as interfaces são usadas para adicionar vários conjuntos de funções às classes. Em comparação com as subclasses (herança), as interfaces são menos acopladas à classe pai. Eles são usados ​​muito amplamente. Em Java, uma interface é um tipo de referência, portanto o programa pode declarar uma variável do tipo interface. Agora é hora de dar um exemplo. Crie uma interface:

public interface CanSwim {
    void swim();
}
Para maior clareza, vamos pegar várias classes não relacionadas e fazê-las implementar a interface:

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();
        }
    }
}
Os resultados da chamada de um método polimórfico definido em uma interface nos mostram as diferenças de comportamento dos tipos que implementam essa interface. No nosso caso, essas são as diferentes strings exibidas pelo método swim . Depois de estudar nosso exemplo, o entrevistador pode perguntar por que executar esse código no método main

for (Swim s : swimmers) {
            s.swim();        
}
faz com que os métodos de substituição definidos em nossas subclasses sejam chamados? Como a implementação desejada do método é selecionada durante a execução do programa? Para responder a essas perguntas, você precisa explicar a vinculação tardia (dinâmica). Binding significa estabelecer um mapeamento entre uma chamada de método e sua implementação de classe específica. Em essência, o código determina qual dos três métodos definidos nas classes será executado. Java usa ligação tardia por padrão, ou seja, a ligação ocorre em tempo de execução e não em tempo de compilação, como é o caso da ligação antecipada. Isso significa que quando o compilador compila esse código

for (Swim s : swimmers) {
            s.swim();        
}
ele não sabe qual classe ( Human , Fish ou Uboat ) possui o código que será executado quando o nadométodo é chamado. Isso é determinado somente quando o programa é executado, graças ao mecanismo de vinculação dinâmica (verificando o tipo de um objeto em tempo de execução e selecionando a implementação correta para esse tipo). Se você for perguntado como isso é implementado, você pode responder que ao carregar e inicializar objetos, a JVM constrói tabelas na memória e vincula variáveis ​​com seus valores e objetos com seus métodos. Ao fazer isso, se uma classe é herdada ou implementa uma interface, a primeira ordem do dia é verificar a presença de métodos substituídos. Se houver, eles estão vinculados a esse tipo. Caso contrário, a busca por um método correspondente move-se para a classe que está um passo acima (o pai) e assim por diante até a raiz em uma hierarquia multinível. Quando se trata de polimorfismo em OOP e sua implementação em código, observamos que é uma boa prática usar classes e interfaces abstratas para fornecer definições abstratas de classes base. Essa prática decorre do princípio da abstração — identificar comportamentos e propriedades comuns e colocá-los em uma classe abstrata, ou identificar apenas comportamentos comuns e colocá-los em uma interface. Projetar e criar uma hierarquia de objetos com base em interfaces e herança de classes são necessários para implementar o polimorfismo. Em relação ao polimorfismo e inovações em Java, notamos que a partir do Java 8, ao criar classes abstratas e interfaces é possível utilizar o ou identificar apenas comportamentos comuns e colocá-los em uma interface. Projetar e criar uma hierarquia de objetos com base em interfaces e herança de classes são necessários para implementar o polimorfismo. Em relação ao polimorfismo e inovações em Java, notamos que a partir do Java 8, ao criar classes abstratas e interfaces é possível utilizar o ou identificar apenas comportamentos comuns e colocá-los em uma interface. Projetar e criar uma hierarquia de objetos com base em interfaces e herança de classes são necessários para implementar o polimorfismo. Em relação ao polimorfismo e inovações em Java, notamos que a partir do Java 8, ao criar classes abstratas e interfaces é possível utilizar opalavra-chave default para escrever uma implementação padrão para métodos abstratos em classes base. Por exemplo:

public interface CanSwim {
    default void swim() {
        System.out.println("I just swim");
    }
}
Às vezes, os entrevistadores perguntam como os métodos nas classes base devem ser declarados para que o princípio do polimorfismo não seja violado. A resposta é simples: esses métodos não devem ser static , private nem final . Private torna um método disponível apenas dentro de uma classe, então você não poderá substituí-lo em uma subclasse. Static associa um método com a classe em vez de qualquer objeto, então o método da superclasse sempre será chamado. E final torna um método imutável e oculto das subclasses.

O que o polimorfismo nos dá?

Você também provavelmente será questionado sobre como o polimorfismo nos beneficia. Você pode responder brevemente sem se prender a detalhes complicados:
  1. Isso torna possível substituir implementações de classe. O teste é construído sobre ele.
  2. Facilita a extensibilidade, tornando muito mais fácil criar uma base que pode ser construída no futuro. Adicionar novos tipos com base nos existentes é a maneira mais comum de expandir a funcionalidade dos programas OOP.
  3. Ele permite combinar objetos que compartilham um tipo ou comportamento comum em uma coleção ou array e manipulá-los uniformemente (como em nossos exemplos, onde forçamos todos a dançar () ou nadar () :)
  4. Flexibilidade na criação de novos tipos: você pode optar pela implementação do pai de um método ou substituí-lo em uma subclasse.

Algumas palavras de despedida

O polimorfismo é um tópico muito importante e extenso. É o assunto de quase metade deste artigo sobre OOP em Java e forma uma boa parte da base da linguagem. Você não poderá evitar a definição desse princípio em uma entrevista. Se você não souber ou não entender, provavelmente a entrevista chegará ao fim. Portanto, não seja preguiçoso - avalie seu conhecimento antes da entrevista e atualize-o, se necessário.

Mais leitura:

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION