Core Java 笔记(七)- 内部类

内部类

 

内部类是定义在另一个类中的类,使用内部类有三个主要原因:

  • 访问外围类的部分私有数据(取决于作用域)

  • 对同一个包中的其他类隐藏起来

  • 通过匿名内部类定义一个回调函数

 

一、访问外部类对象状态

 

先看一个简单的例子:

public class TalkingClock {
    private int interval;
    private boolean beep;
    
    TalkingClock(int interval, boolean beep) {...}
    public void start() {...}
    
    // an inner class
    private class TimePrinter implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    ...
}

只有内部类可以是私有类,常规类只可以具有包/公有可见性。注意,虽然 TimePrinter 类位于 TalkingClock 类的内部,但并不意味着每个 TalkingClock 都有一个 TimePrinter 实例域。

这个例子其实想说明一点:TimePrinter 类中没有名为 beep 的变量,取而代之的是 beep 引用了 TalkingClock 对象的域。将这个结论推广开来就是,内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域

内部类为什么可以这么做?实际上,内部类的对象有一个隐式引用(不可见),由编译器负责在内部类的默认构造器中设置这个引用,让它指向外部类对象:

private TimePrinter(TalkingClock clock) {
    outer = clock; // outer 不是关键字,只是为了方便说明
}

当在某个方法中创建 TimePrinter 对象时,编译器会将 this 引用传递给构造器,不需要我们自己添加:

public void aMethodOfTalkingClock() {
    // ...
    ActionListener listener = new TimePrinter();
    // parameter automatically added : ActionListener listener = new TimePrinter(this); 
    // ...
}

 

二、语法

 

可以用表达式 OuterClass.this 表示外围类引用,用法如下:

public void actionPerformed(ActionEvent event) {
    ...
    if (TalkingClock.this.beep) {
        Toolkit.getDefaultToolkit().beep();
    }
}

对于公有的或包可见的内部类,可以用表达式 OuterClass.InnerClass 引用内部类,还可以通过外围类的引用在其他地方构建实例:

TalkingClock aTalkingClock = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = aTalkingClock.new TimerPrinter();

内部类中声明的所有静态域都必须是 final,这是因为,对于每个外部对象,会分别有一个单独的内部类实例,将静态域设置为 final 可以确保这些域是唯一的。

 

三、通过 javap 命令理解内部类

 

必须注意,内部类是一种编译器现象,与虚拟机无关,编译器会把内部类翻译成用美元符号($)分隔外部类名与内部类名的常规类文件。在上面举的例子中,TalkingClock 类内部的 TimePrinter 类将被翻译成类文件 TalkingClock$TimePrinter.class :

编译好了之后,可以用 javap 指令看看内部类的细节。javap 是 jdk 自带的反解析工具,作用是根据字节码文件反解析出当前类的信息,怎么用呢?先用 -help 选项获取帮助信息:

想要了解 TimePrinter 的内部细节,使用 -private 选项就可以了,结果如下:

可以看到,编译器为了引用外围类,生成了一个实例域 this$0,这个名字是自动合成的。

 

四、局部内部类

 

延续上面的例子,如果说 TimePrinter 这个类名字只在某个方法中创建其对象时使用了一次,那么就可以把 TimePrinter 类的定义放在这个方法中,成为一个局部内部类:

public void start() {
    class TimePrinter implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            System.out.println("The time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    
    ActionListener listener = new TimePrinter();
    Timer timer = new Timer(interval, listener);
    timer.start();
}

局部类不能使用访问权限修饰符,它的作用域被限定在声明这个局部类的代码块中,优势在于可以对外部世界完全隐藏起来。局部类还有另一个优势,即可以访问局部变量(前提是这些变量事实上为 final),下面是一个示例:

public void start(int interval, boolean beep) { // beep 不再是 TalkingClock 类的字段
    class TimePrinter implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            System.out.println("The time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    
    ActionListener listener = new TimePrinter();
    Timer timer = new Timer(interval, listener);
    timer.start();
}

这个方法特殊在哪里呢?来看一下控制流程:

①调用 start 方法 -> ②调用 TimePrinter 构造器,初始化 listener -> ③将 listener 引用传递给 Timer 构造器,timer 开始计时,随着 start 方法结束,beep 对应的内存被释放 -> ④actionPerformed 方法执行 if (beep) ...

看到这里,是不是觉得跟 lambda 捕获自由变量有些相似?局部类是这样做的:TimePrinter 在 beep 域释放之前对其进行了备份,现在同样用 javap 命令对 TimePrinter 的字节码文件进行解析,结果如下:

注意,这里多了一个 boolean 类型的字段 val$beep ,这是因为编译器会检测对局部变量的访问,为每一个变量建立相应的数据域,并将局部变量拷贝到 TimePrinter 的构造器中(通过 javap 无法看到构造器的实际参数,但利用反射就可以),局部变量必须事实上为 final 的意义在于,能与局部类内建立的拷贝保持一致。

 

五、匿名内部类 

 

假如我只创建局部类的一个对象,有没有继续简化的办法呢?有,不过前提是这个类必须用来实现某个接口或继承某个已存在的类,这样就可以在创建对象时只使用接口名或父类名:

public void start(int interval, boolean beep) {
    ActionListener listener = new ActionListener {
        public void actionPerformed(ActionEvent event) {
            System.out.println("...")
        }
    }
    ...
}

这种语法的含义是:创建一个实现 ActionListener 接口的类的新对象,需要实现的方法 actionPerformed 定义在括号 {} 内。一般格式为:

new SuperType(construction parameters) { // SuperType 可以是接口或类
    inner class methods and data
}

 

六、静态内部类 

 

如果只想把一个类隐藏在另一个类的内部,而不需要内部类引用外围类的对象或者进行回调,那么,就可以将内部类声明为 static ,又叫嵌套类。静态内部类的对象的特殊之处在于:

  • 没有外围类对象的引用特权(因为不一定需要创建外围类的对象)

  • 可以有自己的静态域和方法

下面来看一个例子:

public class ArrayAlg { 
    public static class Pair {  
        private double first;
        private double second;
        
        public Pair(double f, double s) {
            first = f;
            second = s;
        }
        public double getFirst() {...}
        public double getSecond() {...}
    }
    public static Pair minmax(double[] values) { // 返回 values 中最小值和最大值
        double min = Double.POSITIVE_INFINITE;
        double max = Double.NEGATIVE_INFINITE;
        for (double v : values) {
            if (min > v)  min = v;
            if (max < v)  max = v;
        }
        return new Pair(min, max);
    }
}

调用 minmax 方法得到最值:

ArrayAlg.Pair pair = ArrayAlg.minmax(arr);
System.out.println("min = " + pair.getFirst());
System.out.println("max = " + pair.getSecond());

如果没有将 Pair 类声明为 static,编译器会给出错误报告:

另外要注意:声明在接口中的内部类会自动成为 static 和 public 。

 

转载于:https://www.cnblogs.com/zzzt20/p/11530975.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值