Entendendo o polimorfismo e a interface em Java

5

Estou lendo algumas notas. E não estou entendendo as duas declarações seguintes.

  1. Polimorfismo significa que é sempre a classe do objeto real em tempo de execução que determina qual método será chamado em tempo de execução.

    class A{
        public void m1(){
        System.out.println(" A");
        }
    } 
    
    class B extends A{
        public void m1(){
        System.out.println(" B");
        }
    
        public void m2(){
        System.out.println(" m2");
        }
    } 
    
    B b = new B();
    A a = new B();
    // will print B
    b.m1();
    // will also print B because a refers to an object of class B.
    a.m1();
    
  2. Ao acessar um método ou variável, o compilador só permitirá que você acesse um método ou variável visível através da classe da referência.

    interface Flyer{
        String getName();
    } 
    class Bird implements Flyer{
    ...
        public String getName(){
            return name;
        }
    
        public String getNativeOf(){
        return nativeOf;
        }
    }
    
    Flyer f = new Bird();
    f.getName(); //valid
    f.getNativeOf(); //will not compile because the class of reference f is Flyer and not Bird.
    ((Bird) f).getNativeOf(); //valid because class of the object referred to by f is Bird.
    

Então, é por objeto ou referência real?

    
por fizzix 13.04.2016 / 18:21
fonte

2 respostas

2

Ambos. Você está combinando as noções de disponibilidade de método / campo por tipo e ligação tardia. Eles são separados, embora relacionados.

... actual object at runtime determines which method ...

Eu poderia reafirmar isso como "cujo" método em vez de "qual" método ser mais específico. O nome do método + assinatura que será chamado é determinado em tempo de compilação e fixo, mas a implementação particular (por exemplo, base ou alguma sobreposição) desse método é determinada em tempo de execução. Isso também é chamado de ligação tardia.

(Além do nome + assinatura, o método que será chamado também estará diretamente relacionado ao método da classe que originalmente o introduziu, por exemplo, será o próprio método ou uma substituição em uma subclasse. )

Considere a situação em que temos:

class A {
     void foo ();
}
class B extends A { ... }
class C extends B { ... }
class D extends A { ... }
...
class U {
    void method ( A a ) {
        a.foo ();
    }
}
...
U u = new U();
u.method ( new A () );
u.method ( new B () );
u.method ( new C () );
u.method ( new D () );

Sabemos que method chamará um método existente chamado foo sem parâmetros, que foi introduzido por class A . No entanto, ele pode chamar A , foo , B foo , C foo ou D foo . Aqui, o tempo de execução da linguagem (Máquina virtual) executando method determina para quem foo chamar usando ligação tardia.

O problema de acesso é uma determinação de tempo de compilação feita usando a análise estática e as regras do sistema de tipos (da linguagem específica envolvida). Usando tipos em tempo de compilação, os idiomas podem impedir certos estados de programas ilegais (com um erro de tempo de compilação em vez de um erro de tempo de execução), como por exemplo, não há foo para chamar.

Você também deve notar que existem fundamentalmente vários tipos de conversão. Os upcasts (conversão de B para A ou C para B ou C para A ) são seguros e podem ser validados em tempo de compilação. Downcasts (por exemplo, de A para B ou A para D ) nem sempre são necessariamente analisados em tempo de compilação e, portanto, são projetados na linguagem para executar uma verificação de tempo de execução no ponto de lançamento vá além e faça uso do valor fundido).

Como resultado, você pode obter uma exceção de conversão ilegal, em tempo de execução. Ao ter a linguagem definida para que o compilador e o tempo de execução conspirem para verificar castings ilegais, eles não precisam verificar se o método não foi encontrado no tempo de execução, independentemente da classe verdadeira do objeto: porque ele sabe que o cast funcionou, portanto o método existe (apesar da reflexão; também o versionamento de DLL complica as coisas, mas há uma verificação de tempo de carregamento adicional que complementa a verificação de tempo de compilação e ambos juntos substituem a necessidade de uma verificação em tempo de execução dos métodos existentes ou não).

Embora em seu exemplo específico com Flyer e Bird , uma análise estática poderia ter elidido a verificação de conversão de tempo de execução em ((Bird) f) , em geral isso nem sempre é possível. Seu exemplo continua chamando um método (somente) disponível em Bird .

Observe que, por definição das instruções de código de bytes, essa invocação de método ( getNativeOf() ) só pode ser executada (isto é, só pode ser alcançada pelo fluxo de controle) se a conversão de tempo de execução for bem-sucedida, pois lançar uma exceção alterando assim o fluxo normal de controle e impedindo que a invocação do método seja atingida.

(Além disso, o compilador não gerará o seguinte, um programa de código de byte especialmente criado tentando acessar getNativeOf() de um objeto Flyer sem um encapsulamento de tempo de execução de proteção necessário para Bird falhará em um tempo de carregamento Embora isso ocorra após o tempo de compilação, também é uma forma de análise estática, em outras palavras, só é feita uma vez em tempo de execução (conhecido como tempo de carregamento) em vez de cada vez que o código é usado.

Após o elenco, o compilador irá gerar a invocação do método assumindo que o elenco tenha sucesso (sabendo que se estamos neste ponto, o elenco deve ter sido bem-sucedido) e como tal sabe em tempo de compilação o nome + assinatura (e introdução da classe ) de qual método chamar. O método real resultante chamado ainda usa a ligação tardia no tempo de execução para encontrar cuja implementação ou substituição desse método é apropriada para o tipo verdadeiro do objeto (apesar da otimização, mas qualquer otimização deve fornecer os mesmos resultados como se a ligação tardia fosse usada).

    
por 13.04.2016 / 19:29
fonte
0

O compilador só pode falar sobre objetos em termos de sua interface / contrato. No entanto, o tempo de execução Java sempre usa referências para se referir a objetos em tempo de execução. Observe a diferença entre o ambiente estático do compilador e o ambiente dinâmico do tempo de execução.

por exemplo,

Square square = new Square();
square.setSideLength(2);

Shape shape = square; // assuming Square is a Shape
shape.size() == 4;

para que o compilador não me deixe setSideLength() em uma 'forma', pois setSideLength() é um método no declarado Square .

Mas quando eu chamo size() na referência de forma, qual método a ser chamado é determinado em tempo de execução . por exemplo. imagine isso:

List<Shape> shapes = mixtureOfSquaresAndTrianglesAndCircles;
shapes.get(2).area(); // I can call this regardless of the underlying being a triangle, square, circle etc.
    
por 13.04.2016 / 18:28
fonte