¿Cómo funciona la palabra clave “this” en la herencia de Java?
inheritance polymorphism (7)
Como se indicó anteriormente, no puede anular los campos, solo puede ocultarlos. Ver JLS 8.3. Declaraciones de campo
Si la clase declara un campo con un nombre determinado, se dice que la declaración de ese campo oculta todas y cada una de las declaraciones de campos accesibles con el mismo nombre en las superclases y las superinterfaces de la clase.
A este respecto, la ocultación de los campos difiere de la ocultación de los métodos ( §8.4.8.3 ), ya que no hay distinción entre los campos estáticos y no estáticos en la ocultación de campos, mientras que se hace una distinción entre los métodos estáticos y no estáticos en la ocultación de métodos. .
Se puede acceder a un campo oculto usando un nombre calificado ( §6.5.6.2 ) si es estático, o usando una expresión de acceso de campo que contenga la palabra clave super ( §15.11.2 ) o una §15.11.2 a un tipo de superclase.
En este sentido, la ocultación de campos es similar a la ocultación de métodos.
Una clase hereda de su superclase directa y superinterfaces directas todos los campos no privados de la superclase y las superinterfaces que son accesibles al código en la clase y no están ocultos por una declaración en la clase.
Puede acceder a los campos ocultos de Father
desde el alcance de Son
usando una super
palabra clave, pero lo opuesto es imposible ya que la clase Father
no es consciente de sus subclases.
En el siguiente fragmento de código, el resultado es realmente confuso.
public class TestInheritance {
public static void main(String[] args) {
new Son();
/*
Father father = new Son();
System.out.println(father); //[1]I know the result is "I''m Son" here
*/
}
}
class Father {
public String x = "Father";
@Override
public String toString() {
return "I''m Father";
}
public Father() {
System.out.println(this);//[2]It is called in Father constructor
System.out.println(this.x);
}
}
class Son extends Father {
public String x = "Son";
@Override
public String toString() {
return "I''m Son";
}
}
El resultado es
I''m Son
Father
Por qué "esto" apunta a Son en el constructor del Padre, pero "this.x" apunta al campo "x" en el Padre. ¿Cómo funciona la palabra clave "this"?
Sé sobre el concepto polimórfico , pero ¿no habrá diferencias entre [1] y [2]? ¿Qué está pasando en la memoria cuando se activa un nuevo Son () ?
Dos cosas están sucediendo aquí, mirémoslas:
En primer lugar, estás creando dos campos diferentes. Mirando un trozo (muy aislado) del código de bytes, ves esto:
class Father {
public java.lang.String x;
// Method descriptor #17 ()V
// Stack: 2, Locals: 1
public Father();
...
10 getstatic java.lang.System.out : java.io.PrintStream [23]
13 aload_0 [this]
14 invokevirtual java.io.PrintStream.println(java.lang.Object) : void [29]
17 getstatic java.lang.System.out : java.io.PrintStream [23]
20 aload_0 [this]
21 getfield Father.x : java.lang.String [21]
24 invokevirtual java.io.PrintStream.println(java.lang.String) : void [35]
27 return
}
class Son extends Father {
// Field descriptor #6 Ljava/lang/String;
public java.lang.String x;
}
Son importantes las líneas 13, 20 y 21; los otros representan el System.out.println();
sí, o el return;
implícito return;
. aload_0
carga this
referencia, getfield
recupera un valor de campo de un objeto, en este caso, de this
. Lo que ve aquí es que el nombre de campo está calificado: Father.x
. En una línea en Son
, puedes ver que hay un campo separado. Pero Son.x
nunca se usa; solo el Father.x
es.
Ahora, qué Son.x
si eliminamos Son.x
y en su lugar agregamos este constructor:
public Son() {
x = "Son";
}
Primero un vistazo al bytecode:
class Son extends Father {
// Field descriptor #6 Ljava/lang/String;
public java.lang.String x;
// Method descriptor #8 ()V
// Stack: 2, Locals: 1
Son();
0 aload_0 [this]
1 invokespecial Father() [10]
4 aload_0 [this]
5 ldc <String "Son"> [12]
7 putfield Son.x : java.lang.String [13]
10 return
}
Las líneas 4, 5 y 7 se ven bien: this
y "Son"
están cargados, y el campo está configurado con putfield
. ¿Por qué Son.x
? porque la JVM puede encontrar el campo heredado. Pero es importante tener en cuenta que aunque el campo está referenciado como Son.x
, el campo encontrado por la JVM es en realidad Father.x
.
¿Entonces da la salida correcta? Lamentablemente no:
I''m Son
Father
El motivo es el orden de los enunciados. Las líneas 0 y 1 en el código de bytes son el super();
implícito super();
llamar, por lo que el orden de las declaraciones es la siguiente:
System.out.println(this);
System.out.println(this.x);
x = "Son";
Por supuesto que va a imprimir "Father"
. Para deshacerse de eso, se podrían hacer algunas cosas.
Probablemente el más limpio es: ¡no imprima en el constructor! Mientras el constructor no haya terminado, el objeto no está completamente inicializado. Está trabajando en el supuesto de que, dado que las println
son las últimas declaraciones en su constructor, su objeto está completo. Como ha experimentado, esto no es cierto cuando tiene subclases, porque el constructor de la superclase siempre terminará antes de que su subclase tenga la oportunidad de inicializar el objeto.
Algunos ven esto como una falla en el concepto de constructores en sí mismos; y algunos lenguajes ni siquiera usan constructores en este sentido. Podrías usar un método init()
lugar . En los métodos comunes, tiene la ventaja del polimorfismo, por lo que puede llamar a init()
en una referencia del Father
, y se invoca a Son.init()
; mientras que el new Father()
siempre crea un objeto Father
. (por supuesto, en Java aún debe llamar al constructor correcto en algún momento).
Pero creo que lo que necesitas es algo como esto:
class Father {
public String x;
public Father() {
init();
System.out.println(this);//[2]It is called in Father constructor
System.out.println(this.x);
}
protected void init() {
x = "Father";
}
@Override
public String toString() {
return "I''m Father";
}
}
class Son extends Father {
@Override
protected void init() {
//you could do super.init(); here in cases where it''s possibly not redundant
x = "Son";
}
@Override
public String toString() {
return "I''m Son";
}
}
No tengo un nombre para eso, pero pruébalo. Se imprimirá
I''m Son
Son
Entonces, ¿qué está pasando aquí? Su constructor superior (el de Father
) llama a un método init()
, que se invalida en una subclase. Como todos los constructores llaman super();
Primero, se ejecutan efectivamente superclase a subclase. Entonces, si la primera llamada del constructor superior es init();
entonces todo el inicio ocurre antes de cualquier código de constructor. Si su método init inicializa completamente el objeto, entonces todos los constructores pueden trabajar con un objeto inicializado. Y como init()
es polimórfico, incluso puede inicializar el objeto cuando hay subclases, a diferencia del constructor.
Tenga en cuenta que init()
está protegido: las subclases podrán llamarlo y anularlo, pero las clases en otro paquete no podrán llamarlo. Eso es una leve mejora sobre el public
y debería considerarse para x
también.
Este es un comportamiento hecho especialmente para tener acceso a miembros privados. Entonces, este.x mira la variable X que se declara para el padre, pero cuando se pasa esto como un parámetro a System.out.println
en un método en el padre - mira el método para llamar dependiendo del tipo del parámetro - En tu caso hijo.
Entonces, ¿cómo se llama el método de las súper clases? Usando super.toString()
, etc.
Desde Padre no puede acceder a la variable x de Hijo.
Esto se conoce comúnmente como sombreado . Tenga en cuenta sus declaraciones de clase:
class Father {
public String x = "Father";
y
class Son extends Father {
public String x = "Son";
Esto crea 2 variables distintas llamadas x
cuando creas una instancia de Son
. Una x
pertenece a la superclase Father
, y la segunda x
pertenece a la subclase Son
. Según la salida, podemos ver que cuando está en el alcance de Father
, this
accede a la variable de instancia x
del Father
. Así que el comportamiento no está relacionado con "a qué apunta this
"; es el resultado de cómo el tiempo de ejecución busca las variables de instancia. Solo sube la jerarquía de clases para buscar variables. Una clase solo puede hacer referencia a variables de sí misma y sus clases primarias; no puede acceder a las variables de sus clases secundarias directamente porque no sabe nada sobre sus hijos.
Para obtener el comportamiento polimórfico que desea, solo debe declarar x
en Father
:
class Father {
public String x;
public Father() {
this.x = "Father"
}
y
class Son extends Father {
public Son() {
this.x = "Son"
}
Este artículo discutió el comportamiento que está experimentando exactamente: http://www.xyzws.com/Javafaq/what-is-variable-hiding-and-shadowing/15 .
Las invocaciones de métodos polimórficos se aplican solo a los métodos de instancia. Siempre puede hacer referencia a un objeto con un tipo de variable de referencia más general (una superclase o interfaz), pero en el tiempo de ejecución, las ÚNICAS cosas que se seleccionan dinámicamente según el objeto real (en lugar del tipo de referencia) son métodos de instancia. MÉTODOS NO ESTÁTICOS . NO VARIABLES . Solo los métodos de instancia reemplazados se invocan dinámicamente según el tipo de objeto real.
Entonces, la variable x
no tiene un comportamiento polimórfico porque NO SERÁ SELECCIONADA DINÁMICAMENTE EN TIEMPO DE EJECUCIÓN.
Explicando tu código:
System.out.println(this);
El tipo de objeto es Son
por lo que se invocará la versión anulada del Son
toString()
.
System.out.println(this.x);
El tipo de objeto no está en la imagen aquí, this.x
está en la clase Father
, por lo que se imprimirá la versión Father
la variable x
.
Ver más en: polimorfismo en java
Mientras que los métodos se pueden anular, los atributos se pueden ocultar.
En su caso, el atributo x
está oculto: en su clase de Son
, no puede acceder al valor x
del Father
menos que use la palabra clave super
. La clase Father
no sabe acerca del atributo x
del Son
.
En el opuesto, el método toString()
está anulado: la implementación que siempre se llamará es la de la clase instanciada (a menos que no la anule), es decir, en su caso, Son
, independientemente del tipo de variable ( Object
, Father
. ..).
Todas las funciones miembro son polimórficas en Java por defecto. Eso significa que cuando llama a this.toString () Java usa un enlace dinámico para resolver la llamada, llamando a la versión secundaria. Cuando accede al miembro x, accede al miembro de su alcance actual (el padre) porque los miembros no son polimórficos.