JavaRush /Blogue Java /Random-PT /Polimorfismo e seus amigos
Viacheslav
Nível 3

Polimorfismo e seus amigos

Publicado no grupo Random-PT
O polimorfismo é um dos princípios básicos da programação orientada a objetos. Ele permite que você aproveite o poder da digitação forte do Java e escreva código utilizável e de fácil manutenção. Muito se tem falado sobre ele, mas espero que todos possam tirar algo novo desta análise.
Polimorfismo e seus amigos - 1

Introdução

Acho que todos sabemos que a linguagem de programação Java pertence à Oracle. Portanto, nosso caminho começa pelo site: www.oracle.com . Existe um "Menu" na página principal. Nele, na seção “Documentação” existe uma subseção “Java”. Tudo o que se relaciona com as funções básicas da linguagem pertence à "documentação Java SE", por isso selecionamos esta seção. A seção de documentação será aberta para a versão mais recente, mas por enquanto a seção "Procurando uma versão diferente?" Vamos escolher a opção: JDK8. Na página veremos muitas opções diferentes. Mas estamos interessados ​​em Aprenda a Linguagem: " Caminhos de Aprendizagem de Tutoriais Java ". Nesta página encontraremos outra seção: “ Aprendendo a Linguagem Java ”. Este é o mais sagrado dos santos, um tutorial sobre noções básicas de Java da Oracle. Java é uma linguagem de programação orientada a objetos (OOP), portanto, aprender a linguagem mesmo no site da Oracle começa com uma discussão dos conceitos básicos de “ Conceitos de Programação Orientada a Objetos ”. Pelo próprio nome fica claro que Java está focado em trabalhar com objetos. Na subseção “ O que é um objeto? ”, fica claro que os objetos em Java consistem em estado e comportamento. Imagine que temos uma conta bancária. A quantidade de dinheiro na conta é um estado, e os métodos de trabalhar com esse estado são comportamentos. Os objetos precisam ser descritos de alguma forma (diga qual estado e comportamento eles podem ter) e essa descrição é a classe . Quando criamos um objeto de alguma classe, especificamos esta classe e isso é chamado de “ tipo de objeto ”. Portanto, diz-se que Java é uma linguagem fortemente tipada, conforme indicado na especificação da linguagem Java na seção " Capítulo 4. Tipos, Valores e Variáveis ". A linguagem Java segue conceitos OOP e suporta herança usando a palavra-chave extends. Por que expansão? Porque com a herança, uma classe filha herda o comportamento e o estado da classe pai e pode complementá-los, ou seja, estender a funcionalidade da classe base. Uma interface também pode ser especificada na descrição da classe usando a palavra-chave implements. Quando uma classe implementa uma interface, significa que a classe está em conformidade com algum contrato – uma declaração do programador ao resto do ambiente de que a classe tem um determinado comportamento. Por exemplo, o player possui vários botões. Esses botões são uma interface para controlar o comportamento do player, e o comportamento mudará o estado interno do player (por exemplo, volume). Nesse caso, o estado e o comportamento como descrição darão uma classe. Se uma classe implementa uma interface, então um objeto criado por esta classe pode ser descrito por um tipo não apenas pela classe, mas também pela interface. Vejamos um exemplo:
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
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
O tipo é uma descrição muito importante. Diz como vamos trabalhar com o objeto, ou seja, qual comportamento esperamos do objeto. Comportamentos são métodos. Portanto, vamos entender os métodos. No site da Oracle, os métodos possuem uma seção própria no Tutorial Oracle: " Definindo Métodos ". A primeira coisa a retirar do artigo: Uma assinatura de método é o nome do método e os tipos de parâmetros :
Polimorfismo e seus amigos - 2
Por exemplo, ao declarar um método método public void(Object o), a assinatura será o nome do método e o tipo do parâmetro Object. O tipo de retorno NÃO está incluído na assinatura. É importante! A seguir, vamos compilar nosso código-fonte. Como sabemos, para isso o código deve ser salvo em um arquivo com o nome da classe e a extensão java. O código Java é compilado usando o compilador “ javac ” em algum formato intermediário que pode ser executado pela Java Virtual Machine (JVM). Esse formato intermediário é denominado bytecode e está contido em arquivos com extensão .class. Vamos executar o comando para compilar: javac MusicPlayer.java Depois que o código java for compilado, podemos executá-lo. Usando o utilitário " java " para iniciar, o processo da máquina virtual java será iniciado para executar o bytecode passado no arquivo de classe. Vamos executar o comando para iniciar o aplicativo: java MusicPlayer. Veremos na tela o texto especificado no parâmetro de entrada do método println. Curiosamente, tendo o bytecode em um arquivo com extensão .class, podemos visualizá-lo utilizando o utilitário " javap ". Vamos executar o comando <ocde>javap -c MusicPlayer:
Polimorfismo e seus amigos - 3
A partir do bytecode podemos ver que a chamada de um método através de um objeto cujo tipo a classe foi especificada é realizada usando invokevirtual, e o compilador calculou qual assinatura de método deve ser usada. Por que invokevirtual? Porque existe uma chamada (invoke é traduzido como chamada) de um método virtual. O que é um método virtual? Este é um método cujo corpo pode ser substituído durante a execução do programa. Simplesmente imagine que você tem uma lista de correspondência entre uma determinada chave (assinatura do método) e o corpo (código) do método. E esta correspondência entre a chave e o corpo do método pode mudar durante a execução do programa. Portanto o método é virtual. Por padrão, em Java, os métodos que NÃO são estáticos, NÃO finais e NÃO privados são virtuais. Graças a isso, Java suporta o princípio do polimorfismo da programação orientada a objetos. Como você já deve ter entendido, é disso que trata nossa análise de hoje.

Polimorfismo

No site da Oracle, em seu Tutorial oficial, há uma seção separada: " Polimorfismo ". Vamos usar o Java Online Compiler para ver como funciona o polimorfismo em Java. Por exemplo, temos alguma classe abstrata Number que representa um número em Java. O que isso permite? Ele possui algumas técnicas básicas que todos os herdeiros terão. Aquele que herda de Número diz literalmente - “Eu sou um número, você pode trabalhar comigo como um número”. Por exemplo, para qualquer sucessor você pode usar o método intValue() para obter seu valor inteiro. Se você olhar a API Java para Number, verá que o método é abstrato, ou seja, cada sucessor de Number deve implementar esse método sozinho. Mas o que isso nos dá? Vejamos um exemplo:
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 pode ser visto no exemplo, graças ao polimorfismo, podemos escrever um método que aceitará argumentos de qualquer tipo como entrada, que será descendente de Number (não podemos obter Number, porque é uma classe abstrata). Assim como no exemplo do jogador, neste caso estamos dizendo que queremos trabalhar com algo, como Número. Sabemos que qualquer pessoa que seja um Número deve ser capaz de fornecer seu valor inteiro. E isso é o suficiente para nós. Não queremos entrar em detalhes da implementação de um objeto específico e queremos trabalhar com esse objeto através de métodos comuns a todos os descendentes de Number. A lista de métodos que estarão disponíveis para nós será determinada pelo tipo em tempo de compilação (como vimos anteriormente no bytecode). Neste caso, nosso tipo será Número. Como você pode ver no exemplo, estamos passando diferentes números de diferentes tipos, ou seja, o método sum receberá Integer, Long e Double como entrada. Mas o que todos eles têm em comum é que são descendentes do número abstrato e, portanto, substituem seu comportamento no método intValue, porque cada tipo específico sabe como converter esse tipo em Inteiro. Tal polimorfismo é implementado através do chamado overriding, em inglês Overriding.
Polimorfismo e seus amigos – 4
Polimorfismo de substituição ou dinâmico. Então, vamos começar salvando o arquivo HelloWorld.java com o seguinte conteúdo:
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();
    }
}
Vamos fazer javac HelloWorld.javae javap -c HelloWorld:
Polimorfismo e seus amigos – 5
Como você pode ver, no bytecode das linhas com chamada de método, é indicada a mesma referência ao método de chamada invokevirtual (#6). Vamos fazê-lo java HelloWorld. Como podemos ver, as variáveis ​​parent e child são declaradas com o tipo Parent, mas a implementação em si é chamada de acordo com qual objeto foi atribuído à variável (ou seja, que tipo de objeto). Durante a execução do programa (também dizem em tempo de execução), a JVM, dependendo do objeto, ao chamar métodos usando a mesma assinatura, executava métodos diferentes. Ou seja, usando a chave da assinatura correspondente, primeiro recebemos um corpo de método e depois recebemos outro. Dependendo de qual objeto está na variável. Essa determinação no momento da execução do programa de qual método será chamado também é chamada de late binding ou Dynamic Binding. Ou seja, a correspondência entre a assinatura e o corpo do método é realizada de forma dinâmica, dependendo do objeto no qual o método é chamado. Naturalmente, você não pode substituir membros estáticos de uma classe (membro de classe), bem como membros de classe com tipo de acesso privado ou final. As anotações @Override também ajudam os desenvolvedores. Isso ajuda o compilador a entender que neste ponto iremos substituir o comportamento de um método ancestral. Se cometermos um erro na assinatura do método, o compilador nos informará imediatamente. Por exemplo:
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");
        }
}
Não compila com o erro: erro: o método não substitui ou implementa um método de um supertipo
Polimorfismo e seus amigos – 6
A redefinição também está associada ao conceito de “ covariância ”. Vejamos um exemplo:
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());
    }
}
Apesar da aparente abstrusão, o significado se resume ao fato de que, ao substituir, podemos retornar não apenas o tipo que foi especificado no ancestral, mas também um tipo mais específico. Por exemplo, o ancestral retornou Número, e podemos retornar Inteiro - o descendente de Número. O mesmo se aplica às exceções declaradas nos lançamentos do método. Os herdeiros podem substituir o método e refinar a exceção lançada. Mas eles não podem expandir. Ou seja, se o pai lançar uma IOException, então poderemos lançar a EOFException mais precisa, mas não podemos lançar uma Exceção. Da mesma forma, não é possível restringir o âmbito nem impor restrições adicionais. Por exemplo, você não pode adicionar estática.
Polimorfismo e seus amigos – 7

Escondido

Também existe algo chamado “ ocultação ”. Exemplo:
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();
    }
}
Isso é uma coisa bastante óbvia se você pensar bem. Membros estáticos de uma classe pertencem à classe, ou seja, ao tipo da variável. Portanto, é lógico que se child for do tipo Parent, então o método será chamado em Parent, e não em child. Se olharmos para o bytecode, como fizemos anteriormente, veremos que o método estático é chamado usando Invokestatic. Isso explica para a JVM que ela precisa olhar para o tipo, e não para a tabela de métodos, como fizeram Invokevirtual ou InvokeInterface.
Polimorfismo e seus amigos – 8

Métodos de sobrecarga

O que mais vemos no Tutorial Java Oracle? Na seção anteriormente estudada " Definindo Métodos " há algo sobre Sobrecarga. O que é isso? Em russo, isso é “sobrecarga de método” e tais métodos são chamados de “sobrecarregados”. Portanto, sobrecarga de método. À primeira vista, tudo é simples. Vamos abrir um compilador Java online, por exemplo tutorialspoint online java compiler .
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);
	}
}
Então, tudo parece simples aqui. Conforme declarado no tutorial do Oracle, os métodos sobrecarregados (neste caso, o método say) diferem no número e no tipo de argumentos passados ​​ao método. Você não pode declarar o mesmo nome e o mesmo número de tipos idênticos de argumentos, porque o compilador não será capaz de distingui-los um do outro. Vale a pena notar uma coisa muito importante desde já:
Polimorfismo e seus amigos – 9
Ou seja, ao sobrecarregar, o compilador verifica a correção. É importante. Mas como o compilador realmente determina que um determinado método precisa ser chamado? Ele usa a regra "o método mais específico" descrita na especificação da linguagem Java: " 15.12.2.5. Escolhendo o método mais específico ". Para demonstrar como funciona, vamos dar um exemplo do Oracle Certified Professional Java Programmer:
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);
  }
}
Veja um exemplo aqui: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... Como você pode ver, estamos passando nulo para o método. O compilador tenta determinar o tipo mais específico. O objeto não é adequado porque tudo é herdado dele. Vá em frente. Existem 2 classes de exceções. Vejamos java.io.IOException e vejamos se há uma FileNotFoundException em "Subclasses diretas conhecidas". Ou seja, FileNotFoundException é o tipo mais específico. Portanto, o resultado será a saída da string “FileNotFoundException”. Mas se substituirmos IOException por EOFException, descobrimos que temos dois métodos no mesmo nível da hierarquia na árvore de tipos, ou seja, para ambos, IOException é o pai. O compilador não poderá escolher qual método chamar e gerará um erro de compilação: reference to method is ambiguous. Mais um exemplo:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
A saída será 1. Não há perguntas aqui. O tipo int... é um vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html e nada mais é do que "açúcar sintático" e na verdade é um int. .. array pode ser lido como array int[]. Se agora adicionarmos um método:
public static void method(long a, long b) {
	System.out.println("2");
}
Então ele exibirá não 1, mas 2, porque estamos passando 2 números e 2 argumentos correspondem melhor do que um array. Se adicionarmos um método:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
Então ainda veremos 2. Porque neste caso as primitivas são uma correspondência mais exata do que o boxe em Inteiro. Porém, se executarmos, method(new Integer(1), new Integer(2));ele imprimirá 3. Construtores em Java são semelhantes a métodos e, como também podem ser usados ​​para obter uma assinatura, as mesmas regras de “resolução de sobrecarga” se aplicam a eles como métodos sobrecarregados. A especificação da linguagem Java nos diz isso em " 8.8.8. Sobrecarga do Construtor ". Sobrecarga de método = vinculação inicial (também conhecida como vinculação estática) Muitas vezes você pode ouvir sobre vinculação inicial e tardia, também conhecida como vinculação estática ou vinculação dinâmica. A diferença entre eles é muito simples. Cedo é a compilação, tarde é o momento em que o programa é executado. Portanto, ligação antecipada (ligação estática) é a determinação de qual método será chamado e quem no momento da compilação. Bem, ligação tardia (ligação dinâmica) é a determinação de qual método chamar diretamente no momento da execução do programa. Como vimos anteriormente (quando alteramos IOException para EOFException), se sobrecarregarmos os métodos de forma que o compilador não consiga entender onde fazer qual chamada, obteremos um erro em tempo de compilação: a referência ao método é ambígua. A palavra ambíguo traduzida do inglês significa ambíguo ou incerto, impreciso. Acontece que a sobrecarga é vinculativa precoce, porque a verificação é executada em tempo de compilação. Para confirmar nossas conclusões, vamos abrir a Especificação da Linguagem Java no capítulo “ 8.4.9. Sobrecarga ”:
Polimorfismo e seus amigos – 10
Acontece que durante a compilação, informações sobre os tipos e número de argumentos (que estão disponíveis no momento da compilação) serão utilizadas para determinar a assinatura do método. Se o método for um dos métodos do objeto (ou seja, método de instância), a chamada de método real será determinada em tempo de execução usando pesquisa de método dinâmico (ou seja, ligação dinâmica). Para deixar mais claro, vamos dar um exemplo semelhante ao discutido anteriormente:
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);
    }
}
Vamos salvar esse código no arquivo HelloWorld.java e compilá-lo usando javac HelloWorld.java Agora vamos ver o que nosso compilador escreveu no bytecode executando o comando: javap -verbose HelloWorld.
Polimorfismo e seus amigos – 11
Conforme declarado, o compilador determinou que algum método virtual será chamado no futuro. Ou seja, o corpo do método será definido em tempo de execução. Mas na hora da compilação, dos três métodos, o compilador escolheu o mais adequado, então indicou o número:"invokevirtual #13"
Polimorfismo e seus amigos – 12
Que tipo de método é esse? Este é um link para o método. Grosso modo, esta é uma pista pela qual, em tempo de execução, a Java Virtual Machine pode realmente determinar qual método procurar para executar. Mais detalhes podem ser encontrados no super artigo: " Como a JVM trata sobrecarga e substituição de métodos internamente ".

Resumindo

Então, descobrimos que Java, como linguagem orientada a objetos, suporta polimorfismo. O polimorfismo pode ser estático (Static Binding) ou dinâmico (Dynamic Binding). Com o polimorfismo estático, também conhecido como ligação antecipada, o compilador determina qual método deve ser chamado e onde. Isso permite o uso de um mecanismo como sobrecarga. Com o polimorfismo dinâmico, também conhecido como ligação tardia, com base na assinatura previamente calculada de um método, um método será calculado em tempo de execução com base em qual objeto é usado (ou seja, qual método do objeto é chamado). O funcionamento desses mecanismos pode ser visto usando bytecode. A sobrecarga analisa as assinaturas do método e, ao resolvê-la, a opção mais específica (mais precisa) é escolhida. A substituição analisa o tipo para determinar quais métodos estão disponíveis, e os próprios métodos são chamados com base no objeto. Bem como materiais sobre o tema: #Viacheslav
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION