Java内部类

内部类

使用内部类的主要原因:(看到后面返回理解)

  • 内部类方法可以访问该类定义所在作用域中的数据,包括私有数据
  • 内部类可以对同一个包中的其他类隐藏起来
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较方便

使用内部类访问对象状态

让我们看一个简单的内部类

public class TalkingClock {

  private int interval;
  private boolean beep;

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

  public void start() {
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
  }

  // an inner class
  public class TimePrinter implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
      System.out.println("At the tone, the time is " + new Date());
      if (beep) {
        Toolkit.getDefaultToolkit().beep();
      }
    }
  }
}

TimePrinter 是一个内部类,它可以访问TalkingClock的实例域beep

所以,内部类既可以访问自身的数据域,也可以访问创建它的外围对象的数据域。

内部类的对象总有一个隐式引用,它指向了创建它的外部类对象;这个引用在内部类定义中是不可见的;

外围类的引用在构造器中设置,编译器修改了所有内部类的构造器,添加一个外围类的引用参数;

就如上面例子,TimePrinter类没有定义构造器,所以编译器为这个类生成一个默认的构造器,样例如下:

public TimePrinter(TalkingClock clock) {
  outer = clock;
}

其中的outer就是内部类对外围类对象的引用(outer 不是 java 中的关键字,这里只是为了方便表示)

这里假如将TimePrinter设置为private,则只有TalkingClock的方法才能构造该内部类对象

只有内部类可以是私有类

内部类的特殊语法规则

使用外围类引用的正规语法如下:

OuterClass.this

如下面的TalkingClock.this.beep

public class TimePrinter implements ActionListener {

  @Override
  public void actionPerformed(ActionEvent e) {
    System.out.println("At the tone, the time is " + new Date());
    if (TalkingClock.this.beep) {
      Toolkit.getDefaultToolkit().beep();
    }
  }
}

可以这样编写内部对象的构造器:

outerObject.new InnerClass(construction parameters)

 ActionListener listener = this.new TimePrinter();
 ActionListener listener = TalkingClock.this.new TimePrinter();

内部类是否有用、必要和安全

内部类是一种编译器现象,与虚拟机无关。编译器会把内部类翻译成用$分隔外部类名和内部类名的常规类文件。

如上面例子中的TimePrinter类将被翻译成类文件TalkingClock$TimePrinter.class

为了说明上面的这种情况,做个小例子:

❯ javap -private TalkingClock\$TimePrinter
Compiled from "TalkingClock.java"
public class TalkingClock$TimePrinter implements java.awt.event.ActionListener {
  final TalkingClock this$0;
  public TalkingClock$TimePrinter(TalkingClock);
  public void actionPerformed(java.awt.event.ActionEvent);
}

可以看到编译器为了让内部类引用外围类,生成了一个附加的实例域this$0

那么内部类又是如何拥有特权来访问外围类的私有域呢?

我们继续使用反射查看TalkingClock

❯ javap -private TalkingClock
Compiled from "TalkingClock.java"
public class TalkingClock {
  private int interval;
  private boolean beep;
  public TalkingClock(int, boolean);
  public void start();
  static boolean access$000(TalkingClock);
}

编译器在外围类中添加了access$000,它将作为返回参数传递给它的对象域beep

局部内部类(局部类)

在上述例子中,TimePrinter这个类名只在start中创建这个类的对象时用过

当遇到这个情况时,可以在一个方法中定义局部类

public void start() {
  class TimePrinter implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
      System.out.println("At the tone, the time is " + new Date());
      if (beep) {
        Toolkit.getDefaultToolkit().beep();
      }
    }
  }
  ActionListener listener = new TimePrinter();
  Timer t = new Timer(interval, listener);
  t.start();
}

局部类不能用 public 和 private 访问说明符进行声明,它的作用域被限定在这个局部块中,除了 start 方法外没有任何方法知道这个类的存在

由外部方法访问变量

与其他内部类相比,局部类还有一个优点,他们不仅能够访问包含它们的外部类,还可以访问局部变量,不过局部变量必须事实上为final

下面是一个典型的例子,这里将TalkingClock构造器参数中的intervalbeep移至start(...)方法中

public void start(int interval, boolean beep) {
  class TimePrinter implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
      System.out.println("At the tone, the time is " + new Date());
      if (beep) {
        Toolkit.getDefaultToolkit().beep();
      }
    }
  }
  ActionListener listener = new TimePrinter();
  Timer t = new Timer(interval, listener);
  t.start();
}

这里分析下控制流程:

  1. 调用 start 方法,传入两个参数
  2. 调用局部类TimePrinter的构造器,为了初始化对象listener
  3. listener引用传递给Timer构造器,定时器开始计时,start 方法结束,这个时候局部参数变量 beep将被销毁
  4. 然后actionPerformed方法执行if(beep)...,按道理这个 beep 参数变量已经被销毁了,应该会调用失败

但是呢,为了能够让actionPerformed方法工作,局部类TimePrinter在 beep 域被释放之前将 beep 域进行了备份,具体证据如下:

❯ javap -private TalkingClock\$1TimePrinter
Compiled from "TalkingClock.java"
class TalkingClock$1TimePrinter implements java.awt.event.ActionListener {
  final boolean val$beep;
  final TalkingClock this$0;
  TalkingClock$1TimePrinter();
  public void actionPerformed(java.awt.event.ActionEvent);
}

前面提到,局部类的方法只能引用定义为 final 的局部变量,可在上面反射的结果中看到 beep 也是 final 格式

public void start(int interval, final boolean beep) {...}

匿名内部类(*)

将局部类再深入一步,假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类

public void start(int interval, final boolean beep) {
  ActionListener listener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      System.out.println("At the tone, the time is " + new Date());
      if (beep) {
        Toolkit.getDefaultToolkit().beep();
      }
    }
  };
  Timer t = new Timer(interval, listener);
  t.start();
}

它的含义是:创建一个实现ActionListener接口的类的新对象,需要实现的方法actionPerformed定义在{}中。

通常的语法格式为:

// 匿名类通常格式
new SuperType(construction parameters)
{
  inner class methods and data
}

因为匿名类没有类名,所以匿名类不能有构造器,取而代之是将构造器参数传给超类构造器;在内部类实现接口时,不能有任何构造参数。

对比一下构造一个类的新对象与构造一个扩展那个类的匿名内部类的对象之间差别

Person queen = new Person("Mary", 12);
Person count = new Person("Dracula", 13) {...};

注 1: 双括号初始化

下面有个技巧叫“双括号初始化”, 假设你想创建一个数组列表,并将它传递到一个方法,可以用双括号初始化法,如下:

printAll(new ArrayList<String>() {
  {
    add("tom");
    add("jerry");
  }
});

这里的外层括号代表这是 ArrayList 的匿名子类,内层的是一个初始化块(第四章:在构建对象时先运行初始化块里的代码,再运行相应的构造器)

注 2: 静态方法中打印当前类的类名

因为静态方法中无法使用 this,所以不能用this.getClass()

可以用如下方法打印类名:

public static void getClassName() {
  System.out.println(new Object(){}.getClass().getEnclosingClass());
}

在这里new Object(){}会建立 Object 的一个匿名子类的一个匿名对象,getEnclosingClass则得到这个匿名类的外围类,也就是包含这个静态方法的类。

看到最后,附上大神对匿名类使用上的建议:

如果满足以下条件,用匿名内部类是比较合适的:

  • 只用到类的一个实例。
  • 类在定义后马上用到。
  • 类非常小(4 行以下)
  • 给类命名并不会导致你的代码更容易被理解。

在使用匿名内部类时,要记住以下几个原则:

  1. 匿名内部类一般不能有构造方法。
  2. 匿名内部类不能定义任何静态成员、方法和类。
  3. 匿名内部类不能是 public,protected,private,static。
  4. 只能创建匿名内部类的一个实例。
  5. 一个匿名内部类一定是在 new 的后面,用其隐含实现一个接口或实现一个类。
  6. 因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。

静态内部类

在内部类不需要访问外围对象的时候,应该使用静态内部类

与常规内部类不同,静态内部类可以有静态域和方法


文章作者: 玄霄
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 玄霄 !
评论
 上一篇
Tor Hidden Service原理初探 Tor Hidden Service原理初探
Tor利用Hidden Service协议中提供的"约会节点"来为除服务提供者之外的其他Tor用户访问被隐藏的服务
2019-08-28
下一篇 
利用ss和redsocks转发流量思路分析 利用ss和redsocks转发流量思路分析
0. 前言最近做的项目有一个需求: 项目开发的代理工具 A,需要开发一个相应的代理客户端接收用户的HTTP/SOCKS5流量并转发给服务器上的 A 服务端 由于时间因素,开发客户端太过麻烦,所以我思考了一种迂回实现用户代理的思路
2019-08-02
  目录