Java内部类、静态类、局部类详解

先看一个例子:

class TalkingClock
{
    private int interval;
    private boolean beep;
    public TalkingClock(int interval,boolean beep) {
        this.interval = interval;
        this.beep = beep;
    }

例子来自《Java核心技术》,这是一个“会叫的时钟”类,功能是每隔一段时间interval,在屏幕上打印当前时间,并且根据beep变量是否为真,决定是否发出beep的响声。可以看到在构造函数中设定interval和beep属性后,调用start()方法就可以运行了。

    public void start() {
        ActionListener listener = new TimePrinter();
        Timer timer = new Timer(interval, listener);
        timer.start();
    }
    public class TimePrinter implements ActionListener
    {
        @Override
        public void actionPerformed(ActionEvent e) {
            Date now = new Date();
            System.out.println("Now the time is :  "+now);
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }

这里的TimePrinter是一个事件监听器,同时也是TalkingClock的内部类。

内部类的访问控制

我们注意到一个细节:

if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }

这里引用了外部类的私有属性beep,说明内部类可以随意获取所在的外部类的各种public、protected、private属性、方法。这是怎么做到的呢?
实质上,内部类只是编译器提供的一个“假象”。在编译后的class文件中,内部类会被移出来,生成一个新的和外部类平行的类,名字为 外部类$内部类 ,如上面代码生成类名为 TalkingClock$TimePrinter 。
用javap 命令把这个类打印出来看一下

这里写图片描述

com.Joey是所在包名。注意红色圈出来的地方,这个奇怪的名字this$0是编译器自动生成的,指向外部类TalkingClock的一个引用。在构造函数中,给这个引用赋值,所以内部类就能够通过这个引用来获取到外部类的属性。
但是,仅有这个引用是不够的,因为这时候内部类已经和外部类处于平级的位置,所以按规定是无法访问到外部类private的属性的,怎么解决这个问题呢?

我们用javap把外部类也打印出来看一下
这里写图片描述

同样注意到红线部分,编译器新增了一个access$0的方法,传入参数为TalkingClock。正是这个静态方法的内部,把beep私有属性返回了,所以我们的语句

if(beep)

实际上编译成

if(access$0(outer))

即通过access$0方法来获取外部类的私有属性

于是我们可以知道,当内部类需要访问外部类 public 或 protected 成员时,在内部类中由编译器生成 final 类型的 this$0 字段,通过构造函数把外部类引用赋值给 this$0 ;当内部类需要访问外部类 private 成员(包括私有属性和方法),在外部类中由编译器生成 static 类型的访问器方法 access$0,通过该方法来间接访问或修改外部类的私有成员。

静态内部类

在前面的例子中,我们发现内部类有很大的特权,可以随意的访问外部类中的成员变量与成员方法。有时候我们希望限制这种特权,有一种方法就是把内部类设置为静态的,即添加static修饰符。

在C#中,static可以修饰任意一个类,称为静态类。静态类只能有静态成员,不能被实例化,通常用于作为全局共享的工具类,提供共享的属性和方法,以简化访问操作。

而java不一样,java的static只能用于修饰内部类,不能作用于其他的类。而java的静态内部类,限制只能够引用外部类中的静态成员方法或者成员变量,对于那些非静态的成员变量与成员方法,由于静态内部类中没有对外部类的引用,所以在静态内部类中是无法访问的。这就是静态内部类的最大使用限制。

静态内部类当然不仅仅只有这个优点。

我们知道,非静态内部类中不能存在静态变量,如

public class OuterClass
{
    class InnerClass
    {
        static int a = 0;
    }
}

会发生编译错误。

从语义的角度来说,内部类是依附外部类而存在的,尽管实际上编译成为两个平级的类,但是外界是通过一个外部类的实例来引用内部类的。如

OuterClass outer = new OuterClass();
OuterClass InnerClass = outer.new InnerClass();

而静态变量的特点属于类而不属于某一个实例,所以从这个角度来说,非静态内部类的如果存在一个静态变量,则说明这个变量可以通过 OuterClass.InnerClass.静态变量 这样的语句来直接访问,这就和内部类的定义矛盾了。

但是可以存在final修饰的静态常量,如

public class OuterClass
{
    class InnerClass
    {
        static final int a = 0;
    }
}

不会编译出错,这是为什么呢?

从编译器的角度,可以很容易的找到答案。static final 修饰的是 静态常量 也就是在编译期间就可以确定的常量。在编译期间,编译器有一个很重要的优化手段就是常量优化。也就是说在编译期间能确定的常量,会放在类文件的常量区。在初始化类的时候,从常量区中取出并赋值给 static final 变量。

一旦把内部类设置成静态的,那么就可以在内部类中定义静态变量了,此时意味着可以在外界通过 OuterClass.InnerClass.静态变量来访问 ,也意味着这个静态类不再依附某一个特定的外部类实例,可以这样来创建:

OuterClass.InnerClass aClass = new InnerClass();

跟一般的类不同,静态内部类生成的实例只有一个,也就是说就算生成多次,也会是多个引用执行同一个实例,如:

class OuterClass
{
    static class InnerClass
    {
        static  int n= 1;
    }
    public static void main(String[] args) {
        OuterClass.InnerClass aClass = new InnerClass();
        OuterClass.InnerClass bClass = new InnerClass();
        bClass.n = 2;
        System.out.println(aClass.n);
    }
}

结果输出 2,证明两个引用指向同一个实例。这个特性使得当使用多个外部类的对象可以共享同一个内部类的对象。

总的来说,静态内部类牺牲了普通内部类访问外部类私有属性的特权,换来更高的自由度和只有一个实例(静态的本意就是全局共享且唯一)的能力。静态内部类和对应的外部类仅有名义上的从属关系,没有共生死的关系。

局部内部类

所谓局部内部类,就是把内部类用 { } 封装起来。很多资料书上给出的解释是定义在方法体内的类(当然同时也是内部类),叫做局部内部类,其实不尽然。在类的初始代码块 { } 以及静态代码块 static { } 中定义的内部类,其表现和定义在方法中一样。由于在类加载期间,static { } 中的代码和静态属性赋值代码会合并生成一个 < clinit >()方法,初始代码块{ }会合并进入实例构造器,所以从这个角度上来说,把定义在方法体内的类称为局部内部类不无道理。

由于被限定在方法体内,局部内部类相比普通内部类有了更大的局限性。就可见性而言,只有方法体内才能知道它的存在,出了方法体,哪怕是在外部类中都看不到它,因此局部内部类没有public等修饰符,对于外界而言完全是隐藏起来的。

相比普通内部类,局部内部类还有一个优势,就是访问其所在方法的局部变量。但是有个限制,访问的局部变量必须显示或隐式地设置成 final 类型。如

class OuterClass 
{
    {
        int a = 0;
        class InnerClass {
            public InnerClass() {
                System.out.println(a);
            }
        }
        new InnerClass();       
    }

这段代码中,InnerClass定义在初始化代码块中,构造函数试图打印一个局部变量a的值。当然就这段代码而言是没有问题的,即便没有显示地设置a 为 final 类型。但是当试图修改 a 的值时,如

class OuterClass 
{
    {
        int a = 0;
        class InnerClass {
            public InnerClass() {
                System.out.println(a);
            }
        }
        a++;
        new InnerClass();       
    }

此时会报错:Local variable a defined in an enclosing scope must be final or effectively final

也就是说此时 a 必须为 final类型,也就不能被修改

为什么会有这样奇怪的设定呢?其实这是由于生命周期不同而做出的妥协。在方法体中,局部变量的生命周期是随着方法的结束而结束的,但是局部类不会随方法体结束而成为垃圾。一旦还有对局部类的引用,那么它将不会被垃圾收集器回收。所以会出现这样一种情况:局部类试图访问一个已经随着方法结束而灭亡的局部变量,这种行为是不允许的。

然而局部类可以访问外围方法的局部变量这个要求却是很合理的。为了解决这个矛盾,Java语言设计人员采取了复制变量的方法,即在局部类中由编译器生成一个属性,把局部变量赋值给该属性,从而把所有对局部变量的引用转化为对局部类的属性的引用。

我们对上面的代码class文件反编译一下看看:

这里写图片描述

不知道为什么,用javap查看的局部类中没有出现复制生成的属性,但是我们可以从构造器中看到,传入的参数一个是对外部类的引用,一个是外围方法局部变量的值,所以Java的确是通过复制值的方式实现的。

通过复制的方式可以实现访问局部变量,但是存在一个很大的问题——变量一致性问题。试想由于局部类的值是在构造函数中传入的,在赋值之后如果局部变量改变了,会导致复制后的值和原来值不一致,这样就做不到“访问外围方法局部变量”的初衷了。所以Java设计人员采用一种比较简单粗暴的做法,强制访问的局部变量必须是 final 类型,这样由于值不可修改,也就避免了一致性问题。

然而这样的一刀切的做法有时候会带来不便。为什么不能在局部类中再设置get和set方法,实时同步复制的值和局部变量的值呢?我们不得而知,当然在绝大多数的情况下设置成 final 类型已经足够了,如果真的有需求改变局部变量的值,可以通过一些变通的办法,如

class OuterClass 
{
    {
        final int[] a = {0};
        class InnerClass {
            public InnerClass() {
                a[0]++;
                System.out.println(a[0]);
            }
        }
        new InnerClass();       
        System.out.println(a[0]);
    }

我们指定一个 final 类型的数组,数组中只有一个元素,这样就可以绕开Java的错误提示,因为 final 作用于对象时指的是这个引用不可修改,而引用的变量可以修改,所以我们在局部类的内部就可以对 a[0] 元素进行任意的访问和修改,结果会同步到外围方法的局部变量中。这个例子输出结果是两个 1

使用这样的技巧时候要特别小心,因为注意到局部类的值是在对象生成时通过构造函数传入的。所以一旦对象生成,局部变量的值就反映不到局部类中去了,如

class OuterClass 
{
    {
        final int[] a = {0};
        class InnerClass {
            public InnerClass() {
                System.out.println(a[0]);
            }
        }
        a[0]++;
        new InnerClass();   
        System.out.println(a[0]);
    }

这样局部类的值为 1 ,如果把

a[0]++;
new InnerClass();

改为

new InnerClass();
a[0]++;

则局部类的值仍然为 0

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值