mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-24 06:53:27 +08:00
@@ -30,7 +30,7 @@
|
||||
- [ ] [第八章 复用](docs/book/08-Reuse.md)
|
||||
- [ ] [第九章 多态](docs/book/09-Polymorphism.md)
|
||||
- [ ] [第十章 接口](docs/book/10-Interfaces.md)
|
||||
- [ ] [第十一章 内部类](docs/book/11-Inner-Classes.md)
|
||||
- [x] [第十一章 内部类](docs/book/11-Inner-Classes.md)
|
||||
- [ ] [第十二章 集合](docs/book/12-Collections.md)
|
||||
- [x] [第十三章 函数式编程](docs/book/13-Functional-Programming.md)
|
||||
- [x] [第十四章 流式编程](docs/book/14-Streams.md)
|
||||
|
||||
@@ -407,9 +407,9 @@ public class Parcel7 {
|
||||
}
|
||||
```
|
||||
|
||||
contents()方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个Contents对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。
|
||||
contents() 方法将返回值的生成与表示这个返回值的类的定义结合在一起!另外,这个类是匿名的,它没有名字。更糟的是,看起来似乎是你正要创建一个 Contents 对象。但是然后(在到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。
|
||||
|
||||
这种奇怪的语法指的是:“创建一个继承自Contents的匿名类的对象。”通过new表达式返回的引用被自动向上转型为对Contents的引用。上述匿名内部类的语法是下述形式的简化形式:
|
||||
这种奇怪的语法指的是:“创建一个继承自 Contents 的匿名类的对象。”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。上述匿名内部类的语法是下述形式的简化形式:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel7b.java
|
||||
@@ -430,7 +430,7 @@ public class Parcel7b {
|
||||
}
|
||||
```
|
||||
|
||||
在这个匿名内部类中,使用了默认的构造器来生成Contents。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:
|
||||
在这个匿名内部类中,使用了默认的构造器来生成 Contents。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel8.java
|
||||
@@ -455,7 +455,7 @@ public class Parcel8 {
|
||||
- \[1\] 将合适的参数传递给基类的构造器。
|
||||
- \[2\] 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。
|
||||
|
||||
尽管Wrapping只是一个具有具体实现的普通类,但它还是被共导出类当作公共“接口”来使用。
|
||||
尽管 Wrapping 只是一个具有具体实现的普通类,但它还是被共导出类当作公共“接口”来使用。
|
||||
|
||||
```java
|
||||
// innerclasses/Wrapping.java
|
||||
@@ -466,7 +466,7 @@ public class Wrapping {
|
||||
}
|
||||
```
|
||||
|
||||
为了多样性,Wrapping拥有一个要求传递一个参数的构造器。
|
||||
为了多样性,Wrapping 拥有一个要求传递一个参数的构造器。
|
||||
|
||||
在匿名类中定义字段时,还能够对其执行初始化操作:
|
||||
|
||||
@@ -489,7 +489,7 @@ public class Parcel9 {
|
||||
}
|
||||
```
|
||||
|
||||
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的,就像你在destination()的参数中看到的那样。如果你忘记了,将会得到一个编译时错误消息。
|
||||
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的,就像你在 destination() 的参数中看到的那样。如果你忘记了,将会得到一个编译时错误消息。
|
||||
|
||||
如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似勾造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:
|
||||
|
||||
@@ -528,9 +528,9 @@ Inside instance initializer
|
||||
In anonymous f()
|
||||
```
|
||||
|
||||
在此例中,不要求变量一定是final的。因为被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。
|
||||
在此例中,不要求变量一定是 final 的。因为被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。
|
||||
|
||||
下例是带实例初始化的"parcel"形式。注意destination() 的参数必须是 final 的,因为它们是在匿名类内部使用的。
|
||||
下例是带实例初始化的"parcel"形式。注意 destination() 的参数必须是 final 的,因为它们是在匿名类内部使用的。
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel10.java
|
||||
@@ -565,7 +565,7 @@ public class Parcel10 {
|
||||
Over budget!
|
||||
```
|
||||
|
||||
在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是if语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。
|
||||
在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 if 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。
|
||||
|
||||
匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
|
||||
|
||||
@@ -573,12 +573,12 @@ Over budget!
|
||||
|
||||
## 嵌套类
|
||||
|
||||
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static,这通常称为嵌套类。想要理解static应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是static的时,就不是这样了。嵌套类意味着:
|
||||
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类。想要理解 static 应用于内部类时的含义,就必须记住,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而,当内部类是 static 的时,就不是这样了。嵌套类意味着:
|
||||
|
||||
1. 要创建嵌套类的对象,并不需要其外围类的对象。
|
||||
2. 不能从嵌套类的对象中访问非静态的外围类对象。
|
||||
|
||||
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有static数据和static字段,也不能包含嵌套类。但是嵌套类类可以包含所有这些东西:
|
||||
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类。但是嵌套类类可以包含所有这些东西:
|
||||
|
||||
```java
|
||||
// innerclasses/Parcel11.java
|
||||
@@ -618,13 +618,13 @@ public class Parcel11 {
|
||||
}
|
||||
```
|
||||
|
||||
在main()中,没有任何Parcell1的对象是必需的;而是使用选取static成员的普通语法来调用方法-这些方法返回对Contents和Destination的引用。
|
||||
在 main() 中,没有任何 Parcell1 的对象是必需的;而是使用选取 static 成员的普通语法来调用方法-这些方法返回对 Contents 和 Destination 的引用。
|
||||
|
||||
就像你在本章前面看到的那样,在一个普通的(非static)内部类中,通过一个特殊的this引用可以链接到其外围类对象。嵌套类就没有这个特殊的this引用,这使得它类似于一个static方法。
|
||||
就像你在本章前面看到的那样,在一个普通的(非 static)内部类中,通过一个特殊的 this 引用可以链接到其外围类对象。嵌套类就没有这个特殊的 this 引用,这使得它类似于一个 static 方法。
|
||||
|
||||
### 接口内部的类
|
||||
|
||||
嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是public和static的。因为类是static的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:
|
||||
嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是 public 和 static 的。因为类是 static 的,只是将嵌套类置于接口的命名空间内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:
|
||||
|
||||
```java
|
||||
// innerclasses/ClassInInterface.java
|
||||
@@ -651,7 +651,7 @@ Howdy!
|
||||
|
||||
如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的铁套类会显得很方便。
|
||||
|
||||
我曾在本书中建议过,在每个类中都写一个main()方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已编译过的额外代码。如果这对你是个麻烦,那就可以使用嵌套类来放置测试代码。
|
||||
我曾在本书中建议过,在每个类中都写一个 main() 方法,用来测试这个类。这样做有一个缺点,那就是必须带着那些已编译过的额外代码。如果这对你是个麻烦,那就可以使用嵌套类来放置测试代码。
|
||||
|
||||
```java
|
||||
// innerclasses/TestBed.java
|
||||
@@ -704,13 +704,13 @@ public class MultiNestingAccess {
|
||||
}
|
||||
```
|
||||
|
||||
可以看到在MNA.A.B中,调用方法g()和f()不需要任何条件(即使它们被定义为private)。这个例子同时展示了如何从不同的类里创建多层嵌套的内部类对象的基本语法。".new"语法能产生正确的作用域,所以不必在调用构造器时限定类名。
|
||||
可以看到在 MNA.A.B 中,调用方法 g() 和 f() 不需要任何条件(即使它们被定义为 private)。这个例子同时展示了如何从不同的类里创建多层嵌套的内部类对象的基本语法。".new"语法能产生正确的作用域,所以不必在调用构造器时限定类名。
|
||||
|
||||
<!-- Why Inner Classes? -->
|
||||
|
||||
## 为什么需要内部类
|
||||
|
||||
至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能同答“为什么需要内部类”这个问题。那么,Sun公司为什么会如此费心地增加这项基本的语言特性呢?
|
||||
至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能同答“为什么需要内部类”这个问题。那么,Sun 公司为什么会如此费心地增加这项基本的语言特性呢?
|
||||
|
||||
一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。
|
||||
|
||||
@@ -762,15 +762,15 @@ public class MultiInterfaces {
|
||||
3. 创建内部类对象的时刻并不依赖于外围类对象的创建
|
||||
4. 内部类并没有令人迷惑的"is-a”关系,它就是一个独立的实体。
|
||||
|
||||
举个例子,如果Sequence.java不使用内部类,就必须声明"Sequence是一个Selector",对于某个特定的Sequence只能有一个Selector,然而使用内部类很容易就能拥有另一个方法reverseSelector(),用它来生成一个反方向遍历序列的Selector,只有内部类才有这种灵活性。
|
||||
举个例子,如果 Sequence.java 不使用内部类,就必须声明"Sequence 是一个 Selector",对于某个特定的 Sequence 只能有一个 Selector,然而使用内部类很容易就能拥有另一个方法 reverseSelector(),用它来生成一个反方向遍历序列的 Selector,只有内部类才有这种灵活性。
|
||||
|
||||
### 闭包与回调
|
||||
|
||||
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。
|
||||
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员。
|
||||
|
||||
在 Java 8 之前,生成闭包行为的唯一方式就是内部类。在 Java 8 之后,我们可以使用 lambda 来生成闭包行为,并且语法更加精细和简洁;你将会在 [函数式编程]() 这一章节中学习相关细节。即使应该优先使用 lambda 表达式用于内部类闭包,你依旧会看到那些 Java 8 以前的代码,即使用内部类来表示闭包的方式,所以非常有必要来理解这种形式。
|
||||
在 Java 8 之前,生成闭包行为的唯一方式就是内部类。在 Java 8 之后,我们可以使用 lambda 来生成闭包行为,并且语法更加精细和简洁;你将会在 [函数式编程 ]() 这一章节中学习相关细节。即使应该优先使用 lambda 表达式用于内部类闭包,你依旧会看到那些 Java 8 以前的代码,即使用内部类来表示闭包的方式,所以非常有必要来理解这种形式。
|
||||
|
||||
Java最引人争议的问题之一就是,人们认为Java应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。稍后将会看到这是一个非常有用的概念。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针。然而,读者应该已经了解到,Java更小心仔细,所以没有在语言中包括指针。
|
||||
Java 最引人争议的问题之一就是,人们认为 Java 应该包含某种类似指针的机制,以允许回调(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。稍后将会看到这是一个非常有用的概念。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针。然而,读者应该已经了解到,Java 更小心仔细,所以没有在语言中包括指针。
|
||||
|
||||
通过内部类提供闭包的功能是优良的解决方案,它比指针更灵活、更安全。见下例:
|
||||
|
||||
@@ -855,14 +855,14 @@ Other operation
|
||||
3
|
||||
```
|
||||
|
||||
这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言,Calleel是简单的解决方式。Callee2继承自MyIncrement,后者已经有了一个不同的increment()方法,并且与Incrementable接口期望的increment()方法完全不相关。所以如果Callee2继承了MyIncrement,就不能为了Incrementable的用途而覆盖increment()方法,于是只能使用内部类独立地实现Incrementable,还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。
|
||||
这个例子进一步展示了外围类实现一个接口与内部类实现此接口之间的区别。就代码而言,Calleel 是简单的解决方式。Callee2 继承自 MyIncrement,后者已经有了一个不同的 increment() 方法,并且与 Incrementable 接口期望的 increment() 方法完全不相关。所以如果 Callee2 继承了 MyIncrement,就不能为了 Incrementable 的用途而覆盖 increment() 方法,于是只能使用内部类独立地实现 Incrementable,还要注意,当创建了一个内部类时,并没有在外围类的接口中添加东西,也没有修改外围类的接口。
|
||||
|
||||
注意,在Callee2中除了getCallbackReference()以外,其他成员都是private的。要想建立与外部世界的任何连接,interface Incrementable都是必需的。在这里可以看到,interface是如何允许接口与接口的实现完全独立的。
|
||||
内部类Closure实现了Incrementable,以提供一个返回Callee2的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此Incrementable的引用,都只能调用increment(),除此之外没有其他功能(不像指针那样,允许你做很多事情)。
|
||||
注意,在 Callee2 中除了 getCallbackReference() 以外,其他成员都是 private 的。要想建立与外部世界的任何连接,interface Incrementable 都是必需的。在这里可以看到,interface 是如何允许接口与接口的实现完全独立的。
|
||||
内部类 Closure 实现了 Incrementable,以提供一个返回 Callee2 的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此 Incrementable 的引用,都只能调用 increment(),除此之外没有其他功能(不像指针那样,允许你做很多事情)。
|
||||
|
||||
Caller的构造器需要一个Incrementable的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,Caller对象可以使用此引用回调Callee类。
|
||||
Caller 的构造器需要一个 Incrementable 的引用作为参数(虽然可以在任意时刻捕获回调引用),然后在以后的某个时刻,Caller 对象可以使用此引用回调 Callee 类。
|
||||
|
||||
回调的价值在于它的灵活性-可以在运行时动态地决定需要调用什么方法。这样做的好处在第22章可以看得更明显,在那里实现GUI功能的时候,到处都用到了回调。
|
||||
回调的价值在于它的灵活性-可以在运行时动态地决定需要调用什么方法。这样做的好处在第 22 章可以看得更明显,在那里实现 GUI 功能的时候,到处都用到了回调。
|
||||
|
||||
### 内部类与控制框架
|
||||
|
||||
@@ -870,9 +870,9 @@ Caller的构造器需要一个Incrementable的引用作为参数(虽然可以
|
||||
|
||||
应用程序框架(application framework)就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决你的特定问题。这是设计模式中模板方法的一个例子,模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。
|
||||
|
||||
控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。在第22章将会看到,Java Swing库就是一个控制框架,它优雅地解决了GUI的问题,并使用了大量的内部类。
|
||||
控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。在第 22 章将会看到,Java Swing 库就是一个控制框架,它优雅地解决了 GUI 的问题,并使用了大量的内部类。
|
||||
|
||||
要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。接下来的问题就是,对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的action()部分时,通过继承来提供的。
|
||||
要理解内部类是如何允许简单的创建过程以及如何使用控制框架的,请考虑这样一个控制框架,它的工作就是在事件“就绪”的时候执行事件。虽然“就绪”可以指任何事,但在本例中是指基于时间触发的事件。接下来的问题就是,对于要控制什么,控制框架并不包含任何具体的信息。那些信息是在实现算法的 action() 部分时,通过继承来提供的。
|
||||
|
||||
首先,接口描述了要控制的事件。因为其默认的行为是基于时间去执行控制,所以使用抽象类代替实际的接口。下面的例子包含了某些实现:
|
||||
|
||||
@@ -898,11 +898,11 @@ public abstract class Event {
|
||||
}
|
||||
```
|
||||
|
||||
当希望运行Event并随后调用start()时,那么构造器就会捕获(从对象创建的时刻开始的)时间,此时间是这样得来的:start()获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。start()是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用Event对象。例如,如果想要重复一个事件,只需简单地在action()中调用start()方法。
|
||||
当希望运行 Event 并随后调用 start() 时,那么构造器就会捕获(从对象创建的时刻开始的)时间,此时间是这样得来的:start() 获取当前时间,然后加上一个延迟时间,这样生成触发事件的时间。start() 是一个独立的方法,而没有包含在构造器内,因为这样就可以在事件运行以后重新启动计时器,也就是能够重复使用 Event 对象。例如,如果想要重复一个事件,只需简单地在 action() 中调用 start() 方法。
|
||||
|
||||
ready()告诉你何时可以运行action()方法了。当然,可以在导出类中覆盖ready()方法,使得Event能够基于时间以外的其他因素而触发。
|
||||
ready() 告诉你何时可以运行 action() 方法了。当然,可以在导出类中覆盖 ready() 方法,使得 Event 能够基于时间以外的其他因素而触发。
|
||||
|
||||
下面的文件包含了一个用来管理并触发事件的实际控制框架。Event对象被保存在List\<Event\>类型(读作“Event的列表”)的容器对象中,容器会在 [集合]() 中详细介绍。目前读者只需要知道add()方法用来将一个Object添加到List的尾端,size()方法用来得到List中元素的个数,foreach语法用来连续获联List中的Event,remove()方法用来从List中移除指定的Event。
|
||||
下面的文件包含了一个用来管理并触发事件的实际控制框架。Event 对象被保存在 List\<Event\> 类型(读作“Event 的列表”)的容器对象中,容器会在 [集合 ]() 中详细介绍。目前读者只需要知道 add() 方法用来将一个 Object 添加到 List 的尾端,size() 方法用来得到 List 中元素的个数,foreach 语法用来连续获联 List 中的 Event,remove() 方法用来从 List 中移除指定的 Event。
|
||||
|
||||
```java
|
||||
// innerclasses/controller/Controller.java
|
||||
@@ -927,18 +927,18 @@ public class Controller {
|
||||
}
|
||||
```
|
||||
|
||||
run() 方法循环遍历eventList,寻找就绪的(ready())、要运行的Event对象。对找到的每一个就绪的(ready())事件,使用对象的toString()打印其信息,调用其action()方法,然后从队列中移除此Event。
|
||||
run() 方法循环遍历 eventList,寻找就绪的(ready())、要运行的 Event 对象。对找到的每一个就绪的(ready())事件,使用对象的 toString() 打印其信息,调用其 action() 方法,然后从队列中移除此 Event。
|
||||
|
||||
注意,在目前的设计中你并不知道Event到底做了什么。这正是此设计的关键所在,"使变化的事物与不变的事物相互分离”。用我的话说,“变化向量”就是各种不同的Event对象所具有的不同行为,而你通过创建不同的Event子类来表现不同的行为。
|
||||
注意,在目前的设计中你并不知道 Event 到底做了什么。这正是此设计的关键所在,"使变化的事物与不变的事物相互分离”。用我的话说,“变化向量”就是各种不同的 Event 对象所具有的不同行为,而你通过创建不同的 Event 子类来表现不同的行为。
|
||||
|
||||
这正是内部类要做的事情,内部类允许:
|
||||
|
||||
1. 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的action()。
|
||||
1. 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的 action()。
|
||||
2. 内部类能够很容易地访问外围类的任意成员,所以可以避免这种实现变得笨拙。如果没有这种能力,代码将变得令人讨厌,以至于你肯定会选择别的方法。
|
||||
|
||||
考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光、水、温度调节器的开关,以及响铃和重新启动系统,每个行为都是完全不同的。控制框架的设计使得分离这些不同的代码变得非常容易。使用内部类,可以在单一的类里面产生对同一个基类Event的多种导出版本。对于温室系统的每一种行为,都继承一个新的Event内部类,并在要实现的action() 中编写控制代码。
|
||||
考虑此控制框架的一个特定实现,如控制温室的运作:控制灯光、水、温度调节器的开关,以及响铃和重新启动系统,每个行为都是完全不同的。控制框架的设计使得分离这些不同的代码变得非常容易。使用内部类,可以在单一的类里面产生对同一个基类 Event 的多种导出版本。对于温室系统的每一种行为,都继承一个新的 Event 内部类,并在要实现的 action() 中编写控制代码。
|
||||
|
||||
作为典型的应用程序框架,GreenhouseControls类继承自Controller:
|
||||
作为典型的应用程序框架,GreenhouseControls 类继承自 Controller:
|
||||
|
||||
```java
|
||||
// innerclasses/GreenhouseControls.java
|
||||
@@ -1089,13 +1089,13 @@ public class GreenhouseControls extends Controller {
|
||||
}
|
||||
```
|
||||
|
||||
注意,light,water和thermostat都属于外围类GreenhouseControls,而这些内部类能够自由地访问那些字段,无需限定条件或特殊许可。而且,action0方法通常都涉及对某种硬件的控制。
|
||||
注意,light,water 和 thermostat 都属于外围类 GreenhouseControls,而这些内部类能够自由地访问那些字段,无需限定条件或特殊许可。而且,action0 方法通常都涉及对某种硬件的控制。
|
||||
|
||||
大多数Event类看起来都很相似,但是Bell和Restart则比较特别。Bell控制响铃,然后在事件列表中增加一个Bell对象,于是过一会儿它可以再次响铃。读者可能注意到了内部类是多么像多重继承:Bell和Restart有Event的所有方法,并且似乎也拥有外围类GreenhouseContrlos的所有方法。
|
||||
大多数 Event 类看起来都很相似,但是 Bell 和 Restart 则比较特别。Bell 控制响铃,然后在事件列表中增加一个 Bell 对象,于是过一会儿它可以再次响铃。读者可能注意到了内部类是多么像多重继承:Bell 和 Restart 有 Event 的所有方法,并且似乎也拥有外围类 GreenhouseContrlos 的所有方法。
|
||||
|
||||
一个由Event对象组成的数组被递交给Restart,该数组要加到控制器上。由于Restart0也是一个Event对象,所以同样可以将Restart对象添加到Restart.action0中,以使系统能够有规律地重新启动自己。
|
||||
一个由 Event 对象组成的数组被递交给 Restart,该数组要加到控制器上。由于 Restart0 也是一个 Event 对象,所以同样可以将 Restart 对象添加到 Restart.action0 中,以使系统能够有规律地重新启动自己。
|
||||
|
||||
下面的类通过创建一个GreenhouseControls对象,并添加各种不同的Event对象来配置该系统,这是命令设计模式的一个例子在eventList中的每一个被封装成对象的请求:
|
||||
下面的类通过创建一个 GreenhouseControls 对象,并添加各种不同的 Event 对象来配置该系统,这是命令设计模式的一个例子在 eventList 中的每一个被封装成对象的请求:
|
||||
|
||||
```java
|
||||
// innerclasses/GreenhouseController.java
|
||||
@@ -1153,7 +1153,7 @@ Greenhouse water is off
|
||||
Terminating
|
||||
```
|
||||
|
||||
这个类的作用是初始化系统,所以它添加了所有相应的事件。Restart事件反复运行,而且它每次都会将eventList加载到GreenhouseControls对象中。如果提供了命令行参数,系统会以它作为毫秒数,决定什么时候终止程序(这是测试程序时使用的)。
|
||||
这个类的作用是初始化系统,所以它添加了所有相应的事件。Restart 事件反复运行,而且它每次都会将 eventList 加载到 GreenhouseControls 对象中。如果提供了命令行参数,系统会以它作为毫秒数,决定什么时候终止程序(这是测试程序时使用的)。
|
||||
|
||||
当然,更灵活的方法是避免对事件进行硬编码。
|
||||
|
||||
@@ -1183,7 +1183,7 @@ public class InheritInner extends WithInner.Inner {
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,InheritInner只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:
|
||||
可以看到,InheritInner 只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:
|
||||
|
||||
```java
|
||||
enclosingClassReference.super();
|
||||
@@ -1231,7 +1231,7 @@ New Egg()
|
||||
Egg.Yolk()
|
||||
```
|
||||
|
||||
默认的无参构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了BigEgg的对象,那么所使用的应该是“覆盖后”的Yolk版本,但从输出中可以看到实际情况并不是这样的。
|
||||
默认的无参构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了 BigEgg 的对象,那么所使用的应该是“覆盖后”的 Yolk 版本,但从输出中可以看到实际情况并不是这样的。
|
||||
|
||||
这个例子说明,当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。当然,明确地继承某个内部类也是可以的:
|
||||
|
||||
@@ -1280,22 +1280,117 @@ BigEgg2.Yolk()
|
||||
BigEgg2.Yolk.f()
|
||||
```
|
||||
|
||||
现在BigEgg2.Yolk通过extends Egg2.Yolk明确地继承了此内部类,并且覆盖了其中的方法。
|
||||
现在 BigEgg2.Yolk 通过 extends Egg2.Yolk 明确地继承了此内部类,并且覆盖了其中的方法。
|
||||
|
||||
insertYolk()方法允许BigEgg2将它自己的Yolk对象向上转型为Egg2中的引用y。所以当g()调用y.f()时,覆盖后的新版的f()被执行。第二次调用Egg2.Yolk(),结果是BigEgg2.Yolk的构造器调用。
|
||||
insertYolk() 方法允许 BigEgg2 将它自己的 Yolk 对象向上转型为 Egg2 中的引用 y。所以当 g() 调用 y.f() 时,覆盖后的新版的 f() 被执行。第二次调用 Egg2.Yolk(),结果是 BigEgg2.Yolk 的构造器调用了其基类的构造器。可以看到在调用 g() 的时候,新版的 f() 被调用了。
|
||||
|
||||
<!-- Local Inner Classes -->
|
||||
|
||||
## 内部类局部变量
|
||||
## 局部内部类
|
||||
|
||||
前面提到过,可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分;但是它可以访问当前代码块内的常量,以及此外围类的所有成员。下面的例子对局部内部类与匿名内部类的创建进行了比较。
|
||||
|
||||
```java
|
||||
// innerclasses/LocalInnerClass.java
|
||||
// Holds a sequence of Objects
|
||||
interface Counter {
|
||||
int next();
|
||||
}
|
||||
public class LocalInnerClass {
|
||||
private int count = 0;
|
||||
Counter getCounter(final String name) {
|
||||
// A local inner class:
|
||||
class LocalCounter implements Counter {
|
||||
LocalCounter() {
|
||||
// Local inner class can have a constructor
|
||||
System.out.println("LocalCounter()");
|
||||
}
|
||||
@Override
|
||||
public int next() {
|
||||
System.out.print(name); // Access local final
|
||||
return count++;
|
||||
}
|
||||
}
|
||||
return new LocalCounter();
|
||||
}
|
||||
// Repeat, but with an anonymous inner class:
|
||||
Counter getCounter2(final String name) {
|
||||
return new Counter() {
|
||||
// Anonymous inner class cannot have a named
|
||||
// constructor, only an instance initializer:
|
||||
{
|
||||
System.out.println("Counter()");
|
||||
}
|
||||
@Override
|
||||
public int next() {
|
||||
System.out.print(name); // Access local final
|
||||
return count++;
|
||||
}
|
||||
};
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
LocalInnerClass lic = new LocalInnerClass();
|
||||
Counter
|
||||
c1 = lic.getCounter("Local inner "),
|
||||
c2 = lic.getCounter2("Anonymous inner ");
|
||||
for(int i = 0; i < 5; i++)
|
||||
System.out.println(c1.next());
|
||||
for(int i = 0; i < 5; i++)
|
||||
System.out.println(c2.next());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出为:
|
||||
|
||||
```
|
||||
LocalCounter()
|
||||
Counter()
|
||||
Local inner 0
|
||||
Local inner 1
|
||||
Local inner 2
|
||||
Local inner 3
|
||||
Local inner 4
|
||||
Anonymous inner 5
|
||||
Anonymous inner 6
|
||||
Anonymous inner 7
|
||||
Anonymous inner 8
|
||||
Anonymous inner 9
|
||||
```
|
||||
|
||||
Counter 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。
|
||||
|
||||
所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。
|
||||
|
||||
|
||||
<!-- Inner-Class Identifiers -->
|
||||
## 内部类标识符
|
||||
|
||||
由于每个类都会产生一个.class 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个"meta-class",叫做 Class 对象)。
|
||||
|
||||
你可能猜到了,内部类也必须生成一个.class 文件以包含它们的 Class 对象信息。这些类文件的命名有严格的规则:外围类的名字,加上“$",再加上内部类的名字。例如,LocalInnerClass.java 生成的.class 文件包括:
|
||||
|
||||
```java
|
||||
Counter.class
|
||||
LocalInnerClass$1.class
|
||||
LocalInnerClass$1LocalCounter.class
|
||||
LocalInnerClass.class
|
||||
```
|
||||
|
||||
如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外围类标识符与“$”的后面。
|
||||
|
||||
虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是 java 的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java 编译器会尽可能地转换它们。)
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
比起面向对象编程中其他的概念来,接口和内部类更深奥复杂,比如 C++ 就没有这些。将两者结合起来,同样能够解决 C++ 中的用多重继承所能解决的问题。然而,多重继承在 C++ 中被证明是相当难以使用的,相比较而言,Java 的接口和内部类就容易理解多了。
|
||||
|
||||
虽然这些特性本身是相当直观的,但是就像多态机制一样,这些特性的使用应该是设计阶段考虑的问题。随着时间的推移,读者将能够更好地识别什么情况下应该使用接口,什么情况使用内部类,或者两者同时使用。但此时,读者至少应该已经完全理解了它们的语法和语义。
|
||||
|
||||
当见到这些语言特性实际应用时,就最终理解它们了。
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
Reference in New Issue
Block a user