Java核心技术1-内部类

  |  

摘要: 本文是《Java核心技术 10th》中内部类的要点总结

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 Chap6 中关于【内部类】的要点总结。

在文章 Java核心技术1-接口 中,我们了解了接口;在文章 Java核心技术1-lambda表达式 中,我们了解了 lambda 表达式

本文讨论内部类(inner class)机制。内部类定义在另外一个类的内部,其中的方法可以访问包含它们的外部类的域。内部类技术主要用于设计具有相互协作关系的类集合。


内部类的引入

引入内部类的三点原因

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

关于内部类,有以下这些要点

  • 简单内部类的写法,以及如何访问外围类的实例域
  • 内部类的特殊语法规则
  • 内部类如何转换成常规类
  • 局部内部类:可以访问外围作用域的局部变量
  • 匿名内部类:在 lambda 表达式之前用于实现回调的方法
  • 如何将静态内部类嵌套在辅助类中

C++ 和 Java 关于嵌套类的区别和联系

C++ 嵌套类:一个被嵌套的类包含在外围类的作用域内。例如链表类内嵌套一个存储节点的类和定义迭代器位置的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LinkedList 
{
public:
class Iterator
{
public:
void insert(int x);
int erase();
...
};
...

private:
class Link
{
public:
Link *next;
int data;
};
...
}

嵌套是一种类之间的关系,而不是对象之间的关系。一个 LinkedList 对象并不包含 Iterator 类型或 Link 类型的子对象。

C++ 中嵌套类有两个好处:

  • 命名控制:Iterator 嵌套在 LinkedList 类的内部,所以在外部被命名为 LinkedList::Iterator,这样就不会与其他名为 Iterator 的类发生冲突。在 Java 中这个并不重要,因为 Java 包已经提供了相同的命名控制。
  • 访问控制:Link 类私有,但 Link 的数据域可以设计为公有的,这些数据域只能被 LinkedList 类中的方法访问。在 Java 中只有内部类可以实现这样的控制。

Java 内部类还有另外一个功能,这使得它比 C++ 的嵌套类更加丰富,用途更加广泛。内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针,可以访问外围类对象的全部状态。

但注意:在 Java 中 static 内部类没有这种附加指针,这样的内部类与 C++ 中的嵌套类很相似。


使用内部类访问对象状态

一般一个方法可以引用调用该方法的对象的域,而内部类既可以访问自身的数据域,也可以访问它的外围类对象的数据域。

例如下面的代码中,TimePrinter 类位于 TalkingClock 类内部,里面的 beep 引用了 TalkingClock 对象的域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TalkingClock {
private int interval;
private boolean beep;

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

public class TimePrinter {
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + new Date());
if(beep) {
...
}
}
}
}

内部类的对象总有一个隐式引用,它指向了创建它的外部类对象,如下图:

注意这个引用是不可见的,这里只是为例方便说明内部类的机制,将外围类的对象称为 outer。因此上面代码中内部类的 beep 相当于 outer.beep。

外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器,添加一个外围类引用的参数。因为 TimePrinter 类没有定义构造器,所以编译器为这个类生成了一个默认的构造器,其代码如下所示:

1
2
3
public TimePrinter(TalkingClock clock) {
outer = clock;
}

当 TalkingClock 类的方法中创建了 TimePrinter 对象后,编译器会将 this 引用传递给当前的 TimePrinter 构造器。

如果 TimePrinter 类声明为私有的,则只有 TalkingClock 的方法才能够构造 TimePrinter 对象只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性


内部类的特殊语法规则

  • 前面的外围类引用 outer,按照正规语法来写应该是下面这样:
1
OuterClass.this

这样前面代码中的 if(beep) 可以写为 if(TalkingClock.this.beep)

  • 内部对象的构造器的正规写法为:
1
outerObject.new InnerClass(...);

例如

1
TimePrinter tp = this.new TimePrinter();
  • 除了 this,还可以通过显式地命名将外围类引用设置为其他的对象(需要内部类为公有),例如
1
2
TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();
  • 在外围类作用域之外,OuterClass.InnerClass 可以引用内部类,例如上面代码。

  • 内部类中声明的所有静态域都必须是 final。原因很简单。我们希望一个静态域只有一个实例,不过对于每个外部对象,会分别有一个单独的内部类实例。如果这个域不是 final,它可能就不是唯一的。

  • 内部类不能有static方法。Java语言规范对这个限制没有做任何解释。


内部类的必要性、安全性

内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用$(美元符号)分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。

内部类可以被编译器翻译成常规类(而虚拟机对此一点也不了解),那么内部类在管理那些额外的访问特权(内部类可以访问外围类的私有数据)上就有问题了。

也就是说,如果内部类访问了私有数据域,就有可能通过附加在外围类所在包中的其他类访问它们,但这需要刻意地构建或修改类文件才有可能达到这个目的。


局部内部类

在一个方法中可以定义局部内部类

局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。局部类有一个优势,即对外部世界可以完全地隐藏起来。

也就是说除了创建局部类的方法之外,没有任何方法知道局部类的存在。

与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。但是这些局部变量需要一旦赋值就绝不会改变,相当于 final。

例子: 统计一下在排序过程中调用 compareTo 方法的次数

(1) 对于已经实现了 Comparable 接口的标准库中的类,统计比较次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Date;
import java.util.Arrays;

public class CompareToCount {
public static void main (String[] args) {
int[] counter = new int[1];

class MyDate extends Date {
public int compareTo(Date other) {
counter[0]++;
return super.compareTo(other);
}
}

Date[] dates = new Date[100];
for (int i = 0; i < dates.length; i++) {
dates[i] = new MyDate();
}
Arrays.sort(dates);
System.out.println(counter[0] + " comparisons.");
}
}

可以写成匿名类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Date;
import java.util.Arrays;

public class CompareToCount {
public static void main (String[] args) {
int[] counter = new int[1];
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[0] + " comparisons.");
}
}

(2) 对自定义类进行排序,并统计比较次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.Random;
import java.util.Arrays;

public class CompareToCount {
public static void main (String[] args) {
int[] counter = new int[1];
Random rd = new Random();

class InnerMyClass extends MyClass {
InnerMyClass () {}
InnerMyClass (int x) {
super(x);
}
public int compareTo (MyClass other) {
counter[0]++;
return super.compareTo(other);
}
}

MyClass[] objects = new MyClass[100];
for (int i = 0; i < objects.length; i++) {
objects[i] = new InnerMyClass(rd.nextInt(100));
}
Arrays.sort(objects);
System.out.println(counter[0] + " comparisons.");
}
}

class MyClass implements Comparable<MyClass> {
private int x;

MyClass () {}
MyClass (int x) {
this.x = x;
}

public int compareTo(MyClass other) {
if (x < other.x) {
return 1;
} else if (x > other.x) {
return -1;
} else {
return 0;
}
}
}

也可以写成匿名类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.Random;
import java.util.Arrays;

public class CompareToCount {
public static void main (String[] args) {
int[] counter = new int[1];
Random rd = new Random();

MyClass[] objects = new MyClass[100];
for (int i = 0; i < objects.length; i++) {
objects[i] = new MyClass(rd.nextInt(100)) {
public int compareTo (MyClass other) {
counter[0]++;
return super.compareTo(other);
}
};
}
Arrays.sort(objects);
System.out.println(counter[0] + " comparisons.");
}
}

class MyClass implements Comparable<MyClass> {
private int x;

MyClass () {}
MyClass (int x) {
this.x = x;
}

public int compareTo(MyClass other) {
if (x < other.x) {
return 1;
} else if (x > other.x) {
return -1;
} else {
return 0;
}
}
}

匿名内部类

对于局部内部类,假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。

通常语法为:

1
2
3
4
new SuperType (construction parameters) 
{
inner class methods and data
}

SuperType 可以是一个接口,此时内部类要实现这个接口、SuperType 也可以是一个类,此时内部类就要扩展它。

由于构造器名字必须与类名相同而匿名类没有类名,因此匿名类不能有构造器,取而代之的是将构造器参数传递给超类(前面提到过,可能是类也可能是接口)构造器。

双括号初始化

双括号初始化利用了内部类语法。假设你想构造一个数组列表,并将它传递到一个方法,例如

1
2
3
4
ArrayList<String> words = new ArrayList<>();
words.add("Harry");
words.add("Tone");
invite(words);

如果调用 invite 后不再需要 words 这个数组列表,那么最好让它作为一个匿名列表,代码如下:

1
invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }});

其中:

  • 外层括号建立 ArrayList 的匿名子类。
  • 内层括号是一个对象构造块,参考 Java核心技术1-对象与类 中的初始化块这个部分。

关于匿名子类的 equals

建立一个与超类大体类似(但不完全相同)的匿名子类通常会很方便。不过,对于 equals 方法要特别当心。

在文章 Java核心技术1-Object类 中,我们提到过 equals 方法最好使用以下测试:

1
2
3
if (getClass() != other.getClass()) {
return false;
}

但对于匿名子类做这个测试会失效。

在静态方法中获取当前类的类名

对于非静态方法,直接调用 getClass 即可:

1
System.out.println("Something awful happened in " + getClass());

调用 getClass 时,实际调用的是 this.getClass(),由于静态方法没有 this,因此对于静态方法中要获取当前类名需要麻烦一点,如下:

1
new Object(){}.getClass().getEnclosingClass();

其中:

  • new Object(){} 建立一个 Object 的匿名子类的一个匿名对象。
  • getEnclosingClass 获得其外围类,也就是这个静态方法的类。

静态内部类

有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。此时可以将内部类声明为 static,以便取消产生的引用。

例如:计算数组最大值和最小值问题,将最大值最小值包装成静态内部类的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class StaticInnerClassTest {
public static void main(String[] args) {
double[] d = new double[20];
for(int i = 0; i < d.length; i++) {
d[i] = 100 * Math.random();
}

ArrayAlg.Pair p = ArrayAlg.minmax(d);

System.out.println("min = " + p.getMin());
System.out.println("max = " + p.getMax());
}
}

class ArrayAlg {
public static class Pair {
private double minx;
private double maxx;

public Pair(){}
public Pair(double minx, double maxx) {
this.minx = minx;
this.maxx = maxx;
}

public double getMin() {
return minx;
}

public double getMax() {
return maxx;
}
}

public static Pair minmax(double[] values) {
double minx = Double.POSITIVE_INFINITY;
double maxx = Double.NEGATIVE_INFINITY;

for(double v: values) {
if (minx > v) {
minx = v;
}
if (maxx < v) {
maxx = v;
}
}

return new Pair(minx, maxx);
}
}

只有内部类可以声明为 static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他所有内部类完全一样。

如果内部类对象是在静态方法中构造的,那么必须用静态内部类。

声明在接口中的内部类自动成为 static 和 public 类。


Share