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 ab
do tipo estático A
, que é B
dinamicamente. 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 overrise
modificadores 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.
fonte
f
ex
, sem mencionar isso explicitamente, a versão C # do IMO é melhor ( mais aceitável do ponto vista OO), em vez de fazer isso, definax
,f
como virtual, e você obterá resultado consistência, substituindo-os emB
. Mas se você perguntar isso no Stackoverflow, receberá mais atenção e tenho certeza de uma ótima resposta.Respostas:
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
x
que oculte a definição dex
na classe base. Isso significa que, na classe derivada, a definição original não é mais acessível via nomex
. 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 campox
, mas depois introduzi um, a semântica do Java evita que isso interrompa a implementação da classe derivada.A variável
x
na 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 tipoA
no seu exemplo, o único tipo que posso derivar é o especificado na classeA
(ou acima). Como isso pode ser diferente do tipo declarado na classeB
, só é seguro digitar o valor do campox
da classeA
. (E, naturalmente, a semântica deve ser a mesma, independentemente do tipo de campo da classeB
.)fonte
ab.x
return 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 .A
x
A
A
f
ab.f()
x
para toda a hierarquia de classes. O que significa substituirx
? 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 ?override
deles para obter tipabilidade. Portanto, algum idioma faz essa escolha. Talvez não exista nenhum argumento claro e simples) neste espaço?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.
fonte
Lembrando que herança é uma forma de dizer
B
"é um"A
, exceto que a funcionalidade deA
possa 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
Person
classe e a pessoa tenha uma variável de nome. O nome intuitivamente não será alterado, mesmo se estendermosPerson
paraStudent
eProfessor
. No entanto, métodos, por exemploStudent.do_work()
, são ampliados, para fazer "trabalhos de casa", enquanto aProfessor
turma pode classificar os testesProfessor.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.
fonte