I. Fundamentos de las clases internas

En Java, se puede definir una clase dentro de otra clase o dentro de un método; a dicha clase se le llama clase interna. En un sentido amplio, las clases internas generalmente incluyen estos cuatro tipos: clase interna miembro, clase interna local, clase interna anónima y clase interna estática. A continuación, veamos el uso de estos cuatro tipos de clases internas.

1. Clase interna miembro

La clase interna miembro es el tipo más común de clase interna. Se define como aquella que se encuentra dentro de otra clase, con la siguiente forma:

class Circle {
    double radius = 0;
     
    public Circle(double radius) {
        this.radius = radius;
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}

Así, la clase Draw parece ser un miembro de la clase Circle; Circle se denomina clase externa. La clase interna miembro puede acceder incondicionalmente a todos los atributos y métodos miembro de la clase externa (incluidos los miembros privados y los miembros estáticos).

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
            System.out.println(count);   //外部类的静态成员
        }
    }
}

Sin embargo, hay que tener en cuenta que cuando la clase interna miembro tiene variables miembro o métodos con el mismo nombre que la clase externa, se produce el fenómeno de ocultamiento; es decir, por defecto se accede a los miembros de la clase interna miembro. Si se desea acceder al miembro homónimo de la clase externa, se debe hacer de la siguiente forma:

外部类.this.成员变量
外部类.this.成员方法

Aunque la clase interna miembro puede acceder incondicionalmente a los miembros de la clase externa, la clase externa no puede acceder tan libremente a los miembros de la clase interna miembro. En la clase externa, para acceder a los miembros de la clase interna miembro, primero se debe crear un objeto de la clase interna miembro y luego acceder a través de la referencia que apunta a ese objeto:

class Circle {
    private double radius = 0;
 
    public Circle(double radius) {
        this.radius = radius;
        getDrawInstance().drawSahpe();   //必须先创建成员内部类的对象,再进行访问
    }
     
    private Draw getDrawInstance() {
        return new Draw();
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
        }
    }
}

La clase interna miembro existe dependiendo de la clase externa; es decir, para crear un objeto de la clase interna miembro, es requisito previo que exista un objeto de la clase externa. La forma general de crear un objeto de clase interna miembro es la siguiente:

public class Test {
    public static void main(String[] args)  {
        //第一种方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必须通过Outter对象来创建
         
        //第二种方式:
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}
 
class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    class Inner {
        public Inner() {
             
        }
    }
}

Las clases internas pueden tener permiso de acceso private, protected, public y acceso de paquete. Por ejemplo, en el caso anterior, si la clase interna miembro Inner se modifica con private, solo se puede acceder a ella dentro de la clase externa; si se modifica con public, se puede acceder desde cualquier lugar; si se modifica con protected, solo se puede acceder dentro del mismo paquete o cuando se hereda la clase externa; si es el permiso de acceso por defecto, solo se puede acceder dentro del mismo paquete. Esto es un poco diferente de la clase externa, que solo puede modificarse con public y acceso de paquete. Personalmente lo entiendo así: como la clase interna miembro parece ser un miembro de la clase externa, puede tener múltiples modificadores de permiso como los miembros de una clase.

2. Clase interna local

La clase interna local es una clase definida dentro de un método o de un ámbito. Se diferencia de la clase interna miembro en que su acceso está limitado al método o al ámbito en el que se define.

class People{
    public People() {
         
    }
}
 
class Man{
    public Man(){
         
    }
     
    public People getWoman(){
        class Woman extends People{   //局部内部类
            int age =0;
        }
        return new Woman();
    }
}
 

Nota: La clase interna local es como una variable local dentro de un método; no puede tener modificadores public, protected, private ni static.

3. Clase interna anónima

La clase interna anónima es probablemente la que más usamos al escribir código habitualmente. Al escribir código para escuchar eventos, usar una clase interna anónima no solo es conveniente, sino que también hace que el código sea más fácil de mantener. El siguiente código es un fragmento de código de escucha de eventos en Android:

scan_bt.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});
 
history_bt.setOnClickListener(new OnClickListener() {
     
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});

Este código establece listeners para dos botones, y aquí se utilizan clases internas anónimas. Dentro de este código:

new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
}

Eso es el uso de una clase interna anónima. En el código se necesita asignar un objeto listener a los botones. Usar una clase interna anónima permite implementar los métodos de la clase padre o de la interfaz y, al mismo tiempo, generar un objeto correspondiente; pero la premisa es que esa clase padre o interfaz debe existir primero para poder usarla de esta manera. Por supuesto, también se puede escribir como se muestra a continuación, logrando el mismo efecto que la clase interna anónima anterior.

private void setListener()
{
    scan_bt.setOnClickListener(new Listener1());       
    history_bt.setOnClickListener(new Listener2());
}
 
class Listener1 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}
 
class Listener2 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}

Aunque esta forma de escribir puede lograr el mismo efecto, es tanto verbosa como difícil de mantener, por lo general se usa el método de clase interna anónima para escribir código de escucha de eventos. De igual manera, la clase interna anónima tampoco puede tener modificadores de acceso ni el modificador static.

La clase interna anónima es el único tipo de clase que no tiene constructor. Precisamente porque no tiene constructor, su ámbito de uso es muy limitado; la mayoría de las clases internas anónimas se utilizan para devoluciones de llamada (callbacks) de interfaces. En tiempo de compilación, el sistema nombra automáticamente a la clase interna anónima como Outter$1.class. En general, la clase interna anónima se usa para heredar otras clases o implementar interfaces; no es necesario añadir métodos adicionales, solo se implementan o sobreescriben los métodos heredados.

4. Clase interna estática

La clase interna estática también es una clase definida dentro de otra clase, solo que tiene la palabra clave static delante de la clase. La clase interna estática no necesita depender de la clase externa; esto es algo similar a los atributos de miembros estáticos de una clase. Además, no puede usar variables miembro ni métodos no static de la clase externa. Esto es fácil de entender: sin un objeto de la clase externa, se puede crear un objeto de la clase interna estática; si se permitiera acceder a los miembros no static de la clase externa, se produciría una contradicción, porque los miembros no static de la clase externa deben depender de un objeto concreto.

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}
 
class Outter {
    public Outter() {
         
    }
     
    static class Inner {
        public Inner() {
             
        }
    }
}


II. Comprensión profunda de las clases internas

1. ¿Por qué la clase interna miembro puede acceder incondicionalmente a los miembros de la clase externa?

Antes de esto, ya hemos discutido que la clase interna miembro puede acceder incondicionalmente a los miembros de la clase externa. Entonces, ¿cómo se implementa exactamente? A continuación, veamos qué sucede mediante la descompilación del archivo de bytecode. De hecho, cuando el compilador compila, compila la clase interna miembro por separado en un archivo de bytecode. El siguiente es el código de Outter.java:

public class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    protected class Inner {
        public Inner() {
             
        }
    }
}

Después de la compilación, aparecen dos archivos de bytecode:

Al descompilar el archivo Outter$Inner.class se obtiene la siguiente información:

E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
  SourceFile: "Outter.java"
  InnerClass:
   #24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
  minor version: 0
  major version: 50
  Constant pool:
const #1 = class        #2;     //  com/cxh/test2/Outter$Inner
const #2 = Asciz        com/cxh/test2/Outter$Inner;
const #3 = class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        this$0;
const #6 = Asciz        Lcom/cxh/test2/Outter;;
const #7 = Asciz        <init>;
const #8 = Asciz        (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz        Code;
const #10 = Field       #1.#11; //  com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;//  this$0:Lcom/cxh/test2/Outter;
const #12 = Method      #3.#13; //  java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;//  "<init>":()V
const #14 = Asciz       ()V;
const #15 = Asciz       LineNumberTable;
const #16 = Asciz       LocalVariableTable;
const #17 = Asciz       this;
const #18 = Asciz       Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz       SourceFile;
const #20 = Asciz       Outter.java;
const #21 = Asciz       InnerClasses;
const #22 = class       #23;    //  com/cxh/test2/Outter
const #23 = Asciz       com/cxh/test2/Outter;
const #24 = Asciz       Inner;
 
{
final com.cxh.test2.Outter this$0;
 
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   putfield        #10; //Field this$0:Lcom/cxh/test2/Outter;
   5:   aload_0
   6:   invokespecial   #12; //Method java/lang/Object."<init>":()V
   9:   return
  LineNumberTable:
   line 16: 0
   line 18: 9
 
  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      10      0    this       Lcom/cxh/test2/Outter$Inner;
 
 
}

Las líneas 11 a 35 son el contenido del grupo de constantes; a continuación, se explica el contenido de la línea 38.

final com.cxh.test2.Outter this$0;

Esta línea es un puntero al objeto de la clase externa. Al ver esto, seguramente todos lo tienen claro. Es decir, el compilador añade por defecto a la clase interna miembro una referencia al objeto de la clase externa. Entonces, ¿cómo se inicializa esta referencia? A continuación, veamos el constructor de la clase interna:

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);

De aquí se puede ver que, aunque el constructor de la clase interna que definimos es un constructor sin parámetros, el compilador añade por defecto un parámetro, cuyo tipo es una referencia al objeto de la clase externa. Por lo tanto, el puntero Outter this&0 dentro de la clase interna miembro apunta al objeto de la clase externa, permitiendo acceder libremente a los miembros de la clase externa. Esto también demuestra indirectamente que la clase interna miembro depende de la clase externa; si no se crea un objeto de la clase externa, no se puede inicializar la referencia Outter this&0 y, por tanto, no se puede crear un objeto de la clase interna miembro.

2. ¿Por qué las clases internas locales y las clases internas anónimas solo pueden acceder a variables locales final?

Seguramente este problema también ha preocupado a muchas personas. Antes de discutirlo, veamos el siguiente código:

public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}
 

Este código se compilará en dos archivos .class: Test.class y Test1.class.Por defecto, el compilador nombra a las clases internas anónimas y a las clases internas locales comoOutterx.class(x es un entero positivo)。

Según la figura anterior, el nombre de la clase interna anónima en el método test es Test$1.

En el código anterior, si se elimina cualquiera de los final delante de las variables a y b, este código no compilará. Primero consideremos la siguiente cuestión:

Cuando el método test termina de ejecutarse, el ciclo de vida de la variable a finaliza. Sin embargo, es muy probable que en ese momento el ciclo de vida del objeto Thread aún no haya terminado, por lo que seguir accediendo a la variable a en el método run de Thread se vuelve imposible. Pero como se necesita lograr ese efecto, ¿qué se hace? Java adopta el método de copia para resolver este problema. Al descompilar el bytecode de este código se puede obtener el siguiente contenido:

Vemos que en el método run hay una instrucción:

bipush 10

Esta instrucción indica que se empuja el operando 10 a la pila, lo que significa que se está usando una variable local. Este proceso lo realiza el compilador por defecto durante la compilación. Si el valor de esta variable se puede determinar en tiempo de compilación, el compilador añadirá por defecto un literal con contenido equivalente al grupo de constantes de la clase interna anónima (o clase interna local), o directamente incrustará el bytecode correspondiente en el bytecode de ejecución. De esta manera, la variable usada por la clase interna anónima es otra variable local, solo que su valor es igual al de la variable local en el método, por lo que es completamente independiente de la variable local del método.

Veamos ahora otro ejemplo:

public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int a) {
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}

La descompilación produce:

Vemos que el constructor de la clase interna anónima Test$1 tiene dos parámetros: uno es una referencia al objeto de la clase externa y el otro es una variable de tipo int. Evidentemente, aquí el parámetro formal a del método test se pasa como argumento para inicializar la copia (la copia de la variable a) dentro de la clase interna anónima.

Es decir, si el valor de la variable local se puede determinar en tiempo de compilación, se crea directamente una copia dentro de la clase interna anónima. Si el valor de la variable local no se puede determinar en tiempo de compilación, entonces se inicializa la copia pasando el parámetro a través del constructor.

De lo anterior se puede ver que la variable a a la que se accede en el método run no es en absoluto la variable local a del método test. De este modo se resuelve el problema de la inconsistencia de ciclos de vida mencionado antes. Pero surge un nuevo problema: dado que la variable a a la que se accede en el método run no es la misma que la variable a del método test, ¿qué ocurre si se cambia el valor de la variable a dentro del método run?

Exacto, provocaría una inconsistencia de datos, y así no se lograría la intención y los requisitos originales. Para resolver este problema, el compilador de Java exige que la variable a deba limitarse a ser una variable final, no permitiendo modificar la variable a (para variables de tipo referencia, no se permite apuntar a un nuevo objeto). De esta manera, se resuelve el problema de la inconsistencia de datos.

En este punto, seguramente todos deberían tener claro por qué las variables locales y los parámetros formales del método deben limitarse con final.

3. ¿Tienen las clases internas estáticas algo especial?

Como se sabe por lo anterior, las clases internas estáticas no dependen de la clase externa, es decir, se puede crear un objeto de la clase interna sin crear un objeto de la clase externa. Además, la clase interna estática no mantiene una referencia al objeto de la clase externa; el lector puede intentar decompilar el archivo .class para comprobarlo, no hayOutter this&0referencia.


III. Escenarios de uso y ventajas de las clases internas

¿Por qué se necesitan clases internas en Java? En resumen, hay principalmente los siguientes cuatro puntos:

  • 1. Cada clase interna puede heredar de forma independiente la implementación de una interfaz, por lo que no importa si la clase externa ya ha heredado alguna implementación (de interfaz), no tiene ningún efecto sobre la clase interna. Las clases internas hacen que la solución de herencia múltiple sea completa.
  • 2. Facilitan organizar juntas clases que tienen cierta relación lógica y, a su vez, pueden ocultarse del exterior.
  • 3. Facilitan la escritura de programas controlados por eventos.
  • 4. Facilitan la escritura de código de hilos.

Personalmente creo que el primer punto es una de las razones más importantes; la existencia de las clases internas hace que el mecanismo de herencia múltiple de Java sea más completo.

4. Preguntas comunes de exámenes escritos y entrevistas relacionadas con clases internas

1. Completa el código en los lugares (1), (2) y (3) según los comentarios.

public class Test{
    public static void main(String[] args){
           // 初始化Bean1
           (1)
           bean1.I++;
           // 初始化Bean2
           (2)
           bean2.J++;
           //初始化Bean3
           (3)
           bean3.k++;
    }
    class Bean1{
           public int I = 0;
    }
 
    static class Bean2{
           public int J = 0;
    }
}
 
class Bean{
    class Bean3{
           public int k = 0;
    }
}

Como se sabe por lo anterior, para una clase interna miembro, primero se debe crear una instancia de la clase externa para poder crear una instancia de la clase interna. En cambio, una clase interna estática puede crear una instancia de la clase interna sin necesidad de crear una instancia de la clase externa.

La forma general de crear un objeto de clase interna estática es:NombreClaseExterna.NombreClaseInterna xxx = new NombreClaseExterna.NombreClaseInterna()

La forma general de crear un objeto de clase interna miembro es:NombreClaseExterna.NombreClaseInterna xxx = NombreObjetoClaseExterna.new NombreClaseInterna()

Por lo tanto, los códigos en los lugares (1), (2) y (3) son respectivamente:

Test test = new Test();    
Test.Bean1 bean1 = test.new Bean1();
Test.Bean2 b2 = new Test.Bean2();
Bean bean = new Bean();     

Bean.Bean3 bean3 =  bean.new Bean3();

2. ¿Cuál es el resultado de salida del siguiente código?

public class Test {
    public static void main(String[] args)  {
        Outter outter = new Outter();
        outter.new Inner().print();
    }
}
 
 
class Outter
{
    private int a = 1;
    class Inner {
        private int a = 2;
        public void print() {
            int a = 3;
            System.out.println("局部变量:" + a);
            System.out.println("内部类变量:" + this.a);
            System.out.println("外部类变量:" + Outter.this.a);
        }
    }
}
3
2
1

Por último, un complemento de conocimiento: sobre el problema de la herencia de las clases internas miembro. En general, las clases internas rara vez se usan para herencia. Pero cuando se usan para heredar, hay que tener en cuenta dos puntos:

  • 1) La forma de referencia de la clase interna miembro debe ser Outter.Inner
  • 2) El constructor debe tener una referencia al objeto de la clase externa, y a través de esta referencia se debe llamar a super(). Este código está extraído de "Thinking in Java".
class WithInner {
    class Inner{
         
    }
}
class InheritInner extends WithInner.Inner {
      
    // InheritInner() 是不能通过编译的,一定要加上形参
    InheritInner(WithInner wi) {
        wi.super(); //必须有这句调用
    }
  
    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner obj = new InheritInner(wi);
    }
}

Fuente original: https://www.cnblogs.com/dolphin0520/p/3811445.html