内部类

本文详细解析Java内部类的四种情况:私有内部类、静态内部类、局部内部类和匿名内部类,阐述它们的特点及如何实现互相访问私有成员,同时探讨静态内部类与非静态内部类的区别,以及局部内部类对方法内局部变量的访问限制。通过代码实例与反编译分析,揭示Java编译器如何处理内部类,并解释内部类为何能访问外部类的私有域。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

内部类有两种情况:

(1) 在类中定义一个类(私有内部类,静态内部类)

(2) 在方法中定义一个类(局部内部类,匿名内部类)

 

1、私有内部类 —— 在方法之间定义的内部类,非静态

      我们首先看看类中内部类的两个特点:

     (1) 在外部类的作用范围内可以任意创建内部类对象,即使内部类是私有的(私有内部类)。即内部类对包围它的外部类可见。

Java代码   
  1. //代码1:内部类对外部类可见   
  2. class Outer{   
  3.      //创建私有内部类对象   
  4.      public Inner in=new Inner();   
  5.      //私有内部类   
  6.      private class Inner{   
  7.           ...   
  8.      }   
  9.  

      (2) 在内部类中可以访问其外部类的所有域,即使是私有域。即外部类对内部类可见。

Java代码   
  1. //代码2:外部类对内部类可见   
  2. class Outer{   
  3.        //外部类私有数据域   
  4.        private int data=0;   
  5.        //内部类   
  6.        class Inner{   
  7.            void print(){   
  8.                  //内部类访问外部私有数据域   
  9.                  System.out.println(data);   
  10.               
  11.        }   
  12.  

       问题来了:上面两个特点到底如何办到的呢?内部类的"内部"到底发生了什么?

 

 

      其实,内部类是Java编译器一手操办的。虚拟机并不知道内部类与常规类有什么不同。 编译器是如何瞒住虚拟机的呢?


     对内部类进行编译后发现有两个class文件:Outer.class 和Outer$Inner.class 。这说明内部类Inner仍然被编译成一个独立的类(Outer$Inner.class),而不是Outer类的某一个域。虚拟机运行的时候,也是把Inner作为一种常规类来处理的。

 

        但问题来了,即然是两个常规类,为什么他们之间可以互相访问私有域那(最开始提到的两个内部类特点)?这就要问问编译器到底把这两个类编译成什么东西了。

 

        我们利用reflect反射机制来探查了一下内部类编译后的情况(关于探查类内部机制的代码提供在下面的附件里Reflect.java)。

        (1)、编译代码1生成 Outer$Inner.class 文件后使用 ReflectUtil.reflect("Outer$Inner") 对内部类Inner进行反射。运行结果 发现了三个隐含的成分:         

反编译代码   
  1. //反编译1  
  2. class Outer$Inner   
  3. {   
  4.         Outer$Inner(Outer,Outer$Inner);  //包可见构造器   
  5.         private Outer$Inner(Outer);   //私有构造器将设置this$0域   
  6.         final Outer this$0  //外部类实例域this$0  
  7.  
 

 

      好了,现在我们可以解释上面的第一个内部类特点了: 为什么外部类可以创建内部类的对象?并且内部类能够方便的引用到外部类对象?

     首先编译器将外、内部类编译后放在同一个包中。在内部类中附加一个包可见构造器。这样, 虚拟机运行Outer类中Inner in=new Inner(); 实际上调用的是包可见构造: new Outer$Inner(this,null)。因此即使是private内部类,也会通过隐含的包可见构造器成功的获得私有内部类的构造权限。

      再者,Outer$Inner类中有一个指向外部类Outer的引用this$0,那么通过这个引用就可以方便的得到外部类对象中可见成员。但是Outer类中的private成员是如何访问到的呢?这就要看看下面Outer.class文件中的秘密了。

 

      (2)、编译代码2生成 Outer.class文件,然后使用 ReflectUtil.reflect("Outer") 对外部类Outer进行反射。 运行结果 发现一个隐含成分如下:

反编译代码   
  1. //反编译2  
  2. class Outer   
  3. {   
  4.           static int access$0(Outer);  //静态方法,返回值是外部类私有域 data 的值。   
  5.  
       

      现在可以解释第二个特点了:为什么内部类可以引用外部类的私有域?

 

         原因的关键就在编译器在外围类中添加了静态方法access$0。它将返回值作为参数传递给他的对象域data。这样内部类Inner中的打印语句:

                     System.out.println(data);

         实际上运行的时候调用的是:

                 S ystem.out.println(this$0.access$0(Outer));

 

总结一下编译器对类中内部类做的手脚吧:

(1)  在内部类中偷偷摸摸的创建了包可见构造器,从而使外部类获得了创建权限。

(2)  在外部类中偷偷摸摸的创建了访问私有变量的静态方法,从而 使 内部类获得了访问权限。

这样,类中定义的内部类无论私有,公有,静态都可以被包围它的外部类所访问。

 

 

2、静态内部类  ——  在方法间定义的内部类,静态

     内部类也有静态的区别,这就是静态内部类,我们来看看代码:

Java代码   
  1. package hr.test;   
  2. //代码3:静态内部类对外部变量的引用   
  3. public class Outer{     
  4.         private static int i=0          
  5.         //创建静态内部类对象   
  6.     public Inner in=new Inner();     
  7.     //静态   
  8.     private static class Inner{     
  9.         public void print(){   
  10.                          System.out.println(i);   //如果i不是静态变量,这里将无法通过编译。   
  11.                 }   
  12.         
  13.   
  14.    

       静态内部类和私有内部类最大的区别在于,静态内部类中无法引用到其外围类的非静态成员。这是为什么?我们还是来看看静态内部类Outer$Inner中发生了什么吧?

反编译代码   
  1. //反编译3  
  2. class Outer$Inner   
  3. {   
  4.       private Outer$Inner();   
  5.       Outer$Inner(hr.test.Outer$Inner);   
  6.  

        与上面私有内部类反编译1比较发现,少了一个指向外围类对象的引用final Outer this$0; 也就是说静态内部类无法得到其外围类对象的引用,那么自然也就无法访问外围类的非静态成员了。因此,静态内部类只能访问其外围类的静态成员,除此之外与非静态内部类没有任何区别。

 

3、局部内部类 —— 在方法中定义的内部类

      方法内部类也有两个特点

      (1)  方法中的内部类没有访问修饰符, 即方法内部类对包围它的方法之外的任何东西都不可见。

      (2)  方法内部类只能够访问该方法中的局部变量,所以也叫局部内部类。而且这些局部变量一定要是final修饰的常量。

Java代码   
  1. class Outter{   
  2.       public void outMethod(){   
  3.              final int beep=0;   
  4.              class Inner{   
  5.                    //使用beep   
  6.              }   
  7.              Inner in=new Inner();   
  8.       }   
  9.  
 

      这又是为什么呢?

      (1) 我们首先对Outter类进行反射发现,Outter中再也没有返回私有域的隐藏方法了。

      (2) 对Inner类的反射发现,Inner类内部多了一个对beep变量的备份隐藏域:final int val$i;

 

      我们可以这样解释Inner类中的这个备份常量域,首先当JVM运行到需要创建Inner对象之后,Outter类已经全部运行完毕,这是垃圾回收机制很有可能释放掉局部变量beep。那么Inner类到哪去找beep变量呢?

      编译器又出来帮我们解决了这个问题,他在Inner类中创建了一个beep的备份 ,也就是说即使Ouuter中的beep被回收了,Inner中还有一个备份存在,自然就不怕找不到了。

      但是问题又来了。如果Outter中的beep不停的在变化那。那岂不是也要让备份的beep变量无时无刻的变化。为了保持局部变量与局部内部类中备份域保持一致。 编译器不得不规定死这些局部域必须是常量,一旦赋值不能再发生变化了。

      所以为什么局部内部类应用外部方法的域必须是常量域的原因所在了。

 

内部类的特点总结

 

(1)  在方法间定义的非静态内部类:

       ● 外围类和内部类可互相访问自己的私有成员。

       ● 内部类中不能定义静态成员变量。


(2) 在方法间定义的静态内部类:

       ● 只能访问外部类的静态成员。

 


(3) 在方法中定义的局部内部类:

       ● 该内部类没有任何的访问控制权限

       ● 外围类看不见方法中的局部内部类的,但是局部内部类可以访问外围类的任何成员。

       ● 方法体中可以访问局部内部类,但是访问语句必须在定义局部内部类之后。

       ● 局部内部类只能访问方法体中的常量,即用final修饰的成员。


(4) 在方法中定义的匿名内部类:

       ● 没有构造器,取而代之的是将构造器参数传递给超类构造器。

 

-------------------------------------------------------------------------------------

方法内部类是JAVA里边非常尴尬的角色,基本上属于食之无味,弃之不惜。但它的使用陷阱非常多。最大的陷阱就是局部方法内部类不能访问方法内的局部变量。人们都会觉得非常之不解,我到自家粮库里拿点粮食,还不行了?!大家都属于一个组织,同在一个方法下,何必逼人太甚。可是就是不行。因为方法内部类对象,那也是堆对象,它生存能力就是比较你局部变量(栈上分配)强,当方法调用结束时,它产生的这个对象还存在着。可能有人觉得非常不服气,方法调用都结束了,你这个类对象还赖着,有什么意思?可是方法是可以返回对象引用的,这个局部方法内部类,可能是实现或者继承了某个类,返回给父类引用,那是完全合情合理的。如果真要使用局部变量,那么在变量前加final,有了final这道护身符,就是跳上枝头当凤凰了,可以被编制入常量区,而不用在局部方法经常混的栈区,那么这个局部变量的生存能力,就可以和对象一样强了。另外,局部方法内部类可以使用final,和 abstract 的修饰符。其它修饰符都不行!最后,局部方法内部类,可以访问外部类的任何数据成员,包括private成员。下边这个例子,是关于局部方法内部类与内部类,如何匹配。可以尝试解一下,答案见最后。

class A {
  
 void m(){
      System.out.println(
"outer");
   }
}

public class TestInners {
  
 public static void main(String[] args) {
     
 new TestInners().go();
     
   }
  
 void go(){
     
 new A().m();
     
 class A{
        
 void m(){
            System.out.println(
"inner");
         }
      }
   }
  
  
 class A {
     
 void m(){
         System.out.println(
"middle");
      }
   }
}

----------------------------------------------------------------------------------------
在执行go方法是,它是先实例化TestInners里面的A类(midlle类)因为他会屏蔽掉外部outer的A类,不实例化inner类是因为inner类处在其后,我在最后的时候加了一句new A().m();他会实例化最近的A类,(屏蔽掉了middle的A类),所以在实例化过程中当出现同名的时候,一般都是实例化最近的类。new A时,查找最近的类A,TestInnner类中的A类,屏蔽了外部的A类,而go函数中定义的A类,在new A()后面,似乎也不能提前使用。

   这与类的加载顺序无关,如果类名使用的全名,即包含包名的全名,则直接查找指定的类,如果类名使用简单名称,则根据当前代码的位置,取最近的那个具有指定简单名称的类,java默认是从自己内部找,先找局部内部类,再找成员内部类,最后找外部类。

    我们看看各他们各自生成的.class文件名。public class TestInners 对应的肯定是 TestInners.class; 内部类是 TestInners$A.class,外部outer类事实上就是另一个外部类,无非就是访问权限问题,引用的时候少了一个import,因为同处于一个.java文件当中,直接访问。而方法内部的这个类也是用用对应的.class文件名的,TestInners&1.class。这似乎是用数字作为序号标记的。而普通内部类是以:外部类名&内部类名.class 命名的。

 

输出:
middle

--------------------------------------------------------------------------------------

 

在搞Spring时,有很多方法都用到了回调函数,因而涉及到了更多的内部类,在使用内部类时,发现从内部类中访问局部变量需要被声明为最终类型(final),有些迷惑,找了一些分析文章。如下:

这是从编译器的角度去分析的:

class A
{
public void shout(final   int iargs)
{
class B
{
public void shout1()
{
System.out.println(iargs);
}

}
B b=new B();
b.shout1();
}
}
class C
{
public static void main(String [] args)
{
A a=new A();
a.shout(5);
}
}这段代码...为什么要加final我觉得这个外部类的方法直接把参数传进去...执行方法...产生一个B对象调用这个shout1();就可以了.为什么非要加final?我知道一个方法中局部变量使用完之后就被释放掉了.而final定义的就超过了这个外部方法中的生命周期...但是就是搞不懂啊..能自己讲讲吗?
------------------解释如下------------------

局部内部类直接访问在其外部定义的对象(包括普通变量),编译器要求参数引用必须是final的。

其中:
1. 必须是局部内部类,显然包括匿名内部类;
2. 内部类访问外部类的对象必须是直接访问。
看下面的代码,注意a并不需要是final的:
类A1:
class A1{
   public void shout(int iargs){
     class B{
      public void shout1(int a){
      System.out.println(a);
      }
     
     }
     B b = new B();
     b.shout1(iargs);
   }
}
类A:
class A{
public static void main(String [] args) {
   A1 a = new A1();
   a.shout(5);
}
}


再看这个代码:
class A1{
   public void shout1(){
     System.out.println("hi");
   }
  
   public A1 shout(final int iargs){
    class B extends A1{
     public void shout1(){
       System.out.println(iargs);
     }    
    }
    return new B();
   }
}
类A:
class A{
   public static void main(String [] args){
    A1 a = new A1();
    C c = new C();
    c.shoutc(a.shout(5));
   }
}
类C:
class C{
   void shoutc(A1 b){
     b.shout1();
   }
}

语句 c.shoutc(a.shout(5)); 在 a.shout(5) 得到返回值后,a 的 shout()方法栈被清空了!
即iargs不存在了,而c.shoutc()却又调用了 b.shout1(); 语句去执行 System.out.println(iargs); 你不觉得很诡异吗?呵呵。

我们再来看java虚拟机是怎么实现这个诡异的访问的:有人认为这种访问时之所以能完成是因为iargs是final的,由于变量的生命周期,事实是这样的吗?方法栈都不存在了,变量即使存在,怎么可能能访问到?试想下:一个方法能访问另一个方法的定义的final局部变量吗(不通过返回值)?

研究一下这个诡异的访问执行的原理,用反射探测一下局部内部类
编译器会探测局部内部类中是否有直接使用外部定义变量的情况:如果有访问就会定义一个同类型的字段然后在构造方法中用外部变量给自己定义的字段赋值而后局部内部类所使用的变量都是自己定义的字段!当然就可以访问!见下:
class A1$1$B
{
A1$1$B(A1, int);

private final int var$iargs;
private final A1 this$0;
}
A1$1$B 类型的对象会使用var$iars变量,而不是shout()方法中的final int iargs变量,当然就可以访问了!

OK,终于到正题了,为什么是final,即使外部变量不是final,编译器也可以如此处理:自己定义一个同类型的变量,然后在构造方法中赋值就行了。

原因就是为了让我们能够挺合逻辑的直接使用外部变量,而且看起来是在始终使用 iargs 变量(而不是赋值以后的自己的字段)!

考虑出现这种情况:局部内部类使用的是外部的变量iargs,而且又对这个变量作了变值操作,如:iargs++,根据前面的分析,如果编译器允许iargs不是final的,那么,改变的是 var$iargs,而iargs并没有变!仍然是5(var$iargs才是6)。为了避免这样如此不合逻辑的事情发生:你用了外部变量,又改变了变量的值,但那个变量死活没有变化!自然的iargs被强行规定必须是final 所修饰的(让两个值永远一样,或所指向的对象永远一样,后者可能更重要)!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值