内部类(inner class)是定义在另一个类中的类。使用内部类的原因有三:

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

    使用内部类访问对象状态

    内部类的语法比较复杂。这里举一个简单但不太实用的例子:
    构造一个语音时钟类,其中有两个实例域:发布通告的间隔和开关铃声的标志: ``` public class TalkingClock { private int interval; private boolean beep;

    public TalkingClock(int interval, boolean beep) {

    1. this.interval = interval;
    2. this.beep = beep;

    }

    public void start() {

    1. ActionListener listener = new TimePrinter();
    2. Timer timer = new Timer(interval, listener);
    3. timer.start();

    }

    // an inner class public class TimePrinter implements ActionListener {

    1. public void actionPerformed(ActionEvent event) {
    2. System.out.println("At the tone, the time is " + new Date());
    3. if (beep) Toolkit.getDefaultToolkit().beep();
    4. }

    } }

  1. 来看一下 TimerPrinter 类的详细内容:

public 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(); } }

  1. 可以看到,这里的 TimePrinter 类没有实例域或名为 beep 的变量,取而代之的是 beep 引用了创建 TimePrinter TalkingClock 对象的域。可以看到,内部类既可以访问自身的数据域,也可以访问创建它的**外围类**对象的数据域。<br />内部类(即这里的 TimePrinter)的对象中有一个隐式引用,指向创建他的外部类对象(即这里的 TalkingClock)。这个引用在内部类中的定义是不可见的。
  2. ### 内部类中的特殊语法
  3. 上述说了内部类中有一个隐式引用,他的正规语法为:

OuterClass.this

  1. 比如,TimerPrinter 内部类中的 actionPerformed 方法可以这样写:

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

  1. 反过来,在外围类中构造内部类也有正规语法:

outerObject.new InnerClass()

  1. 例如:

ActionListener listener = this.new TimePrinter();

  1. 上述是在 TalkingClock 类的内部引用内部类对象。通常 this 限定词是多余的。<br />如果 TimePrinter 是一个共有内部类,对于任意的 TalkingClock 对象,都可以构建一个 TimerPrinter

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

  1. ### 内部类是否有用、必要和安全
  2. **内部类是一种编译器现象,与虚拟机无关**。编译器将会把内部类翻译成用 $(美元符号)分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。<br />例如,在 TalkingClock 类内部的 TimePrinter 类将被翻译成类文件 TalkingClock$TimePPrinter.class。你可以试着编译 TalkingClock 类文件:

java TalkingClock.java

  1. ### 局部内部类
  2. 可以看到上述 TalkingClock 的代码中,TimePrinter 类只用了一次,可以使用局部内部类在 `start()` 方法中去重构它:

public void start() { class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println(“At the tone, the time is “ + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); } }

  1. ActionListener listener = new TimePrinter();
  2. Timer timer = new Timer(interval, listener);
  3. timer.start();

}

  1. 局部类不能用 public private 访问说明符进行声明。**它的作用域被限定在声明这个局部类的块中**。<br />局部类有一个优势,即**对外部世界可以完全地隐藏起来**。**即使 TalkingClock 类中的其他代码也不能访问它**。除start 方法之外,没有任何方法知道 TimePrinter 类的存在。
  2. ### 由外部方法访问变量
  3. 局部类不仅能够访问包含它们的外部类,还可以访问**局部变量**。不过,那些局部变量必须事实上为 final 。这说明,**它们一旦赋值就绝不会改变**。<br />例如,我们将上述 `start()` 再次重构,将 TalkingClock 构造器的参数 interval beep 移至 start 中:

public void start(int interval, boolean beep) { class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println(“At the tone, the time is “ + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); } }

  1. ActionListener listener = new TimePrinter();
  2. Timer timer = new Timer(interval, listener);
  3. timer.start();

}

  1. 可以看到,上述除了方法参数添加了两个参数,其余的没有变动。但 TalkingClock 类已经不再需要 interval beep 这两个实例域了。
  2. > 上述的 beep 会在内部类初始化时自动备份。
  3. 这样的好处是局部变量的访问非常容易,它减少了需要显式编写的实例域,从而使内部类更加简单。<br />不过,引用局部内部类的方法只可以引用定义为 final 的局部变量。你可以将 `start()` beep 参数声明为 final

public void start(int interval, final boolean beep)

  1. 但有时声明 final 不太方便。只要不去显式的改变引用的局部变量,可以不显式的声明 final。还有时 final 限制显得不太方便。假设想更新在一个封闭作用域内的计数器。这里想要统计一下在排序过程中调用 compareTo 方法的次数。

public void printComparisions() { int counter = 0; Date[] dates = new Date[100]; for (int i = 0; i < dates.length; i++) { dates[i] = new Date() { public int compareTo(Date other) { counter++; // Error return super.compareTo(other); } }; } Arrays.sort(dates); System.out.println(counter + “ comparisions”); }

  1. 这里显式的改变了 counter 的值,所以这里会报错。并且我们要更新 counter 的值,所以不能将 counter 声明为 final。由于 Integer 对象是不可变的,所以不能用 Integer 替代她。最好的方法是使用一个长度为 1 的数组:

public void getComparisions() { int[] counter = new int[1]; // this Date[] dates = new Date[100]; for (int i = 0; i < dates.length; i++) { dates[i] = new Date() { public int compareTo(Date other) { counter[0]++; return super.compareTo(other); } }; } Arrays.sort(dates); System.out.println(counter + “ comparisions”); }

  1. ### 匿名内部类
  2. 上面的局部内部类已经将内部类优化的很好了,但是还能再进一步。如果只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。

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

  1. 上述过程的解释为:创建一个实现 ActionListener 接口的类的新对象,需要实现的方法 actionPerformed 定义在括号 {} 内。<br />由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。<br />仔细看一下构造一个类的新对象与拓展那个类的匿名函数的对象之间有什么差别:

Person queen = new Person(“Mary”); // a Person object Person count = new Person(“Dracula”) {…}; // an object of an inner class extending Person

  1. 对于上述方法还可以进一步的优化,最好的方式就是上一讲谈到的 lambda 表达式。作为 Java SE 8 的新特性,可以非常好的写出简洁而又优雅的代码:

public void start(int interval, final boolean beep) { Timer timer = new Timer(interval, event -> { System.out.println(“At the tone, the time is “ + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); }); timer.start(); }

  1. 有一个名为「双括号初始化」的技巧,假设你想构造一个数组列表,并将它传递到一个方法:

ArrayList friends = new ArrayList<>(); friends.add(“Harry”); friends.add(“Tony”); invite(friends);

  1. 如果不再需要这个数组列表,最好让它作为一个匿名列表。可以这样写:

invite(new ArrayList() {{ add(“Harry”); add(“Tony”); }});

  1. 可以看到这里有两个括号:外层括号建立了 ArrayList 的一个匿名子类。内层括号则是一个对象构造块<br />当通过 `getClass()` 来打印当前类名时,可能会遇到一些阻拦。因为这个方法对于**静态方法**并不奏效。因为调用 `getClass` 时,实际上调用的是 `this.getClass()`,而静态返回没有 this。所以可以使用:

new Object() {}.getClass().getEnclosingClass(); // get class of static method

  1. `new Object(){}`会建立 Object 的一个匿名子类的一个匿名对象,getEnclosingClass 则得到其外围类,也就是包含这个静态方法的类。
  2. ### 静态内部类
  3. 使用内部类只是为了把一个类隐藏在另外一个类的内部,并**不需要内部类引用外围类对象**。为此,可以将内部类声明为 static ,以便取消产生的引用。<br />下面举一个例子:计算一个数组中的最大值和最小值。最简单的方法就是遍历一遍数组,就可以得到整个数组中的最大值和最小值了。为此,我们构建一个 Pair 对象,已让一个方法返回两个数值:

public class Pair { private double first; private double second;

  1. public Pair(double f, double s) {
  2. first = f;
  3. second = s;
  4. }
  5. public double getFirst() { return first; }
  6. public double getSecond() { return second; }

}

  1. `minmax()` 就可以返回一个 Pair 类型对象:

class ArrayAlg { private static Pair minmax(double[] values) { … return new Pair(min, max); } }

  1. 如何就可以来调用该方法:

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

  1. 当然,Pair 是一个十分大众化的名字。在大型项目中,除了定义包含一对字符串的 Pair 类之外,其他程序员也很可能使用这个名字。这样就会产生名字冲突。解决这个问题的办法是将 Pair 定义为 ArrayAlg 的**内部公有类**。此后,通过ArrayAlg.Pair访问它:

ArrayAlg.pair p = new ArrayAlg.minmax(d);

  1. 不过,Pair 对象中不需要引用任何其他的对象,为此,可以将这个内部类声明为 static

class ArrayAlg { public static class Pair { … } … }

  1. 当然,只有内部类可以声明为 static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他所有内部类完全一样。<br />这里给出静态内部类的全部代码:

package staticInnerClass;

/**

  • This program demonstrates the use of static inner classes. */ public class StaticInnerClassTest { public static void main(String[] args) { double[] d = new double[20]; for (int i = 0; i < d.length; i++)
    1. d[i] = 100 * Math.random();
    ArrayAlg.Pair p = ArrayAlg.minmax(d); System.out.println(“min = “ + p.getFirst()); System.out.println(“max = “ + p.getSecond()); } }

class ArrayAlg { /**

  1. * A pair of floating-point numbers
  2. */

public static class Pair { private double first; private double second;

  1. /**
  2. * Constructs a pair from two floating-point numbers
  3. * @param f the first number
  4. * @param s the second number
  5. */
  6. public Pair(double f, double s)
  7. {
  8. first = f;
  9. second = s;
  10. }
  11. /**
  12. * Returns the first number of the pair
  13. * @return the first number
  14. */
  15. public double getFirst()
  16. {
  17. return first;
  18. }
  19. /**
  20. * Returns the second number of the pair
  21. * @return the second number
  22. */
  23. public double getSecond()
  24. {
  25. return second;
  26. }

}

/**

  1. * Computes both the minimum and the maximum of an array
  2. * @param values an array of floating-point numbers
  3. * @return a pair whose first element is the minimum and whose second element
  4. * is the maximum
  5. */

public static Pair minmax(double[] values) { double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY; for (double v : values) { if (min > v) min = v; if (max < v) max = v; } return new Pair(min, max); } }

```