Herança e acesso dinâmico a membros / atributos e métodos em linguagens semelhantes a Java

7

Eu tenho uma pergunta sobre herança em linguagens de programação OO do tipo Java. Ele surgiu na minha classe de compilador, quando expliquei como compilar métodos e sua invocação. Eu estava usando Java como exemplo de linguagem de origem para compilar.

Agora considere este programa Java.

class A {
  public int x = 0;
  void f () { System.out.println ( "A:f" ); } }

class B extends A {
  public int x = 1;
  void f () { System.out.println ( "B:f" ); } }

public class Main {
  public static void main ( String [] args ) {
    A a = new A ();
    B b = new B ();
    A ab = new B ();

    a.f();
    b.f();
    ab.f();
    System.out.println ( a.x );
    System.out.println ( b.x );
    System.out.println ( ab.x ); } }

Quando você o executa, obtém o seguinte resultado.

A:f
B:f
B:f
0
1 
0

Os casos interessantes são aqueles que acontecem com o objeto abdo tipo estático A, que é Bdinamicamente. Como ab.f()imprime

B:f

segue que as invocações de método não são afetadas pelo tipo de tempo de compilação do objeto com o qual o método é chamado. Mas como é System.out.println ( ab.x )impressa 0, o acesso dos membros é afetado pelos tipos de tempo de compilação.

Um aluno perguntou sobre essa diferença: o acesso de membros e métodos não deve ser consistente entre si? Não consegui encontrar uma resposta melhor do que "essa é a semântica do Java".

Você conheceria uma clara razão conceitual pela qual membros e métodos são diferentes nesse sentido? Algo que eu poderia dar aos meus alunos?

Edit : Após uma investigação mais aprofundada, isso parece ser uma idiossincrasia Java: C ++ e C # agem de maneira diferente, veja, por exemplo, o comentário de Saeed Amiri abaixo.

Edit 2 : Acabei de experimentar o programa Scala correspondente:

class A {
  val x = 0;
  def f () : Unit = { System.out.println ( "A:f" ); } }

class B extends A {
  override val x = 1;
  override def f () : Unit = { System.out.println ( "B:f" ); } }

object Main {
  def main ( args : Array [ String ] ) = {
    var a : A = new A ();
    var b : B = new B ();
    var ab : A = new B ();
    a.f();
    b.f();
    ab.f();
    System.out.println ( "a.x = " + a.x );
    System.out.println ( "b.x = " + b.x );
    System.out.println ( "ab.x = " + ab.x ); } }

E para minha surpresa, isso resulta em

A:f
B:f
B:f
a.x = 0
b.x = 1
ab.x = 1

Observe que os overrisemodificadores são necessários. Isso me surpreende porque o Scala compila na JVM e, além disso, quando eu compilo e executo o programa Java na parte superior usando o compilador / tempo de execução Scala, ele se comporta como o programa Java.

Martin Berger
fonte
3
Isso está relacionado à sua linguagem de programação, como lida com a inicialização variável. Se você fizer isso em C #: ideone.com/q6KpKk , obterá outro resultado, seu problema será anuladof e x, sem mencionar isso explicitamente, a versão C # do IMO é melhor ( mais aceitável do ponto vista OO), em vez de fazer isso, defina x, fcomo virtual, e você obterá resultado consistência, substituindo-os em B. Mas se você perguntar isso no Stackoverflow, receberá mais atenção e tenho certeza de uma ótima resposta.
@SaeedAmiri obrigado pelo C #, eu esperava que fosse o mesmo lá. Se eu não conseguir boas respostas aqui, cruzarei para o Stackoverflow.
9788 Martin-Berger
Passar um link ao cruzar.
Nejc
11
@SaeedAmiri: Não é possível substituir os campos em Java. Pode-se apenas sombrear campos.
Dave Clarke
Posso estar errado, mas li recentemente que Java permite que apenas "substituição de método" e "substituição de campo" não sejam suportadas. Portanto, ao criar uma instância da classe A, o campo procurará a definição mais próxima da entidade x que encontrará na classe A e, portanto, o resultado. As pessoas podem me corrigir se eu estiver errado aqui.
Rajat Saxena

Respostas:

6

Eu suspeito que isso esteja intimamente relacionado ao sombreamento de campos em Java.

Ao escrever uma classe derivada, posso, como no seu exemplo, escrever um campo xque oculte a definição de xna classe base. Isso significa que, na classe derivada, a definição original não é mais acessível via nome x. O motivo pelo qual Java permite isso é que as implementações de classe derivada sejam menos dependentes dos detalhes de implementação da classe base, evitando (apenas parcialmente) o problema frágil da classe base. Por exemplo, se a classe base não tiver originalmente um campo x, mas depois introduzi um, a semântica do Java evita que isso interrompa a implementação da classe derivada.

A variável xna classe derivada pode ter um tipo completamente diferente da variável da classe base - eu testei isso, funciona. Isso contrasta fortemente com a substituição do método, onde o tipo precisa ser suficientemente compatível (também conhecido como o mesmo). Portanto, quando tenho uma variável do tipo Ano seu exemplo, o único tipo que posso derivar é o especificado na classe A(ou acima). Como isso pode ser diferente do tipo declarado na classe B, só é seguro digitar o valor do campo xda classe A. (E, naturalmente, a semântica deve ser a mesma, independentemente do tipo de campo da classe B.)

Dave Clarke
fonte
Olá @DaveClarke sua explicação porque o atributo ab.xreturn deve ser convincente. Mas essa mesma razão, também é válido para os membros de tipo , e de fato C # e C ++ fazer invocação de método no . AxAAfab.f()
22812 Martin Martin-
2
Historicamente, o C ++ permitia o envio dinâmico (virtual) e estático. Os designers de Java consideraram isso muito complexo e, seguindo linguagens como Eiffel e Smalltalk, suportaram apenas semântica para envio de métodos. Novamente, pode-se argumentar que isso é mais simples e mais natural.
Dave Clarke
Olá @DaveClarke, concordo que virtualizar todos os métodos é mais simples. Mas seria ainda mais uniforme se os membros também fossem virtualizados (e não pudessem ter tipos arbitrários). Afinal, era possível ver os métodos como membros de ordem superior. Não estou dizendo que isso é a coisa certa a se fazer, mas me pergunto se há uma razão conceitual e insignificante para fazer uma distinção tão drástica entre métodos e membros.
Martin Berger
Em última análise, a virtualização de membros / campos não significaria realmente nada. Haveria um campo xpara toda a hierarquia de classes. O que significa substituir x? Simplesmente para dar um novo valor. Portanto, a questão é realmente: por que o Java escolheu todos os métodos virtuais e sombreamento de campos ?
Dave Clarke
Mas, @DaveClarke, o exemplo acima do Scala parece-me mostrar que o Scala realmente virtualiza seus membros (você precisa overridedeles para obter tipabilidade. Portanto, algum idioma faz essa escolha. Talvez não exista nenhum argumento claro e simples) neste espaço?
Martin Berger
4

Em Java, não há 'tipo estático de objeto' - existe o tipo de objeto e o tipo de referência. Todos os métodos Java são 'virtuais' (para usar a terminologia C ++), ou seja, são resolvidos com base no tipo do objeto. E aliás. '@Override' não tem nenhum efeito no código compilado - é apenas uma salvaguarda que gera um erro do compilador se o método anotado não substituir o método de uma superclasse '.

Por outro lado, o acesso ao campo nunca é resolvido dinamicamente - ou, para dizer de outra forma, o Java não usa polimorfismo ao acessar campos. No caso (patológico) em que uma subclasse possui um campo x com o mesmo nome que um campo de superclasse, a subclasse possui dois campos 'x'. Obviamente, há sombreamento de nome - o nome 'x' pode ser vinculado a apenas um desses campos em um determinado contexto.

Digite a sintaxe da resolução de nomes - quando você escreve bx, o compilador resolve isso para o campo declarado nas subclasses, mas quando você escreve ab.x, o compilador resolve isso para o campo declarado em A - ambos os campos são membros da classe B. pode acessar os dois campos do código na classe B usando super.x:

classe B {... void g () {System.out.println ("both:" + x + "e" + super.x);}}

Portanto, no tempo de execução, não faz nenhuma diferença se o campo da subclasse 'foi nomeado' x 'ou' y '- nos dois casos, a subclasse possui dois campos distintos sem relação entre eles.

Arno
fonte
Você está certo @Arno, mas por que o Java faz essas escolhas e por que o C # e o Scala fazem escolhas diferentes? Existe uma boa razão clara para isso?
Martin Berger
1

Lembrando que herança é uma forma de dizer B"é um" A, exceto que a funcionalidade de Apossa ser estendida, as variáveis ​​de membro herdadas podem ser consideradas atributos do objeto. Intuitivamente, da maneira como somos ensinados sobre herança (admitidamente do ponto de vista Java-ish), as variáveis ​​de membro são como atributos e os atributos comuns não devem ser estendidos, mas apenas a funcionalidade.

Por exemplo, suponha que você tenha uma Personclasse e a pessoa tenha uma variável de nome. O nome intuitivamente não será alterado, mesmo se estendermos Personpara Studente Professor. No entanto, métodos, por exemplo Student.do_work(), são ampliados, para fazer "trabalhos de casa", enquanto a Professorturma pode classificar os testes Professor.do_work().

É claro que você pode quebrar essa intuição e essencialmente tornar variáveis ​​de membros equivalentes a propriedades ou usar getters, que podem ser substituídos etc., mas acho que o que disse acima foi a intuição de fazê-lo dessa maneira. Na verdade, eu diria que não seria convencional na maioria dos casos ter uma variável de membro com o mesmo nome, em uma classe derivada.

Realz Slaw
fonte