Red de conocimiento informático - Conocimiento sistemático - Comprensión profunda de por qué las clases internas pueden acceder a miembros de clases externas en Java

Comprensión profunda de por qué las clases internas pueden acceder a miembros de clases externas en Java

Introducción a las clases internas

Aunque Java es un lenguaje de programación relativamente simple, todavía hay muchas cosas que resultan confusas para los principiantes.

La comprensión no es muy buena. claro. Las clases internas son una característica que a menudo confunde a los principiantes. Aunque ahora creo que he aprendido bien Java,

pero todavía no está muy claro. Una de las dudas es ¿por qué los objetos de la clase interna pueden acceder a miembros (incluidas las variables miembro y los métodos miembro) en los objetos de la clase externa?

Hace tiempo que quería saber más sobre la función de las clases internas y hoy finalmente me tomé el tiempo para estudiarlas.

Una clase interna es una clase definida dentro de una clase. Hay dos situaciones para las clases definidas dentro de una clase: una es modificada por la palabra clave estática, que se llama clase interna estática,

La otra no es modificada por la palabra clave estática, que es una clase interna ordinaria . Todas las clases internas que se mencionan a continuación se refieren a clases internas ordinarias que no se modifican con la palabra clave estática.

Aunque la clase interna estática también está definida en la clase externa, solo está relacionada con la clase externa en forma (en términos de escritura).

De hecho, lo es lógicamente. Lo mismo que la clase externa. No existe una relación directa. La clase interna general no solo está relacionada formalmente con la clase externa (escrita dentro de la clase externa), sino también lógicamente relacionada con la clase externa.

Esta relación lógica se puede resumir en los siguientes dos puntos:

1 La creación de objetos de clase internos depende de objetos de clase externos

2 Objetos de clase internos; Contiene una referencia a un objeto de clase externo.

El segundo elemento anterior puede explicar por qué se puede acceder a los miembros de la clase externa en la clase interna. Es porque el objeto de clase interno contiene una referencia al objeto de clase externo. Pero no podemos evitar preguntarnos, ¿por qué mantenemos esta referencia? Entonces mira hacia abajo, la respuesta está detrás.

Obtenga la respuesta descompilando el código de bytes

En el nivel del código fuente, no podemos ver la razón, porque Java omite muchas cosas que deberían escribirse por razones de introducción a la sintaxis. es decir, se dice que muchas cosas deben escribirse en el código fuente, pero a modo de introducción, no es necesario escribirlas en el código fuente. El compilador agregará algo de código al compilar. Ahora veamos qué nos agrega el compilador de Java.

Primero cree un proyecto TestInnerClass para realizar pruebas. Para simplificar este proyecto, no se crea ningún paquete, por lo que el código fuente está directamente en el paquete predeterminado. En este proyecto, solo existe el siguiente archivo simple.

1

2

3

4

5

6

6

p>

7

8

9

clase pública exterior {

int campoexterior = 0;

clase interior{

void InnerMethod(){

int i = campo exterior;

}

}

}

Este archivo es muy simple, por lo que no es necesario presentarlo en detalle. La clase interna Inner se define en la clase externa Outer, y se accede a la variable miembro externalField de Outer en el método de Inner.

Aunque estas dos clases están escritas en el mismo archivo, una vez completada la compilación, sus respectivos archivos de clase aún se generan:

Nuestro propósito aquí es explorar el comportamiento de las clases internas. clase, por lo que solo descompila el archivo de clase Outer$Inner.class de la clase interna.

En la línea de comando, cambie al directorio bin del proyecto e ingrese el siguiente comando para descompilar el archivo de clase:

1

javap -classpath -v Outer$Inner

-classpath Indica encontrar el archivo de clase que se va a descompilar en el directorio actual

-v Agregar este parámetro generará información más completa. Incluyendo grupo constante y tabla de variables locales dentro del método, número de línea, indicador de acceso, etc.

Tenga en cuenta que si hay un nombre de paquete, debe escribir el nombre completo del archivo de clase, como por ejemplo:

1

javap -classpath. -v com.baidu .Outer$Inner

Hay muchos resultados de salida de la descompilación. Por razones de espacio, omitimos aquí el grupo constante. La información de salida, excepto el grupo constante, se proporciona a continuación.

1

2

3

4

5

6 p>

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

p>

32

33

34

35

36

37

38

{

final Exterior $0;

banderas: ACC_FINAL, ACC_SYNTHETIC

Exterior$Interior (Exterior);

banderas:

Código:

pila=2, locales=2, args_size=2

0: aload_0

1: aload_1

2: putfield #10 // Campo this$0:LOuter;

5: aload_0

6: invokespecial #12 // Método java/lang/Object."":()V

9: retorno

LineNumberTable:

línea 5: 0

LocalVariableTable:

Longitud inicial Nombre de ranura Firma

0 10 0 this LOuter$Inner;

void InnerMethod();

banderas:

Código:

pila=1, locals=2, args_size=1

0: aload_0

1: getfield #10 // Campo this$0:LOuter;

4: getfield #20 // Campo Outer.outerField:I

7: istore_1

8: devolver

LineNumberTable:

línea 7: 0

línea 8: 8

LocalVariableTable:

Inicio Longitud Ranura Nombre Firma

0 9 0 this LOuter$Inner;

8 1 1 i I

}

Primero veremos la información de la primera línea De la siguiente manera:

1

final Outer this$0;

Esta oración significa que en la clase interna Outer$Inner, hay un nombre llamado this$0. una variable miembro de tipo Exterior, y esta variable es final.

De hecho, esta es la llamada "referencia al objeto de clase externo que existe en el objeto de clase interno". Pero cuando definimos esta clase interna, no la declaramos,

Así que el compilador agregó esta variable miembro.

Aunque el compilador agrega una referencia a la clase externa al crear la clase interna, ¿cómo se asigna esta referencia? Después de todo, primero se le debe asignar un valor antes de que pueda apuntar al objeto de clase externo. A continuación centramos nuestra atención en el constructor. El siguiente resultado es información sobre el constructor.

1

2

3

4

5

6 p>

7

8

9

10

11

12

13

14

15

Exterior$Interior(Exterior);

banderas:

Código:

pila=2, locals=2, args_size=2

0: aload_0

1: aload_1

2 : putfield #10 // Campo this$0:LOuter;

5: aload_0

6: invokespecial #12 // Método java/lang/Object."":( )V

9: retorno

LineNumberTable:

línea 5: 0

LocalVariableTable:

Longitud inicial Firma del nombre de la ranura

0 10 0 this LOuter$Inner;

Sabemos que si un constructor no está declarado en una clase, el compilador agregará un constructor sin parámetros mediante método de construcción predeterminado. Pero esta oración no funciona aquí, porque vemos claramente que este constructor tiene un constructor y el tipo es Externo. Por lo tanto,

El compilador agregará un parámetro al constructor de la clase interna, y el tipo del parámetro es el tipo de la clase externa.

Veamos cómo utilizar este parámetro agregado por defecto en los parámetros de construcción. Analicemos el código de bytes del método constructor. El siguiente es el significado de cada línea de código de bytes:

aload_0:

Carga la primera variable de referencia en la tabla de variables locales en la pila de operandos. Hay algunos puntos a tener en cuenta aquí.

Las variables en la tabla de variables locales se han inicializado antes de que se ejecute el método; las variables en la tabla de variables locales incluyen los parámetros del método; siempre esto; operación La pila es la pila que ejecuta el código actual. Entonces, lo que significa esta oración es: Cargue esta referencia de la tabla de variables locales a la pila de operandos.

aload_1:

Carga la segunda variable de referencia en la tabla de variables locales en la pila de operandos. La variable cargada aquí es el parámetro de tipo externo en el constructor.

putfield #10 // Campo this$0:LOuter;

Utilice la variable de referencia en la parte superior de la pila de operandos para asignar un valor a la variable miembro especificada. Lo que esto significa aquí es asignar el parámetro de tipo externo pasado a la variable miembro this$0.

Este código de bytes de putfield revela cómo se asigna la variable de referencia que apunta al objeto de clase externo.

Las siguientes oraciones de código de bytes no tienen nada que ver con los temas tratados en este artículo y son solo una breve introducción. El significado de los siguientes códigos de bytes es: Utilice esta referencia para llamar al constructor de la clase principal (Objeto) y luego regresar.

Traducido en una forma familiar, esta clase interna y su constructor se parecen un poco a esto: (Tenga en cuenta que esto no se ajusta a la sintaxis de Java, solo para ilustrar el problema)

1

2

3

4

5

6

7< / p>

8

clase Exterior$Interior{

final Exterior este$0;

público Exterior$Interior(Exterior exterior){

this.this$0 = exterior;

super();

}

}

Hablando de esto, puedes Se infiere que cuando se llama al constructor de una clase interna para inicializar un objeto de clase interna, el compilador también pasa una referencia a la clase externa de forma predeterminada.

El formulario de llamada es un poco como este: (Tenga en cuenta que esto no se ajusta a la sintaxis de Java, solo para ilustrar el problema)

vcq9ysfP4M2stcShoyDU2sTasr/A4LXESW5uZXJNZXRob2S3vbeo1tCjrCC3w87KwcvN4rK/wOC1xLPJ1LGx5MG/b3V0ZXJGaWVsZKOsIM /Cw+a 1xNfWvdrC673Syr7By7fDzsrKx8jnus69+NDQtcSjugo8YnI+Cgo8cHJlIGNsYXNzPQ= ="brush:java; ">

void InnerMethod();

banderas:

Código:

pila=1, locales =2, args_size=1

0: aload_0

1: getfield #10 // Campo this$0:LOuter;

4: getfield #20 // Campo

Outer.outerField:I

7: istore_1

8: return

getfield #10 // Campo this$0:LOuter ;

Cargue la variable miembro this$0 en la pila de operandos

getfield #20 // Campo Outer.outerField:I

Utilice la referencia this$0 cargada arriba para agregar la variable miembro externalField de la clase externa se carga en la pila de operandos

istore_1

Guarde el valor de tipo int en la parte superior de la pila de operandos en la segunda variable de la variable local tabla (tenga en cuenta que las dos primeras variables locales están ocupadas por esto,

La segunda variable local es i). La variable de tipo int en la parte superior de la pila de operandos es la variable de campo externo cargada en el paso anterior. Por lo tanto, el significado de este código de bytes es:

Utilice externalField para asignar un valor a i.

Los tres pasos anteriores son cómo la clase interna accede a los miembros de la clase externa a través de la referencia que apunta al objeto de la clase externa.

A estas alturas del artículo, creo que los lectores tendrán una comprensión clara de todo el principio. Hagamos un resumen:

Este artículo explica cómo la clase interna accede a los miembros del objeto de la clase externa descompilando el código de bytes de la clase interna. Además, también tenemos algunas ideas sobre el comportamiento del compilador. Con algo de comprensión, el compilador agregará automáticamente algo de lógica durante la compilación, por lo que nos sentimos confundidos.

En cuanto a cómo la clase interna accede a los miembros de la clase externa, en realidad es muy simple después del análisis. Se realiza principalmente mediante los siguientes pasos:

1 El compilador agrega automáticamente. una variable miembro, el tipo de esta variable miembro es el mismo que el tipo de la clase externa, esta variable miembro es una referencia al objeto de clase externa;

2 El compilador agrega automáticamente un parámetro al constructor de la clase interna, el tipo del parámetro Es el tipo de la clase externa Utilice este parámetro dentro del constructor para asignar valores a las variables miembro agregadas en 1;

3 Al llamar al constructor de. la clase interna para inicializar el objeto de clase interna, la clase externa se pasará de forma predeterminada.