diff --git a/README.md b/README.md index dbed145..d220c30 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ - [ ] [第二十章 泛型](docs/book/20-Generics.md) - [ ] [第二十一章 数组](docs/book/21-Arrays.md) - [ ] [第二十二章 枚举](docs/book/22-Enumerations.md) -- [ ] [第二十三章 注解](docs/book/23-Annotations.md) +- [x] [第二十三章 注解](docs/book/23-Annotations.md) - [ ] [第二十四章 并发编程](docs/book/24-Concurrent-Programming.md) - [ ] [第二十五章 设计模式](docs/book/25-Patterns.md) - [ ] [附录:补充](docs/book/Appendix-Supplements.md) diff --git a/SUMMARY.md b/SUMMARY.md index a605560..cf8048a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -166,7 +166,7 @@ * [高阶函数](docs/book/13-Functional-Programming.md#高阶函数) * [闭包](docs/book/13-Functional-Programming.md#闭包) * [函数组合](docs/book/13-Functional-Programming.md#函数组合) - * [Currying和Partial-Evaluation](docs/book/13-Functional-Programming.md#Currying和Partial-Evaluation) + * [柯里化和部分求值](docs/book/13-Functional-Programming.md#柯里化和部分求值) * [纯函数式编程](docs/book/13-Functional-Programming.md#纯函数式编程) * [本章小结](docs/book/13-Functional-Programming.md#本章小结) * [第十四章 流式编程](docs/book/14-Streams.md) diff --git a/docs/_coverpage.md b/docs/_coverpage.md index 3472a5c..11b556a 100644 --- a/docs/_coverpage.md +++ b/docs/_coverpage.md @@ -8,7 +8,10 @@ [![stars](https://badgen.net/github/stars/lingcoder/OnJava8?icon=github&color=4ab8a1)](https://github.com/lingcoder/OnJava8) [![forks](https://badgen.net/github/forks/lingcoder/OnJava8?icon=github&color=4ab8a1)](https://github.com/lingcoder/OnJava8) - 本页总访问次数: + 👁️本页总访问次数: + + + | 🧑总访客数: [GitHub](https://github.com/lingcoder/onJava8/) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index ffffa1b..0a025cf 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -145,7 +145,7 @@ * [高阶函数](book/13-Functional-Programming.md#高阶函数) * [闭包](book/13-Functional-Programming.md#闭包) * [函数组合](book/13-Functional-Programming.md#函数组合) - * [Currying和Partial-Evaluation](book/13-Functional-Programming.md#Currying和Partial-Evaluation) + * [柯里化和部分求值](book/13-Functional-Programming.md#柯里化和部分求值) * [纯函数式编程](book/13-Functional-Programming.md#纯函数式编程) * [本章小结](book/13-Functional-Programming.md#本章小结) * [第十四章 流式编程](book/14-Streams.md) diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index b02b09c..14e88cd 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -27,7 +27,7 @@ OO(object oriented,面向对象)是抽象数据,FP(functional programm 更好的是,“不可变对象和无副作用”范例解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。这是可变共享状态的问题,这意味着代码的不同部分(在不同的处理器上运行)可以尝试同时修改同一块内存(谁赢了?没人知道)。如果函数永远不会修改现有值但只生成新值,则不会对内存产生争用,这是纯函数式语言的定义。 因此,经常提出纯函数式语言作为并行编程的解决方案(还有其他可行的解决方案)。 -需要提醒大家的是,函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:“为并行编程”,“代码可靠性”和“代码创建和库复用”。[^1] 同时,函数式编程的参数能帮助程序员创建更快更健壮的代码 —— 部分仍然只是假设。虽然已有一些好的范例,[^2]但我们还不能证明纯函数式语言就是解决编程问题的最佳方法。 +需要提醒大家的是,函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:“为并行编程”,“代码可靠性”和“代码创建和库复用”。[^1] 同时,函数式编程的参数能帮助程序员创建更快更健壮的代码 —— 部分仍然只是假设。虽然已有一些好的范例[^2],但我们还不能证明纯函数式语言就是解决编程问题的最佳方法。 FP 思想值得融入非 FP 语言,如 Python。Java 8 也从中吸收并支持了 FP。我们将在此章探讨。 @@ -366,7 +366,7 @@ Help! **[3]** `hello()` 也符合 `call()` 的签名。 -[4] ...就像是 `help()`,一个静态内部类中的非静态方法。(原文:… as is help(), a non-static method within a static inner class.) +**[4]** 就像是 `help()`,静态内部类中的非静态方法。 **[5]** `assist()` 是静态内部类中的静态方法。 @@ -378,11 +378,11 @@ Help! **[9]** 这是 **[6]** 的另一个版本:附加到存活对象的方法的方法参考,有时称为*绑定方法引用*。 -**[10]** 最后,获取静态内部类的静态方法的方法引用,用起来就像 **[8]** 中的外部类。 +**[10]** 最后,获取静态内部类的方法引用的操作与 **[8]** 中外部类方式一样。 -上例并无详尽; 我们很快就会看到方法参考的所有变化。 +上例只是简短的介绍,我们很快就能看到方法引用的全部变化。 -### Runnable +### Runnable接口 **Runnable** 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 `run()` 不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 **Runnable**: @@ -653,21 +653,22 @@ public class FunctionalAnnotation { | **特征** |**函数式方法名**|**示例**| | :---- | :----: | :----: | -|无参;
无返回值|**Runnable**
(java.lang)
`run()`|**Runnable**| -|无参;
返回类型任意|**Supplier**
`get()`
`getAs类型()`| **Supplier
BooleanSupplier
IntSupplier
LongSupplier
DoubleSupplier**| -|无参数;
返回类型任意|**Callable**
(java.util.concurrent)
`call()`|**Callable\**| -|1 参数;
无返回值|**Consumer**
`accept()`|**Consumer
IntConsumer
LongConsumer
DoubleConsumer**| -|2 参数 **Consumer**|**BiConsumer**
`accept()`|**BiConsumer\**| -|2 参数 **Consumer**;
1 引用;
1 基本类型|**Obj类型Consumer**
`accept()`|**ObjIntConsumer
ObjLongConsumer\
ObjDoubleConsumer\**| -|1 参数;
返回类型不同|**Function**
`apply()`
**To类型** 和 **类型To类型**
`applyAs类型()`|**Function\
IntFunction\
LongFunction\
DoubleFunction
ToIntFunction
ToLongFunction\
ToDoubleFunction\
IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunction
DoubleToLongFunction**| -|1 参数;
返回类型相同|**UnaryOperator**
`apply()`|**UnaryOperator
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator**| -|2 参数类型相同;
返回类型相同|**BinaryOperator**
`apply()`|**BinaryOperator\
IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator**| -|2 参数类型相同;
返回整型|Comparator
(java.util)
`compare()`|**Comparator\**| -|2 参数;
返回布尔型|**Predicate**
`test()`|**Predicate\
BiPredicate\
IntPredicate
LongPredicate
DoublePredicate**| +|无参数;
无返回值|**Runnable**
(java.lang)
`run()`|**Runnable**| +|无参数;
返回类型任意|**Supplier**
`get()`
`getAs类型()`| **Supplier\
BooleanSupplier
IntSupplier
LongSupplier
DoubleSupplier**| +|无参数;
返回类型任意|**Callable**
(java.util.concurrent)
`call()`|**Callable\**| +|1 参数;
无返回值|**Consumer**
`accept()`|**Consumer\
IntConsumer
LongConsumer
DoubleConsumer**| +|2 参数 **Consumer**|**BiConsumer**
`accept()`|**BiConsumer\**| +|2 参数 **Consumer**;
1 引用;
1 基本类型|**Obj类型Consumer**
`accept()`|**ObjIntConsumer\
ObjLongConsumer\
ObjDoubleConsumer\**| +|1 参数;
返回类型不同|**Function**
`apply()`
**To类型** 和 **类型To类型**
`applyAs类型()`|**Function\
IntFunction\
LongFunction\
DoubleFunction\
ToIntFunction\
ToLongFunction\
ToDoubleFunction\
IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunction
DoubleToLongFunction**| +|1 参数;
返回类型相同|**UnaryOperator**
`apply()`|**UnaryOperator\
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator**| +|2 参数类型相同;
返回类型相同|**BinaryOperator**
`apply()`|**BinaryOperator\
IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator**| +|2 参数类型相同;
返回整型|Comparator
(java.util)
`compare()`|**Comparator\**| +|2 参数;
返回布尔型|**Predicate**
`test()`|**Predicate\
BiPredicate\
IntPredicate
LongPredicate
DoublePredicate**| |参数基本类型;
返回基本类型|**类型To类型Function**
`applyAs类型()`|**IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunction
DoubleToLongFunction**| -|2 参数类型不同|**Bi操作**
(不同方法名)|**BiFunction\
BiConsumer\
BiPredicate
ToIntBiFunction\
ToLongBiFunction\
ToDoubleBiFunction (不同方法名)|**BiFunction\
BiConsumer\
BiPredicate\
ToIntBiFunction\
ToLongBiFunction\
ToDoubleBiFunction\**| -此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出更多行的函数式接口。 + +  此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出更多行的函数式接口。 能看出来在创建 `java.util.function` 时,设计者做出了一些选择。 @@ -938,9 +939,9 @@ public interface IntToDoubleFunction { ## 高阶函数 -这个名字听起来有点令人生畏,但是:高阶函数只是一个消耗或产生函数的函数。 +这个名字听起来有点令人生畏,但是:[高阶函数](https://en.wikipedia.org/wiki/Higher-order_function)(Higher-order Function)只是一个消费或产生函数的函数。 -我们先来看看产生一个函数: +我们先来看看如何产生一个函数: ```java // functional/ProduceFunction.java @@ -959,18 +960,20 @@ public class ProduceFunction { System.out.println(f.apply("YELLING")); } } -/* Output: +``` + +输出结果: +``` yelling -*/ ``` 这里,`produce()` 是高阶函数。 -**[1]** 使用继承,你可以轻松地为你的专用接口创建别名。 +**[1]** 使用继承,可以轻松地为你的专用接口创建别名。 -**[2]** 使用 Lambda 表达式,在方法中创建和返回一个函数几乎毫不费力。 +**[2]** 使用 Lambda 表达式,在方法中轻松创建和返回一个函数。 -要 `consume` 函数(consume a function),其对应的 `consuming` 方法 (the consuming method)的参数列表必须正确描述函数类型: +要消费一个函数,消费函数需要在参数列表正确地描述函数类型。代码示例: ```java // functional/ConsumeFunction.java @@ -990,7 +993,7 @@ public class ConsumeFunction { } ``` -当你根据 `consume` 的函数生成新函数时,事情变得特别有趣,比如: +有趣的是,根据消费函数生成新函数。代码示例: ```java // functional/TransformFunction.java @@ -1022,27 +1025,32 @@ public class TransformFunction { O o = f2.apply(new I()); } } -/* Output: -I -O -*/ ``` -在这里,`transform()` 生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。这在 `Function` 接口中使用名为 `andThen()` 的默认方法,该方法专门用于操作函数。 顾名思义,在调用in函数之后调用 `toThen()`(还有 `compose()`,它在 `in` 函数之前应用新函数)。 要附加一个 `andThen()` 函数,我们只需将该函数作为参数传递。 `transform()` 产生的是一个新函数,它将 `in` 的动作与 `andThen()` 参数的动作结合起来。 +输出结果: + +``` +I +O +``` + +在这里,`transform()` 生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。 + +这在 `Function` 接口中使用名为 `andThen()` 的默认方法,该方法专门用于操作函数。 顾名思义,在调用 `in` 函数之后调用 `toThen()`(还有 `compose()`,它在 `in` 函数之前应用新函数)。 要附加一个 `andThen()` 函数,我们只需将该函数作为参数传递。 `transform()` 产生的是一个新函数,它将 `in` 的动作与 `andThen()` 参数的动作结合起来。 ## 闭包 -在上一节的 `ProduceFunction.java` 中,我们从方法返回了一个 Lambda 函数。 这个例子让事情变得简单,但是我们必须在返回 Lambdas 时探讨一些问题。 +在上一节的 `ProduceFunction.java` 中,我们从方法中返回 Lambda 函数。 虽然过程简单,但是有些问题必须再回过头来探讨一下。 -*闭包*一词概括了这些问题。 闭包非常重要,因为它们可以轻松生成函数。 +**闭包**(Closure)一词总结了这些问题。 它非常重要,利用闭包可以轻松生成函数。 -考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者在词法上限定范围( 也使用术语变量捕获 )。Java 8提供了有限但合理的闭包支持, +考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者在词法上限定范围( 也使用术语变量捕获 )。Java 8 提供了有限但合理的闭包支持, 我们将用一些简单的例子来研究它。 -首先,这里有一个方法返回一个访问对象字段和方法参数的函数: +首先,下例函数中,方法返回访问对象字段和方法参数。代码示例: ```java // functional/Closure1.java @@ -1057,8 +1065,7 @@ public class Closure1 { } ``` -但是,仔细考虑一下,`i` 的这种用法并不是一个很大的挑战,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们都为 `i` 共享相同的存储空间: - +但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们都为 `i` 共享相同的存储空间: ```java // functional/SharedStorage.java @@ -1075,11 +1082,14 @@ public class SharedStorage { System.out.println(f3.getAsInt()); } } -/* Output: +``` + +输出结果: + +``` 0 1 2 -*/ ``` 每次调用 `getAsInt()` 都会增加 `i`,表明存储是共享的。 @@ -1099,7 +1109,7 @@ public class Closure2 { } ``` -由 `makeFun()` 返回的 `IntSupplier` “关闭” `i` 和 `x`,因此当你调用返回的函数时两者仍然有效。 但请注意,我没有像 `Closure1.java` 那样增加i。 尝试递增它会产生编译时错误: +由 `makeFun()` 返回的 `IntSupplier` “关闭” `i` 和 `x`,因此当你调用返回的函数时两者仍然有效。 但请**注意**,我没有像 `Closure1.java` 那样递增 `i`,因为会产生编译时错误。代码示例: ```java // functional/Closure3.java @@ -1110,15 +1120,15 @@ import java.util.function.*; public class Closure3 { IntSupplier makeFun(int x) { int i = 0; - // Neither x++ nor i++ will work: + // x++ 和 i++ 都会报错: return () -> x++ + i++; } } ``` -`x` 和 `i` 的操作都犯了同样的错误: +`x` 和 `i` 的操作都犯了同样的错误:从 Lambda 表达式引用的局部变量必须是 `final` 或者实 `final` 效果的。 -显然,从 Lambda 表达式引用的局部变量必须是 `final` 或者 实际的 `final` (effectively final),如果我们声明 `x`和 `i` 是 `final` ,它将起作用,因为那时我们不能增加任何一个: +如果使用 `final` 修饰 `x`和 `i`,就不能再递增它们的值了。代码示例: ```java // functional/Closure4.java @@ -1133,16 +1143,16 @@ public class Closure4 { } ``` -但是为什么 `Closure2.java` 在 `x` 和 `i `不是 `final` 却可以运行? +那么为什么在 `Closure2.java` 中, `x` 和 `i` 非 `final` 却可以运行呢? -这就是 “实际” `final `(effectively final)的含义出现的地方。 这个术语是为 Java 8 创建的,表示你没有明确地声明变量是 `final` 的,但你仍然是这样对待它 - 你没有改变它。 如果局部变量的初始值永远不会改变,那么它实际上是最终的。 +这就叫做**等同 final 效果**(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是 `final` 的,但是因变量值没被改变过而实际有了 `final` 同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是 `final` 的。 -如果 `x` 和 `i` 在方法中的其他位置更改(但不在返回函数内部),则编译器仍将其视为错误。 每个增量产生一个单独的错误消息: +如果 `x` 和 `i` 的值在方法中的其他位置发生改变(但不在返回的函数内部),则编译器仍将视其为错误。每个递增操作则会分别产生错误消息。代码示例: ```java / functional/Closure5.java -// {WillNotCompile} +// {无法编译成功} import java.util.function.*; public class Closure5 { @@ -1155,9 +1165,9 @@ public class Closure5 { } ``` -要成为 “effectively final” ,意味着你可以将 final 关键字应用于变量声明而不更改任何其余代码。 它实际上是 `final`的,你只是没有明说。 +我们可以通过将 `final` 关键字应用于变量声明来实现**等同 final 效果**, 不用更改任何其余代码。 实际上它就是具备 `final` 效果的,只是没有明确说明。 -我们实际上可以通过在闭包中使用它们之前将 `x` 和 `i` 赋值给 `final` 变量来解决 `Closure5.java` 中的问题: +通过在闭包中使用 `final` 关键字提前修饰变量 `x` 和 `i` , 我们解决了 `Closure5.java` 中的问题。代码示例: ```java @@ -1177,14 +1187,14 @@ public class Closure6 { } ``` -由于我们在赋值后永远不会更改 `iFinal` 和 `xFinal` ,因此在这里使用 `final` 是多余的。 +上例中 `iFinal` 和 `xFinal` 的值在赋值后并没有改变过,因此在这里使用 `final` 是多余的。 -如果你使用引用怎么办? 我们可以从 **int** 更改为 **Integer**: +如果这里是引用的话,需要把 **int** 型更改为 **Integer** 型。代码示例: ```java // functional/Closure7.java -// {WillNotCompile} +// {无法编译成功} import java.util.function.*; public class Closure7 { @@ -1196,7 +1206,7 @@ public class Closure7 { } ``` -编译器仍然足够聪明,可以看到 `i` 正在被更改。 包装器类型可能正在进行特殊处理,所以让我们尝试一下List: +编译器非常智能,它能识别变量 `i` 的值正在被更改。 对于包装类型的处理可能比较特殊,所以让我们使用下面的 List 的例子。代码示例: ```java // functional/Closure8.java @@ -1223,24 +1233,27 @@ public class Closure8 { System.out.println(l2); } } -/* Output: +``` + +输出结果: + +``` [1] [1] [1, 42] [1, 96] -*/ ``` -这次它可以运行:我们修改 `List` 的内容而没产生编译时错误。 当你查看此示例的输出时,它看起来确实非常安全,因为每次调用 `makeFun()`时,都会创建并返回一个全新的 `ArrayList` - 这意味着它不会被共享,因此每个生成的闭包都有自己独立的 `ArrayList` 他们不能互相干扰。 +可以看到,这次一切正常。我们改变了 List 的值却没产生编译时错误。通过观察本例的输出结果,我们发现这看起来非常安全。这是因为每次调用 `makeFun()` 时,其实都会创建并返回一个全新的 `ArrayList`。 也就是说,每个闭包都有自己独立的 `ArrayList` 他们不能互相干扰和共享。 -并且请注意我已经声明 `ai` 是 `final` 的,尽管在这个例子中你可以去掉 `final` 并得到相同的结果(试试吧!)。 应用于对象引用的 `final` 关键字仅表示不会重新赋值引用。 它并没有说你无法修改对象本身。 +请**注意**我已经声明 `ai` 是 `final` 的了。尽管在这个例子中你可以去掉 `final` 并得到相同的结果(试试吧!)。 应用于对象引用的 `final` 关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。 -看看 `Closure7.java` 和 `Closure8.java` 之间的区别,我们看到 `Closure7.java` 实际上有一个 `i` 的重新赋值。 也许这是 “effectively final” 错误消息的触发点: +下面我们来看看 `Closure7.java` 和 `Closure8.java` 之间的区别。我们看到:在 `Closure7.java` 中变量 `i` 有过重新赋值。 也许这就是**等同 final 效果**错误消息的触发点。 ```java // functional/Closure9.java -// {WillNotCompile} +// {无法编译成功} import java.util.*; import java.util.function.*; @@ -1253,12 +1266,11 @@ public class Closure9 { } ``` -引用的重新赋值确实会触发错误消息。 如果只修改指向的对象,Java 会接受它。 只要没有其他人获得对该对象的引用(这意味着你有多个可以修改对象的实体,此时事情会变得非常混乱),这可能是安全的。[^6] +上例,重新赋值引用会触发错误消息。如果只修改指向的对象则没问题,只要没有其他人获得对该对象的引用(这意味着你有多个实体可以修改对象,此时事情会变得非常混乱),基本上就是安全的[^6]。 -然而,如果我们现在回顾一下 `Closure1.java.` ,那就有一个难题:`i` 被修改却没有编译器投诉。 它既不是 `final` 的,也不是“effectively final"的。因为 `i` 是外围类的成员,所以这样做肯定是安全的( 除非你正在创建共享可变内存的多个函数)。实际上,你可以争辩说在这种情况下不会发生变量捕获(variable capture)。 可以肯定的是,`Closure3.java` 的错误消息专门针对局部变量。 因此,规则并不像说“在Lambda之外定义的任何变量必须是 `final` 的或 `effectively final` 那么简单。相反,你必须考虑捕获的变量是否实际 `final`。 如果它是对象中的字段,那么它有一个独立的生存期,并且不需要任何特殊的捕获,以便稍后在调用 Lambda 时存在。 +让我们回顾一下 `Closure1.java`。那么现在问题来了:为什么变量 `i` 被修改编译器却没有报错呢。 它既不是 `final` 的,也不是**等同 final 效果**的。因为 `i` 是外围类的成员,所以这样做肯定是安全的(除非你正在创建共享可变内存的多个函数)。是的,你可以辩称在这种情况下不会发生变量捕获(Variable Capture)。但可以肯定的是,`Closure3.java` 的错误消息是专门针对局部变量的。因此,规则并非只是“在 Lambda 之外定义的任何变量必须是 `final` 的或**等同 final 效果**那么简单。相反,你必须考虑捕获的变量是否是**等同 final 效果**的。 如果它是对象中的字段,那么它拥有独立的生存周期,并且不需要任何特殊的捕获,以便稍后在调用 Lambda 时存在。 - ### 作为闭包的内部类 我们可以复制我们的例子使用匿名内部类: @@ -1271,9 +1283,9 @@ import java.util.function.*; public class AnonymousClosure { IntSupplier makeFun(int x) { int i = 0; - // Same rules apply: - // i++; // Not "effectively final" - // x++; // Ditto + // 同样规则的应用: + // i++; // 非等同 final 效果 + // x++; // 同上 return new IntSupplier() { public int getAsInt() { return x + i; } }; @@ -1281,21 +1293,24 @@ public class AnonymousClosure { } ``` -事实证明,只要有内部类,就会有闭包(Java 8只 会使闭包变得更容易)。 在 Java 8 之前,要求是 `x` 和 `i` 被明确声明为 `final`。 使用 Java 8,内部类的规则已经放宽,包括 “effectively final”。 +实际上只要有内部类,就会有闭包(Java 8 只是简化了闭包操作)。在 Java 8 之前,变量 `x` 和 `i` 必须被明确声明为 `final`。在 Java 8 中,内部类的规则放宽,包括**等同 final 效果**。 ## 函数组合 -函数组合基本上意味着“将函数粘贴在一起以创建新函数”,它通常被认为是函数编程的一部分。你在`TransformFunction.java` 中看到了一个使用 `andThen()` 的函数组合示例。一些 `java.util` 的函数接口包含支持函数组合的方法。 +函数组合(Function Composition)意为“多个函数组合成新函数”。它通常是函数式编程的基本组成部分。在前面的 `TransformFunction.java` 类中,有一个使用 `andThen()` 的函数组合示例。在 `java.util.function` 包里包含支持函数组合的一些接口方法 [^7]。 -Compositional Supporting Method Interfaces Function BiFunction Consumer BiConsumer IntConsumer andThen(argument) Performs the original LongConsumer operation followed by DoubleConsumer the argument operation. +| 组合方法 | 支持接口 | +| :----- | :----- | +| `andThen(argument)`
根据参数执行原始操作 | **Function
BiFunction
Consumer
BiConsumer
IntConsumer
LongConsumer
DoubleConsumer
UnaryOperator
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
BinaryOperator** | +| `compose(argument)`
根据参数执行原始操作 | **Function
UnaryOperator
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator** | +| `and(argument)`
短路**逻辑与**原始断言和参数断言 | **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** | +| `or(argument)`
短路**逻辑或**原始断言和参数断言 | **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** | +| `negate()`
该断言的**逻辑否**断言| **Predicate
BiPredicate
IntPredicate
LongPredicate
DoublePredicate** | -UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOperator Function compose(argument) UnaryOperator Performs the argument IntUnaryOperator operation followed by the original operation. LongUnaryOperator DoubleUnaryOperator Predicate and(argument) BiPredicate Short-circuiting logical IntPredicate AND of the original predicate and the LongPredicate argument predicate. DoublePredicate Predicate or(argument) BiPredicate Short-circuiting logical OR of the original IntPredicate predicate and the LongPredicate argument predicate. DoublePredicate Predicate negate() BiPredicate A predicate that is the IntPredicate logical negation of this predicate. LongPredicate DoublePredicate -( 待整理 ) - -此示例使用 `Function` 里的 `compose()`和 `andThen()` +下例使用了 `Function` 里的 `compose()`和 `andThen()`。代码示例: ```java // functional/FunctionComposition.java @@ -1316,17 +1331,20 @@ public class FunctionComposition { f4.apply("GO AFTER ALL AMBULANCES")); } } -/* Output: -AFTER ALL AMBULANCES -_fter _ll _mbul_nces -*/ ``` -这里要看的重要一点是我们正在创建一个新函数 `f4`,然后可以使用 `apply()`(几乎)像任何其他函数一样调用它。[^8] +输出结果: -当 `f1` 获得String时,它已经被`f2` 剥离了前三个字符。 这是因为对 `compose(f2)`的调用意味着在 `f1` 之前调用 `f2`。 +``` +AFTER ALL AMBULANCES +_fter _ll _mbul_nces +``` -这是Predicate逻辑运算的演示: +这里我们重点看正在创建的新函数 `f4`。它调用 `apply()` 的方式与常规几乎无异[^8]。 + +当 `f1` 获得字符串时,它已经被`f2` 剥离了前三个字符。这是因为对 `compose(f2)`的调用意味着在 `f1` 之前调用 `f2`。 + +下例是 `Predicate` 的逻辑运算演示.代码示例: ```java // functional/PredicateComposition.java @@ -1346,21 +1364,26 @@ public class PredicateComposition { .forEach(System.out::println); } } -/* Output: -foobar -foobaz -*/ ``` -`p4` 获取所有谓词并将它们组合成一个更复杂的谓词,其中包含:“如果 `String` 不包含 'bar' 且长度小于5,或者它包含 'foo' ,则结果为 `true`。”因为它产生如此清晰的语法,我在`main()`中作了一些小伎俩,并借用了下一章的内容。 首先,我创建一个 `String` 对象的 “流”(序列),然后将每个对象提供给 `filter()`操作。 `filter()`使用我们的 `p4` 谓词来决定要保留流中的哪个对象以及要丢弃的对象。 最后,我使用 `forEach()`将 `println` 方法引用应用于每个幸存的对象。 +输出结果: -你可以从输出中看到 `p4` 是如何工作的:任何带有 “foo ”的东西都会存活,即使它的长度大于5。 “fongopuckey” 太长了,没有 “bar” 来保存它。 +``` +foobar +foobaz +``` + +`p4` 获取到了所有断言并组合成一个更复杂的断言。解读:如果字符串中不包含 `bar` 且长度小于 5,或者它包含 `foo` ,则结果为 `true`。 + +正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建一个流的字符串对象集合,然后将每个对象传递给 `filter()` 操作。 `filter()` 使用 `p4` 的断言来确定对象的去留。最后我们使用 `forEach()` 和 `println` 方法打印出留下来的对象。 + +从输出结果我们可以看到 `p4` 的工作流程:任何带有 `foo` 的东西都会留下,即使它的长度大于 5。 `fongopuckey` 因长度超出和不包含 `bar` 而被丢弃。 -## Currying和Partial-Evaluation +## 柯里化和部分求值 -*Currying* 以 Haskell Curry 命名,Haskell Curry 是其发明者之一,可能是唯一一个以他的名字命名的重要事物的计算机领域的人物(另一个是 Haskell 编程语言)。 Currying 意味着从一个函数开始,该函数接受多个参数,并将其转换为一系列函数,每个函数只接受一个参数。 +[柯里化](https://en.wikipedia.org/wiki/Currying)(Currying)的名称来自于其发明者之一 *Haskell Curry*。他可能是计算机领域唯一名字被命名重要概念的人(另外就是 Haskell 编程语言)。 柯里化意为:将一个多参数的函数,转换为一系列单参数函数。 ```java // functional/CurryingAndPartials.java @@ -1368,12 +1391,12 @@ foobaz import java.util.function.*; public class CurryingAndPartials { - // Uncurried: + // 未柯里化: static String uncurried(String a, String b) { return a + b; } public static void main(String[] args) { - // Curried function: + // 柯里化的函数: Function> sum = a -> b -> a + b; // [1] @@ -1383,26 +1406,29 @@ public class CurryingAndPartials { hi = sum.apply("Hi "); // [2] System.out.println(hi.apply("Ho")); - // Partial application: + // 部分应用: Function sumHi = sum.apply("Hup "); System.out.println(sumHi.apply("Ho")); System.out.println(sumHi.apply("Hey")); } } -/* Output: +``` + +输出结果: + +``` Hi Ho Hi Ho Hup Ho Hup Hey -*/ ``` -**[1]** 这是一条巧妙的线:一连串的箭头。注意,在函数接口声明中,函数的第二个参数是另一个函数。 +**[1]** 这一连串的箭头很巧妙。*注意*,在函数接口声明中,第二个参数是另一个函数。 -**[2]** currying 的目标是能够通过提供一个参数来创建一个新函数,所以现在有了一个 “带参函数” 和剩下的 “无参函数” 。实际上,你从一个双参数函数开始,最后得到一个单参数函数。 +**[2]** 柯里化的目的是能够通过提供一个参数来创建一个新函数,所以现在有了一个“带参函数”和剩下的 “无参函数” 。实际上,你从一个双参数函数开始,最后得到一个单参数函数。 -你可以通过添加另一个级别来 curry 一个三参数函数: +我们可以通过添加级别来柯里化一个三参数函数: ```java // functional/Curry3Args.java @@ -1423,14 +1449,17 @@ public class Curry3Args { System.out.println(ho.apply("Hup")); } } -/* Output: -Hi Ho Hup -*/ ``` -对于每个级别的箭头级联(arrow-cascading),你可以围绕类型声明包装另一个函数。 +输出结果: -处理基本类型和装箱时,请使用适当的功能接口: +``` +Hi Ho Hup +``` + +对于每个级别的箭头级联(Arrow-cascading),你可以围绕类型声明包装成另一个函数。 + +处理基本类型和装箱时,请使用适当的 **Function** 接口: ```java // functional/CurriedIntAdd.java @@ -1445,47 +1474,48 @@ public class CurriedIntAdd { System.out.println(add4.applyAsInt(5)); } } -/* Output: -9 -*/ ``` -你可以在因特网上找到更多 currying 示例。 通常这些是 Java 以外的语言,但如果你理解它们的基本概念,它们应该很容易翻译。 +输出结果: + +``` +9 +``` + +更多柯里化示例,可搜索互联网。通常这些是 Java 以外的语言,理解它们的基本概念,就应该很容易翻译。 ## 纯函数式编程 -在没有函数支持的情况下,即使用像C这样的原始语言,也可以按照一定的原则编写纯函数程序。Java使它比这更容易,但是你必须小心地使一切都成为 `final`,并确保你的所有方法和函数没有副作用。因为 Java 本质上不是一种不可变的语言,所以如果你犯了错误,编译器不会提供任何帮助。 +即使没有函数式支持,像 C 这样的基础语言,也可以按照一定的原则编写纯函数程序。Java 8 让函数式编程更简单,不过我们要确保一切是 `final` 的,同时你的所有方法和函数没有副作用。因为 Java 在本质上并非是不可变语言,我们无法通过编译器查错。 -有第三方工具可以帮助你[^9],但是使用 `Scala` 或 `Clojure` 这样的语言可能更容易,因为它们从一开始就是为保持不变性而设计的。这些语言使你可以用 Java 编写项目的一部分,如果你必须用纯函数式编写,则可以用 `Scala` 编写其他部分 (这需要一些规则) 或 `Clojure` (这需要的少得多)。虽然你将在并发编程一章中看到 Java确实支持并发,但是如果这是你项目的核心部分,你可能会考虑至少在项目的一部分中使用 `Scala` 或 `Clojure`之类的语言。 +这种情况下,我们可以借助第三方工具[^9],但使用 Scala 或 Clojure 这样的语言可能更简单。因为它们从一开始就是为保持不变性而设计的。你可以采用这些语言来编写你的 Java 项目的一部分。如果必须要用纯函数式编写,则可以用 Scala(需要一些规则) 或 Clojure (需要的规则更少)。虽然 Java 支持[并发编程](./24-Concurrent-Programming.md),但如果这是你项目的核心部分,你应该考虑在项目部分功能中使用 `Scala` 或 `Clojure` 之类的语言。 ## 本章小结 -Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。它们对 Java 是一个巨大的改进,因为它们允许你编写更简洁、更干净、更容易理解的代码。在下一章中,你将看到它们如何启用流。如果你像我一样,你会喜欢流媒体。 +Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。这对 Java 来说是一个巨大的改进。因为这允许你编写更简洁明了,易于理解的代码。在下一章中,你会看到它们在流式编程中的应用。相信你会像我一样,喜欢上流式编程。 -这些特性可能会满足大部分 Java 程序员的需求,他们已经对 Clojure 和 Scala 等新的、功能更强的语言感到不安和嫉妒,并阻止 Java 程序员流向这些语言 (或者,如果他们仍然决定迁移,至少会为他们做好更好的准备)。 +这些特性满足大部分 Java 程序员的需求。他们开始羡慕嫉妒 Clojure、Scala 这类新语言的功能,并试图阻止 Java 程序员流失到其他阵营 (就算不能阻止,起码提供了更好的选择)。 -但是,Lambdas 和方法引用远非完美,我们永远要为 Java 设计人员在早期令人兴奋的语言中做出的草率决定付出代价。特别是,没有泛型 Lambda,所以 Lambda 实际上不是 Java 中的第一类公民。这并不意味着 Java 8不是一个很大的改进,但它确实意味着,就像许多 Java 特性一样,你最终会感到沮丧。 +但是,Lambdas 和方法引用远非完美,我们永远要为 Java 设计者早期的草率决定付出代价。特别是没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。虽然我不否认 Java 8 的巨大改进,但这意味着和许多 Java 特性一样,它的使用还是会让人感觉沮丧和鸡肋。 -当你遇到学习困难时,请记住,你可以从 ide (如 NetBeans、IntelliJ Idea 和 Eclipse )获得帮助,这些 ide 将建议你何时可以使用 Lambda 表达式或方法引用 (并且经常为你重写代码!) +当你遇到学习困难时,请记住通过 IDE(NetBeans、IntelliJ Idea 和 Eclipse)获得帮助,因为 IDE 可以智能提示你何时使用 Lambda 表达式或方法引用,甚至有时还能为你优化代码。 -1. 粘贴功能结合在一起是一个非常不同的方法,但它仍然使一种图书馆。 -2. 例如,这个电子书是利用 Pandoc 制作出来的,纯函数式语言编写的一个程序 Haskell。 -3. 有时函数语言将其描述为“代码即数据”。“ -4. 这个语法来自 C++。 -5. 我还没有验证过这种说法。 -6. 在并发编程一章中,当你理解更改共享变量 “不是线程安全的” 时,这将更有意义。 -7. 接口能够支持方法的原因是它们是 Java 8 默认方法,你将在下一章中了解到。 -8. 一些语言,例如 Python,允许像调用其他函数一样调用组合函数。但这是 Java,所以我们取我们能得到的。 -9. 见,例如,不可变和可变性检测器。( Immutables and Mutability Detector) - +[^1]: 功能粘贴在一起的方法的确有点与众不同,但它仍不失为一个库。 +[^2]: 例如,这个电子书是利用 [Pandoc](http://pandoc.org/) 制作出来的,它是用纯函数式语言 [Haskell](https://www.haskell.org/) 编写的一个程序 。 +[^3]: 有时函数式语言将其描述为“代码即数据”。“ +[^4]: 这个语法来自 C++。 +[^5]: 我还没有验证过这种说法。 +[^6]: 当你理解了[并发编程](./24-Concurrent-Programming.md)章节的内容,你就能明白为什么更改共享变量 “不是线程安全的” 的了。 +[^7]: 接口能够支持方法的原因是它们是 Java 8 默认方法,你将在下一章中了解到。 +[^8]: 一些语言,如 Python,允许像调用其他函数一样调用组合函数。但这是 Java,所以我们做做可为之事。 +[^9]: 例如,[Immutables](https://immutables.github.io/) 和 [Mutability Detector](https://mutabilitydetector.github.io/MutabilityDetector/)。 -
\ No newline at end of file diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md index 1369377..7302737 100644 --- a/docs/book/19-Type-Information.md +++ b/docs/book/19-Type-Information.md @@ -3,7 +3,7 @@ # 第十九章 类型信息 -> RTTI(RunTime Type Information, 运行时类型信息)能够在程序运行时发现和使用类型信息 +> RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息 RTTI把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。对RTTI的需要,揭示了面向对象设计中许多有趣(并且复杂)的特性,同时也带来了关于如何组织程序的基本问题。 @@ -22,7 +22,7 @@ RTTI把我们从只能在编译期进行面向类型操作的禁锢中解脱了 这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程的一个基本目的是:让代码只操纵对基类(这里即 `Shape` )的引用。这样,如果你想添加一个新类(比如从`Shape`派生出`Rhomboid`)来扩展程序,就不会影响原来的代码。在这个例子中,`Shape`接口中动态绑定了`draw()`方法,这样做的目的就是让客户端程序员可以使用泛化的`Shape`引用来调用`draw()`。`draw()`方法在所有派生类里都会被覆盖,而且由于它是动态绑定的,所以它可以使用`Shape`引用来调用,这就是多态。 -因此,我们通常会创建一个具体的对象(`Circle`、`Square` 或者`Triangle`),把它向上转型成`Shape`(忽略对象的具体类型),并且在后面的程序中使用`Shape`引用来调用在具体对象中被重载的方法(如`draw()`)。 +因此,我们通常会创建一个具体的对象(`Circle`、`Square` 或者 `Triangle`),把它向上转型成 `Shape` (忽略对象的具体类型),并且在后面的程序中使用 `Shape` 引用来调用在具体对象中被重载的方法(如 `draw()`)。 代码如下: @@ -31,108 +31,113 @@ RTTI把我们从只能在编译期进行面向类型操作的禁锢中解脱了 import java.util.stream.*; abstract class Shape { - void draw() { System.out.println(this + ".draw()"); } - @Override - public abstract String toString(); + void draw() { System.out.println(this + ".draw()"); } + @Override + public abstract String toString(); } class Circle extends Shape { - @Override - public String toString() { return "Circle"; } + @Override + public String toString() { return "Circle"; } } class Square extends Shape { - @Override - public String toString() { return "Square"; } + @Override + public String toString() { return "Square"; } } class Triangle extends Shape { - @Override - public String toString() { return "Triangle"; } + @Override + public String toString() { return "Triangle"; } } public class Shapes { - public static void main(String[] args) { - Stream.of( - new Circle(), new Square(), new Triangle()) - .forEach(Shape::draw); - } + public static void main(String[] args) { + Stream.of( + new Circle(), new Square(), new Triangle()) + .forEach(Shape::draw); + } } -/* Output: +``` + +输出结果: + +``` Circle.draw() Square.draw() Triangle.draw() -*/ ``` -基类中包含`draw()`方法,它通过传递`this`参数传递给`System.out.println()`,间接地使用`toString()`打印类标识符(注意:这里将`toString()`声明为了`abstract`,以此强制继承者覆盖改方法,并防止对`Shape`的实例化)。如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),`toString()`方法就会被自动调用,以生成表示该对象的`String`。每个派生类都要覆盖(从`Object`继承来的)`toString()`方法,这样`draw()`在不同情况下就打印出不同的消息(多态)。 +基类中包含 `draw()` 方法,它通过传递 `this` 参数传递给 `System.out.println()`,间接地使用 `toString()` 打印类标识符(注意:这里将 `toString()` 声明为了 `abstract`,以此强制继承者覆盖改方法,并防止对 `Shape` 的实例化)。如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),`toString()` 方法就会被自动调用,以生成表示该对象的 `String`。每个派生类都要覆盖(从 `Object` 继承来的)`toString()` 方法,这样 `draw()` 在不同情况下就打印出不同的消息(多态)。 -这个例子中,在把`Shape`对象放入`Stream`中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对`steam`而言,它们只是`Shape`对象。 +这个例子中,在把 `Shape` 对象放入 `Stream` 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 `steam` 而言,它们只是 `Shape` 对象。 -严格来说,`Stream`实际上是把放入其中的所有对象都当做`Object`对象来持有,只是取元素时会自动将其类型转为`Shape`。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。 +严格来说,`Stream` 实际上是把放入其中的所有对象都当做 `Object` 对象来持有,只是取元素时会自动将其类型转为 `Shape`。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。 -另外在这个例子中,类型转换并不彻底:`Object`被转型为`Shape`,而不是`Circle`、`Square`或者`Triangle`。这是因为目前我们只能确保这个 `Stream`保存的都是`Shape`: +另外在这个例子中,类型转换并不彻底:`Object` 被转型为 `Shape` ,而不是 `Circle`、`Square` 或者 `Triangle`。这是因为目前我们只能确保这个 `Stream` 保存的都是 `Shape`: -- 编译期,`stream`和 Java 泛型系统确保放入`stream`的都是`Shape`对象(`Shape`子类的对象也可视为`Shape`的对象),否则编译器会报错; -- 运行时,自动类型转换确保了从`stream`中取出的对象都是`Shape`类型。 +- 编译期,`stream` 和 Java 泛型系统确保放入 `stream` 的都是 `Shape` 对象(`Shape` 子类的对象也可视为 `Shape` 的对象),否则编译器会报错; +- 运行时,自动类型转换确保了从 `stream` 中取出的对象都是 `Shape` 类型。 -接下来就是多态机制的事了,`Shape`对象实际执行什么样的代码,是由引用所指向的具体对象(`Circle`、`Square`或者`Triangle`)决定的。这也符合我们编写代码的一般需求,通常,我们希望大部分代码尽可能少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(本例中即为`Shape`)。这样,代码会更容易写,更易读和维护;设计也更容易实现,更易于理解和修改。所以多态是面向对象的基本目标。 +接下来就是多态机制的事了,`Shape` 对象实际执行什么样的代码,是由引用所指向的具体对象(`Circle`、`Square` 或者 `Triangle`)决定的。这也符合我们编写代码的一般需求,通常,我们希望大部分代码尽可能少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(本例中即为 `Shape`)。这样,代码会更容易写,更易读和维护;设计也更容易实现,更易于理解和修改。所以多态是面向对象的基本目标。 -但是,有时你会碰到一些编程问题,在这些问题中如果你能知道某个泛化引用的具体类型,就可以把问题轻松解决。例如,假设我们允许用户将某些几何形状高亮显示,现在希望找到屏幕上所有高亮显示的三角形;或者,我们现在需要旋转所有图形,但是想跳过圆形(因为圆形旋转没有意义)。这时我们就希望知道`Stream`里边的形状具体是什么类型,而Java 实际上也满足了我们的这种需求。使用 RTTI,我们可以查询某个`Shape`引用所指向对象的确切类型,然后选择或者剔除特例。 +但是,有时你会碰到一些编程问题,在这些问题中如果你能知道某个泛化引用的具体类型,就可以把问题轻松解决。例如,假设我们允许用户将某些几何形状高亮显示,现在希望找到屏幕上所有高亮显示的三角形;或者,我们现在需要旋转所有图形,但是想跳过圆形(因为圆形旋转没有意义)。这时我们就希望知道 `Stream` 里边的形状具体是什么类型,而Java 实际上也满足了我们的这种需求。使用 RTTI,我们可以查询某个 `Shape` 引用所指向对象的确切类型,然后选择或者剔除特例。 -## `Class`对象 +## `Class` 对象 -要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为**`Class`对象**的特殊对象完成的,它包含了与类有关的信息。实际上,`Class`对象就是用来创建该类所有"常规"对象的。Java使用`Class`对象来实现RTTI,即便是类型转换这样的操作都是用`Class`对象实现的。不仅如此,`Class`类还提供了很多使用RTTI的其它方式。 +要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为 **`Class`对象** 的特殊对象完成的,它包含了与类有关的信息。实际上,`Class` 对象就是用来创建该类所有"常规"对象的。Java 使用 `Class` 对象来实现 RTTI,即便是类型转换这样的操作都是用 `Class` 对象实现的。不仅如此,`Class` 类还提供了很多使用RTTI的其它方式。 -类是程序的一部分,每个类都有一个`Class`对象。换言之,每当我们编写并且编译了一个新类,就会产生一个`Class`对象(更恰当的说,是被保存在一个同名的`.class`文件中)。为了生成这个类的对象,Java虚拟机(JVM)先会调用"类加载器"子系统把这个类加载到内存中。 +类是程序的一部分,每个类都有一个 `Class` 对象。换言之,每当我们编写并且编译了一个新类,就会产生一个 `Class` 对象(更恰当的说,是被保存在一个同名的 `.class` 文件中)。为了生成这个类的对象,Java虚拟机(JVM)先会调用"类加载器"子系统把这个类加载到内存中。 -类加载器子系统可能包含一条类加载器链,但有且只有一个**原生类加载器**,它是JVM实现的一部分。原生类加载器加载的是"可信类"(包括Java API类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。 +类加载器子系统可能包含一条类加载器链,但有且只有一个**原生类加载器**,它是JVM实现的一部分。原生类加载器加载的是”可信类”(包括Java API类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。 所有的类都是第一次使用时动态加载到JVM中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。 -> 其实构造器也是类的静态方法,虽然构造器前面并没有`static`关键字。所以,使用`new`操作符创建类的新对象,这个操作也算作对类的静态成员引用。 +> 其实构造器也是类的静态方法,虽然构造器前面并没有 `static` 关键字。所以,使用 `new` 操作符创建类的新对象,这个操作也算作对类的静态成员引用。 -因此,Java程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得Java具有一些静态加载语言(如C++)很难或者根本不可能实现的特性。 +因此,Java 程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得Java具有一些静态加载语言(如C++)很难或者根本不可能实现的特性。 -类加载器首先会检查这个类的`Class`对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找`.class`文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM会对其进行验证,确保它没有损坏,并且不包含不良的Java代码(这是Java安全防范的一种措施)。 +类加载器首先会检查这个类的 `Class` 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 `.class` 文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM会对其进行验证,确保它没有损坏,并且不包含不良的Java代码(这是Java安全防范的一种措施)。 -> 译者注:这里对类加载机制讲得不是很清楚,可以参考《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》第7章 - -一旦某个类的`Class`对象被载入内存,它就可以用来创建这个类的所有对象。下面的示范程序可以证明这点: +一旦某个类的 `Class` 对象被载入内存,它就可以用来创建这个类的所有对象。下面的示范程序可以证明这点: ```java // typeinfo/SweetShop.java // Examination of the way the class loader works class Cookie { - static { System.out.println("Loading Cookie"); } + static { System.out.println("Loading Cookie"); } } class Gum { - static { System.out.println("Loading Gum"); } + static { System.out.println("Loading Gum"); } } class Candy { - static { System.out.println("Loading Candy"); } + static { System.out.println("Loading Candy"); } } public class SweetShop { - public static void main(String[] args) { - System.out.println("inside main"); - new Candy(); - System.out.println("After creating Candy"); - try { - Class.forName("Gum"); - } catch(ClassNotFoundException e) { - System.out.println("Couldn't find Gum"); + public static void main(String[] args) { + System.out.println("inside main"); + new Candy(); + System.out.println("After creating Candy"); + try { + Class.forName("Gum"); + } catch(ClassNotFoundException e) { + System.out.println("Couldn't find Gum"); + } + System.out.println("After Class.forName(\"Gum\")"); + new Cookie(); + System.out.println("After creating Cookie"); } - System.out.println("After Class.forName(\"Gum\")"); - new Cookie(); - System.out.println("After creating Cookie"); - } } -/* Output: +``` + +输出结果: + +``` inside main Loading Candy After creating Candy @@ -140,12 +145,11 @@ Loading Gum After Class.forName("Gum") Loading Cookie After creating Cookie -*/ ``` -上面的代码中,`Candy`、`Gum`和`Cookie`这几个类都有一个`static{...}`静态初始化块,这些静态初始化块在类第一次被加载的时候就会执行。也就是说,静态初始化块会打印出相应的信息,告诉我们这些类分别是什么时候被加载了。而在`main()`里边,创建对象 的代码都放在了`print()`语句之间,以帮助我们判断类加载的时间点。 +上面的代码中,`Candy`、`Gum` 和 `Cookie` 这几个类都有一个 `static{...}` 静态初始化块,这些静态初始化块在类第一次被加载的时候就会执行。也就是说,静态初始化块会打印出相应的信息,告诉我们这些类分别是什么时候被加载了。而在 `main()` 里边,创建对象 的代码都放在了 `print()` 语句之间,以帮助我们判断类加载的时间点。 -从输出中可以看到,`Class`对象仅在需要的时候才会被加载,`static`初始化是在类加载时进行的。 +从输出中可以看到,`Class` 对象仅在需要的时候才会被加载,`static` 初始化是在类加载时进行的。 代码里面还有特别有趣的一行: @@ -153,11 +157,399 @@ After creating Cookie Class.forName("Gum"); ``` -所有`Class`对象都属于`Class`类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。`forName()`是`Class`类的一个静态方法,我们可以使用`forName()`根据目标类的类名(`String`)得到该类的`Class`对象。上面的代码忽略了`forName()`的返回值,因为那个调用是为了得到它产生的"副作用"。从结果可以看出,`forName()`执行的副作用是如果`Gum`类没有被加载就加载它,而在加载的过程中,`Gum`的`static`初始化块被执行了。 +所有 `Class` 对象都属于 `Class` 类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。`forName()` 是 `Class` 类的一个静态方法,我们可以使用 `forName()` 根据目标类的类名(`String`)得到该类的 `Class` 对象。上面的代码忽略了 `forName()` 的返回值,因为那个调用是为了得到它产生的“副作用”。从结果可以看出,`forName()` 执行的副作用是如果`Gum`类没有被加载就加载它,而在加载的过程中,`Gum` 的 `static` 初始化块被执行了。 -还需要注意的是,如果`Class.forName()`找不到要加载的类,它就会抛出异常`ClassNotFoundException`。上面的例子中我们只是简单地报告了问题,但在更严密的程序里,就要在异常处理程序中解决这个问题。 +还需要注意的是,如果 `Class.forName()` 找不到要加载的类,它就会抛出异常 `ClassNotFoundException`。上面的例子中我们只是简单地报告了问题,但在更严密的程序里,就要考虑在异常处理程序中把问题解决掉(具体例子详见[设计模式](./25-Patterns)章节)。 +无论何时,只要你想在运行时使用类型信息,就必须先得到那个 `Class` 对象的引用。`Class.forName()` 就是实现这个功能的一个便捷途径,因为使用该方法你不需要先持有这个类型 的对象。但是,如果你已经拥有了目标类的对象,那就可以通过调用 `getClass()` 方法来获取 `Class` 引用了,这个方法来自根类 `Object`,它将返回表示该对象实际类型的 `Class`对象的引用。`Class` 包含很多有用的方法,下面代码展示了其中的一部分: +```java +// typeinfo/toys/ToyTest.java +// Testing class Class +// {java typeinfo.toys.ToyTest} +package typeinfo.toys; + +interface HasBatteries {} +interface Waterproof {} +interface Shoots {} + +class Toy { + // Comment out the following no-arg + // constructor to see NoSuchMethodError + Toy() {} + Toy(int i) {} +} + +class FancyToy extends Toy +implements HasBatteries, Waterproof, Shoots { + FancyToy() { super(1); } +} + +public class ToyTest { + static void printInfo(Class cc) { + System.out.println("Class name: " + cc.getName() + + " is interface? [" + cc.isInterface() + "]"); + System.out.println( + "Simple name: " + cc.getSimpleName()); + System.out.println( + "Canonical name : " + cc.getCanonicalName()); + } + public static void main(String[] args) { + Class c = null; + try { + c = Class.forName("typeinfo.toys.FancyToy"); + } catch(ClassNotFoundException e) { + System.out.println("Can't find FancyToy"); + System.exit(1); + } + printInfo(c); + for(Class face : c.getInterfaces()) + printInfo(face); + Class up = c.getSuperclass(); + Object obj = null; + try { + // Requires no-arg constructor: + obj = up.newInstance(); + } catch(InstantiationException e) { + System.out.println("Cannot instantiate"); + System.exit(1); + } catch(IllegalAccessException e) { + System.out.println("Cannot access"); + System.exit(1); + } + printInfo(obj.getClass()); + } +} +``` + +输出结果: + +``` +Class name: typeinfo.toys.FancyToy is interface? +[false] +Simple name: FancyToy +Canonical name : typeinfo.toys.FancyToy +Class name: typeinfo.toys.HasBatteries is interface? +[true] +Simple name: HasBatteries +Canonical name : typeinfo.toys.HasBatteries +Class name: typeinfo.toys.Waterproof is interface? +[true] +Simple name: Waterproof +Canonical name : typeinfo.toys.Waterproof +Class name: typeinfo.toys.Shoots is interface? [true] +Simple name: Shoots +Canonical name : typeinfo.toys.Shoots +Class name: typeinfo.toys.Toy is interface? [false] +Simple name: Toy +Canonical name : typeinfo.toys.Toy +``` + +`FancyToy` 继承自 `Toy` 并实现了 `HasBatteries`、`Waterproof` 和 `Shoots` 接口。在 `main()` 中,我们创建了一个 `Class` 引用,然后在 `try` 语句里边用 `forName()` 方法创建了一个 `FancyToy` 的类对象并赋值给该引用。需要注意的是,传递给 `forName()` 的字符串必须使用类的全限定名(包含包名)。 + +`printInfo()` 函数使用 `getName()` 来产生完整类名,使用 `getSimpleName()` 产生不带包名的类名,`getCanonicalName()` 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 `getName()` 相同)。`isInterface()` 用于判断某个 `Class` 对象代表的是否为一个接口。因此,通过 `Class` 对象,你可以得到关于该类型的所有信息。 + +在 `main()` 中调用的 `Class.getInterface()` 方法返回的是存放 `Class` 对象的数组,里面的 `Class` 对象表示的是那个类实现的接口。 + +另外,你还可以调用 `getSuperclass()` 方法来得到父类的 `Class` 对象,再用父类的 `Class` 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。 + +`Class` 对象的 `newInstance()` 方法是实现“虚拟构造器”的一种途径,虚拟构造器可以让你在不知道一个类的确切类型的时候,创建这个类的对象。在前面的例子中,`up` 只是一个 `Class` 对象的引用,在编译期并不知道这个引用会指向哪个类的 `Class` 对象。当你创建新实例时,会得到一个 `Object` 引用,但是这个引用指向的是 `Toy` 对象。当然,由于得到的是 `Object` 引用,目前你只能给它发送 `Object` 对象能够接受的调用。而如果你想请求具体对象才有的调用,你就得先获取该对象更多的类型信息,并执行某种转型。另外,使用 `newInstance()` 来创建的类,必须带有无参数的构造书。在本章稍后部分,你将会看到如何通过Java的反射API,用任意的构造器来动态的创建类的对象。 + +### 类字面常量 + +Java还提供了另一种方法来生成类对象的引用:**类字面常量**。对上述程序来说,就像这样:`FancyToy.class;`。这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不必放在`try`语句块中)。并且它根除了对 `forName()` 方法的调用,所以效率更高。 + +类字面常量不仅不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段 `TYPE`。`TYPE`字段是一个引用,指向对应的基本数据类型的 `Class` 对象,如下所示: + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
...等价于...
boolean.classBoolean.TYPE
char.classCharacter.TYPE
byte.classByte.TYPE
short.classShort.TYPE
int.classInteger.TYPE
long.classLong.TYPE
float.classFloat.TYPE
double.classDouble.TYPE
void.classVoid.TYPE
+
+我的建议是使用 `.class` 的形式, 以保持与普通类的一致性。 + +注意,有一点很有趣:当使用 `.class` 来创建对 `Class` 对象的引用时,不会自动地初始化该`Class` 对象。为了使用类而做的准备工作实际包含三个步骤: + +1. **加载**,这是由类加载器执行的。该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必须的),并从这些字节码中创建一个 `Class` 对象。 + +2. **链接**。在链接阶段将验证类中的字节码,为 `static` 域分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。 + +3. **初始化**。如果该类具有超类,则对其进行初始化,执行 `static` 初始化器和 `static` 初始化块。 + +初始化被延迟到了对 `static` 方法(构造器隐式地是 `static` 的)或者非常数 `static` 域进行首次引用时才执行: + +```java +// typeinfo/ClassInitialization.java +import java.util.*; + +class Initable { + static final int STATIC_FINAL = 47; + static final int STATIC_FINAL2 = + ClassInitialization.rand.nextInt(1000); + static { + System.out.println("Initializing Initable"); + } +} + +class Initable2 { + static int staticNonFinal = 147; + static { + System.out.println("Initializing Initable2"); + } +} + +class Initable3 { + static int staticNonFinal = 74; + static { + System.out.println("Initializing Initable3"); + } +} + +public class ClassInitialization { + public static Random rand = new Random(47); + public static void + main(String[] args) throws Exception { + Class initable = Initable.class; + System.out.println("After creating Initable ref"); + // Does not trigger initialization: + System.out.println(Initable.STATIC_FINAL); + // Does trigger initialization: + System.out.println(Initable.STATIC_FINAL2); + // Does trigger initialization: + System.out.println(Initable2.staticNonFinal); + Class initable3 = Class.forName("Initable3"); + System.out.println("After creating Initable3 ref"); + System.out.println(Initable3.staticNonFinal); + } +} +``` + +输出结果: + +``` +After creating Initable ref +47 +Initializing Initable +258 +Initializing Initable2 +147 +Initializing Initable3 +After creating Initable3 ref +74 +``` + +初始化有效地实现了尽可能的“惰性”,从对 `initable` 引用的创建中可以看到,仅使用 `.class` 语法来获得对类对象的引用不会引发初始化。但与此相反,使用 `Class.forName()` 来产生 `Class` 引用会立即就进行初始化,如 `initable3`。 + +如果一个 `static final` 值是“编译期常量”(如 `Initable.staticFinal`),那么这个值不需要对 `Initable` 类进行初始化就可以被读取。但是,如果只是将一个域设置成为 `static` 和 `final`,还不足以确保这种行为。例如,对 `Initable.staticFinal2` 的访问将强制进行类的初始化,因为它不是一个编译期常量。 + +如果一个 `static` 域不是 `final` 的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间),就像在对 `Initable2.staticNonFinal` 的访问中所看到的那样。 + +### 泛化的 `Class` 引用 + +`Class`引用总是指向某个 `Class` 对象,而 `Class` 对象可以用于产生类的实例,并且包含可作用于这些实例的所有方法代码。它还包含该类的 `static` 成员,因此 `Class` 引用表明了它所指向对象的确切类型,而该对象便是 `Class` 类的一个对象。 + + + +但是,Java设计者看准机会,将它的类型变得更具体了一些。Java引入泛型语法之后,我们可以使用泛型对 `Class` 引用所指向的 `Class` 对象的类型进行限定。在下面的实例中,两种语法都是正确的: + +```java +// typeinfo/GenericClassReferences.java + +public class GenericClassReferences { + public static void main(String[] args) { + Class intClass = int.class; + Class genericIntClass = int.class; + genericIntClass = Integer.class; // Same thing + intClass = double.class; + // genericIntClass = double.class; // Illegal + } +} +``` + +普通的类引用不会产生警告信息。你可以看到,普通的类引用可以重新赋值指向任何其他的 `Class` 对象,但是使用泛型限定的类引用只能指向其声明的类型。通过使用泛型语法,我们可以让编译器强制执行额外的类型检查。 + +那如果我们希望稍微放松一些限制,应该怎么办呢?乍一看,下面的操作好像是可以的: + +```java +Class geenericNumberClass = int.class; +``` + +这看起来似乎是起作用的,因为 `Integer` 继承自 `Number`。但事实却是不行,因为 `Integer` 的 `Class` 对象并不是 `Number`的 `Class` 对象的子类(这看起来可能有点诡异,我们将在[泛型](./20-Generics)这一章详细讨论)。 + +为了在使用 `Class` 引用时放松限制,我们使用了通配符,它是Java泛型中的一部分。通配符就是 `?`,表示“任何事物”。因此,我们可以在上例的普通 `Class` 引用中添加通配符,并产生相同的结果: + +```java +// typeinfo/WildcardClassReferences.java + +public class WildcardClassReferences { + public static void main(String[] args) { + Class intClass = int.class; + intClass = double.class; + } +} +``` + +使用 `Class` 比单纯使用 `Class` 要好,虽然它们是等价的,并且单纯使用 `Class` 不会产生编译器警告信息。使用 `Class` 的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。 + +为了创建一个限定指向某种类型或其子类的 `Class` 引用,我们需要将通配符与 `extends` 关键字配合使用,创建一个范围限定。这与仅仅声明 `Class` 不同,现在做如下声明: + +```java +// typeinfo/BoundedClassReferences.java + +public class BoundedClassReferences { + public static void main(String[] args) { + Class bounded = int.class; + bounded = double.class; + bounded = Number.class; + // Or anything else derived from Number. + } +} +``` + +向 `Class` 引用添加泛型语法的原因只是为了提供编译期类型检查,因此如果你操作有误,稍后就会发现这点。使用普通的 `Class` 引用你要确保自己不会犯错,因为一旦你犯了错误,就要等到运行时你才能发现它,这并不是很方便。 + +下面的示例使用了泛型语法,它保存了一个类引用,稍后又用 `newInstance()` 方法产生类的对象: + +```java +// typeinfo/DynamicSupplier.java +import java.util.function.*; +import java.util.stream.*; + +class CountedInteger { + private static long counter; + private final long id = counter++; + @Override + public String toString() { return Long.toString(id); } +} + +public class DynamicSupplier implements Supplier { + private Class type; + public DynamicSupplier(Class type) { + this.type = type; + } + public T get() { + try { + return type.newInstance(); + } catch(InstantiationException | + IllegalAccessException e) { + throw new RuntimeException(e); + } + } + public static void main(String[] args) { + Stream.generate( + new DynamicSupplier<>(CountedInteger.class)) + .skip(10) + .limit(5) + .forEach(System.out::println); + } +} +``` + +输出结果: + +``` +10 +11 +12 +13 +14 +``` + +注意,这个类必须假设与它与它一起工作的任何类型都有一个无参构造器,否者运行时会抛出异常。编译期对该程序不会产生任何警告信息。 + +当你将泛型语法用于 `Class` 对象时,`newInstance()` 将返回该对象的确切类型,而不仅仅只是在 `ToyTest.java` 中看到的基类 `Object`。然而,这在某种程度上有些受限: + +```java +// typeinfo/toys/GenericToyTest.java +// Testing class Class +// {java typeinfo.toys.GenericToyTest} +package typeinfo.toys; + +public class GenericToyTest { + public static void + main(String[] args) throws Exception { + Class ftClass = FancyToy.class; + // Produces exact type: + FancyToy fancyToy = ftClass.newInstance(); + Class up = + ftClass.getSuperclass(); + // This won't compile: + // Class up2 = ftClass.getSuperclass(); + // Only produces Object: + Object obj = up.newInstance(); + } +} +``` + +如果你手头的是超类,那编译期将只允许你声明超类引用为“某个类,它是 `FancyToy` 的超类”,就像在表达式 `Class` 中所看到的那样。而不会接收 `Class` 这样的声明。这看上去显得有些怪,因为 `getSuperClass()` 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 `Toy.class`),而不仅仅只是“某个类,它是 `FancyToy` 的超类”。不管怎样,正是由于这种含糊性,`up.newInstance` 的返回值不是精确类型,而只是 `Object`。 + +### `cast()` 方法 + +Java中还有用于 `Class` 引用的转型语法,即 `cast()` 方法: + +```java +// typeinfo/ClassCasts.java + +class Building {} +class House extends Building {} + +public class ClassCasts { + public static void main(String[] args) { + Building b = new House(); + Class houseType = House.class; + House h = houseType.cast(b); + h = (House)b; // ... or just do this. + } +} +``` + +`cast()` 方法接受参数对象,并将其转型为 `Class` 引用的类型。但是,如果观察上面的代码,你就会发现,与实现了相同功能的 `main()` 中最后一行相比,这种转型好像做了很多额外的工作。 + +`cast()` 在无法使用普通转型的情况下会显得非常有用,在你编写泛型代码(你将在[泛型](./20-Generics)这一章学习到)时,如果你保存了 `Class` 引用,并希望以后通过这个引用来执行转型,你就需要用到 `cast()`。但事实却是这种情况并不常见,我发现整个Java类库中,只有一处使用了 `cast()`(在 `com.sun.mirror.util.DeclarationFilter` 中)。 + +Java类库中另一个没有任何用处的特性就是 `Class.asSubclass()`,该方法允许你将一个 `Class` 对象转型为更加具体的类型。 @@ -171,8 +563,8 @@ Class.forName("Gum"); ## 类的等价比较 - + ## 反射运行时类信息 diff --git a/docs/book/23-Annotations.md b/docs/book/23-Annotations.md index 1f386a5..63da4aa 100644 --- a/docs/book/23-Annotations.md +++ b/docs/book/23-Annotations.md @@ -1,30 +1,1809 @@ [TOC] + # 第二十三章 注解 +注解(也被成为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。 + +注解在一定程度上是把元数据和源代码文件结合在一起的趋势所激发的,而不是保存在外部文档。这同样是对像 C# 语言对于 Java 语言特性压力的一种回应。 + +注解是 Java 5 所引入的众多语言变化之一。它们提供了 Java 无法表达的但是你需要完整表述程序所需的信息。因此,注解使得我们可以以编译器验证的格式存储程序的额外信息。注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,你可以将元数据保存在 Java 源代码中。并拥有如下有下优势:简单易读的代码,编译器类型检查,使用 annotation API 为自己的注解构造处理工具。即使 Java 定义了一些类型的元数据,但是一般来说注解类型的添加和如何使用完全取决于你。 + +注解的语法十分简单,主要是在现有语法中添加 @ 符号。Java 5 引入了前三种定义在 **java.lang** 包中的注解: + +- **@Override**:表示当前的方法定义将覆盖基类的方法。如果你不小心拼写错误,或者方法签名被错误拼写的时候,编译器就会发出错误提示。 +- **@Deprecated**:如果使用该注解的元素被调用,编译器就会发出警告信息。 +- **@SuppressWarnings**:关闭不当的编译器警告信息。 +- **@SafeVarargs**:在 Java 7 中加入用于禁止对具有泛型varargs参数的方法或构造函数的调用方发出警告。 +- **@FunctionalInterface**:Java 8 中加入用于表示类型声明为函数式接口 + +还有 5 种额外的注解类型用于创造新的注解。你将会在这一章学习它们。 + +每当创建涉及重复工作的类或接口时,你通常可以使用注释来自动化和简化流程。例如在 Enterprise JavaBean(EJB)中的许多额外工作就是通过注解来消除的。 + +注解的出现可以替代一些现有的系统,例如 XDoclet,它是一种独立的文档化工具,专门设计用来生成注解风格的文档。与之相比,注解是真正语言层级的概念,以前构造出来就享有编译器的类型检查保护。注解在源代码级别保存所有信息而不是通过注释文字,这使得代码更加整洁和便于维护。通过使用拓展的 annotation API 或稍后在本章节可以看到的外部的字节码工具类库,你会拥有对源代码及字节码强大的检查与操作能力。 + ## 基本语法 - + +在下面的例子中,使用 `@Test` 对 `testExecute()` 进行注解。该注解本身不做任何事情,但是编译器要保证其类路径上有 `@Test` 注解的定义。你将在本章看到,我们通过注解创建了一个工具用于运行这个方法: + +```java +// annotations/Testable.java +package annotations; +import onjava.atunit.*; +public class Testable { + public void execute() { + System.out.println("Executing.."); + } + @Test + void testExecute() { execute(); } +} +``` + +被注解标注的方法和其他的方法没有任何区别。在这个例子中,注解 `@Test` 可以和任何修饰符共同用于方法,注入 **public**、**static** 或 **void**。用语法的角度上看,注解和使用方式和修饰符的使用方式一致。 + +### 定义注解 + +如下是一个注解的定义。注解的定义看起来很像接口的定义。事实上,它们和其他 Java 接口一样,也会被编译成 class 文件。 + +```java +// onjava/atunit/Test.java +// The @Test tag +package onjava.atunit; +import java.lang.annotation.*; +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Test {} +``` + +除了 @ 符号之外, `@Test` 的定义看起来更像一个空接口。注解的定义也需要一些元注解(meta-annoation),比如 `@Target` 和 `@Retention`。`@Target` 定义你的注解可以应用在哪里(例如是方法还是字段)。`@Retention` 定义了注解在哪里可用,在源代码中(SOURCE),class文件(CLASS)中或者是在运行时(RUNTIME)。 + +注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。 + +不包含任何元素的注解称为标记注解(marker annotation),例如上例中的 `@Test` 就是标记注解。 + +下面是一个简单的注解,我们可以用它来追踪项目中的用例。程序员可以使用该注解来标注满足特定用例的一个方法或者一组方法。于是,项目经理可以通过统计已经实现的用例来掌控项目的进展,而开发者在维护项目时可以轻松的找到用例用于更新,或者他们可以调试系统中业务逻辑。 + +```java +// annotations/UseCase.java +import java.lang.annotation.*; +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface UseCase { + int id(); + String description() default "no description"; +} +``` + +注意 **id** 和 **description** 与方法定义类似。由于编译器会对 **id** 进行类型检查,因此将跟踪数据库与用例文档和源代码相关联是可靠的方式。**description** 元素拥有一个 **default** 值,如果在注解某个方法时没有给出 **description** 的值。则该注解的处理器会使用此元素的默认值。 + +在下面的类中,有三个方法被注解为用例: + +```java +// annotations/PasswordUtils.java +import java.util.*; +public class PasswordUtils { + @UseCase(id = 47, description = + "Passwords must contain at least one numeric") + public boolean validatePassword(String passwd) { + return (passwd.matches("\\w*\\d\\w*")); + } + @UseCase(id = 48) + public String encryptPassword(String passwd) { + return new StringBuilder(passwd) + .reverse().toString(); + } + @UseCase(id = 49, description = + "New passwords can't equal previously used ones") + public boolean checkForNewPassword( + List prevPasswords, String passwd) { + return !prevPasswords.contains(passwd); + } +} +``` + +注解的元素在使用时表现为 名-值 对的形式,并且需要放置在 `@UseCase` 声明之后的括号内。在 `encryptPassword()` 方法的注解中,并没有给出 **description** 的默认值,所以在 **@interface UseCase** 的注解处理器分析处理这个类的时候会使用该元素的默认值。 + +你应该能够想象到如何使用这套工具来“勾勒”出将要建造的凶,然后在建造的过程中逐渐实现系统的各项功能。 + +### 元注解 + +Java 语言中目前有 5 种标准注解(前面介绍过),以及 5 种元注解。元注解用于注解其他的注解 + +| 注解 | 解释 | +| ----------- | ------------------------------------------------------------ | +| @Target | 表示注解可以用于哪些地方。可能的 **ElementType** 参数包括:
**CONSTRUCTOR**:构造器的声明
**FIELD**:字段声明(包括 enum 实例)
**LOCAL_VARIABLE**:局部变量声明
**METHOD**:方法声明
**PACKAGE**:包声明
**PARAMETER**:参数声明
**TYPE**:类、接口(包括注解类型)或者 enum 声明 | +| @Retention | 表示注解信息保存的时长。可选的 **RetentionPolicy** 参数包括:
**SOURCE**:注解将被编译器丢弃
**CLASS**:注解在 class 文件中可用,但是会被 VM 丢弃。
**RUNTIME**:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。 | +| @Documented | 将此注解保存在 Javadoc 中 | +| @Interited | 允许子类继承父类的注解 | +| @Repeatable | 允许一个注解可以被使用一次或者多次(Java 8)。 | + +大多数时候,程序员定义自己的注解,并编写自己的处理器来处理他们。 + ## 编写注解处理器 +如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。同时他还提供了 javac 编译器钩子在编译时使用注解。 + +下面是一个非常简单的注解处理器,我们用它来读取被注解的 **PasswordUtils** 类,并且使用反射机制来寻找 **@UseCase** 标记。给定一组 **id** 值,然后列出在 **PasswordUtils** 中找到的用例,以及缺失的用例。 + +```java +// annotations/UseCaseTracker.java +import java.util.*; +import java.util.stream.*; +import java.lang.reflect.*; +public class UseCaseTracker { + public static void + trackUseCases(List useCases, Class cl) { + for(Method m : cl.getDeclaredMethods()) { + UseCase uc = m.getAnnotation(UseCase.class); + if(uc != null) { + System.out.println("Found Use Case " + + uc.id() + "\n " + uc.description()); + useCases.remove(Integer.valueOf(uc.id())); + } + } + useCases.forEach(i -> + System.out.println("Missing use case " + i)); + } + public static void main(String[] args) { + List useCases = IntStream.range(47, 51) + .boxed().collect(Collectors.toList()); + trackUseCases(useCases, PasswordUtils.class); + } +} +``` + +输出为: + +```java +Found Use Case 48 +no description +Found Use Case 47 +Passwords must contain at least one numeric +Found Use Case 49 +New passwords can't equal previously used ones +Missing use case 50 +``` + +这个程序用了两个反射的方法:`getDeclaredMethods()` 和 `getAnnotation()`,它们都属于 **AnnotatedElement** 接口(**Class**,**Method** 与 **Field** 类都实现了该接口)。`getAnnotation()` 方法返回指定类型的注解对象,在本例中就是 “**UseCase**”。如果被注解的方法上没有该类型的注解,返回值就为 **null**。我们通过调用 `id()` 和 `description()` 方法来提取元素值。注意 `encryptPassword()` 方法在注解的时候没有指定 **description** 的值,因此处理器在处理它对应的注解时,通过 `description()` 取得的是默认值 “no description”。 + +### 注解元素 + +在 **UseCase.java** 中定义的 **@UseCase** 的标签包含 int 元素 **id** 和 String 元素 **description**。注解元素可用的类型如下所示: + +- 所有基本类型(int、float、boolean等) +- String +- Class +- enum +- Annotation +- 以上类型的数组 + +如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。 + +### 默认值限制 + +编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值。 + +这里有另外一个限制:任何非基本类型的元素, 无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。这个限制使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在。 + +```java +// annotations/SimulatingNull.java +import java.lang.annotation.*; +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SimulatingNull { + int id() default -1; + String description() default ""; +} +``` + +这是一个在定义注解的习惯用法。 + +### 生成外部文件 + +当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。像 Enterprise JavaBeans (EJB3 之前)这样的技术,每一个 Bean 都需要需要大量的接口和部署描述文件,而这些就是“样板”文件。Web Service,自定义标签库以及对象/关系映射工具(例如 Toplink 和 Hibernate)通常都需要 XML 描述文件,而这些文件脱离于代码之外。除了定义 Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中已经提供的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时这也要求了为项目工作的程序员在知道如何编写 Java 程序的同时,也必须知道如何编辑描述文件。 + +假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 **JavaBean** 源文件中。为此你需要一些用于定义数据库名称、数据库列以及将 SQL 类型映射到属性的注解。 + +以下是一个注解的定义,它告诉注解处理器应该创建一个数据库表: + +```java +// annotations/database/DBTable.java +package annotations.database; +import java.lang.annotation.*; +@Target(ElementType.TYPE) // Applies to classes only +@Retention(RetentionPolicy.RUNTIME) +public @interface DBTable { + String name() default ""; +} +``` + +在 `@Target` 注解中指定的每一个 **ElementType** 就是一个约束,它告诉编译器,这个自定义的注解只能用于指定的类型。你可以指定 **enum ElementType** 中的一个值,或者以逗号分割的形式指定多个值。如果想要将注解应用于所有的 **ElementType**,那么可以省去 `@Target` 注解,但是这并不常见。 + +注意 **@DBTable** 中有一个 `name()` 元素,该注解通过这个元素为处理器创建数据库时提供表的名字。 + +如下是修饰字段的注解: + +```java +// annotations/database/Constraints.java +package annotations.database; +import java.lang.annotation.*; +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Constraints { + boolean primaryKey() default false; + boolean allowNull() default true; + boolean unique() default false; +} +``` + +```java +// annotations/database/SQLString.java +package annotations.database; +import java.lang.annotation.*; +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SQLString { + int value() default 0; + String name() default ""; + Constraints constraints() default @Constraints; +} +``` + +```java +// annotations/database/SQLInteger.java +package annotations.database; +import java.lang.annotation.*; +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SQLInteger { + String name() default ""; + Constraints constraints() default @Constraints; +} +``` + +**@Constraints** 注解允许处理器提供数据库表的元数据。**@Constraints** 代表了数据库通常提供的约束的一小部分,但是它索要表达的思想已经很清楚了。`primaryKey()`,`allowNull()` 和 `unique()` 元素明智的提供了默认值,从而使得在大多数情况下,该注解的使用者不需要输入太多东西。 + +另外两个 **@interface** 定义的是 SQL 类型。如果希望这个框架更有价值的话,我们应该为每个 SQL 类型都定义相应的注解。不过为为示例,两个元素足够了。 + +这些 SQL 类型具有 `name()` 元素和 `constraints()` 元素。后者利用了嵌套注解的功能,将数据库列的类型约束信息嵌入其中。注意 `constraints()` 元素的默认值时 **@Constraints**。由于在 **@Constraints** 注解类型之后,没有在括号中指明 **@Constraints** 元素的值,因此,**constraints()** 的默认值为所有元素都为默认值的 **@Constraints** 注解。如果要使得嵌入的 **@Constraints** 注解中的 `unique()` 元素为 true,并作为 `constraints()` 元素的默认值,你可以像如下定义: + +```java +// annotations/database/Uniqueness.java +// Sample of nested annotations +package annotations.database; +public @interface Uniqueness { + Constraints constraints() + default @Constraints(unique = true); +} +``` + +下面是一个简单的,使用了如上注解的类: + +```java +// annotations/database/Member.java +package annotations.database; +@DBTable(name = "MEMBER") +public class Member { + @SQLString(30) String firstName; + @SQLString(50) String lastName; + @SQLInteger Integer age; + @SQLString(value = 30, + constraints = @Constraints(primaryKey = true)) + String reference; + static int memberCount; + public String getReference() { return reference; } + public String getFirstName() { return firstName; } + public String getLastName() { return lastName; } + @Override + public String toString() { return reference; } + public Integer getAge() { return age; } +} +``` + +类注解 **@DBTable** 注解给定了元素值 MEMBER,它将会作为标的名字。类的属性 **firstName** 和 **firstName** 都被注解为 **@SQLString** 类型并且给了默认元素值分别为 30 和 50。这些注解都有两个有趣的地方:首先,他们都使用了嵌入的 **@Constraints** 注解的默认值;其次,它们都是用了快捷方式特性。如果你在注解中定义了名为 **value** 的元素,并且在使用该注解时,**value** 为唯一一个需要赋值的元素,你就不需要使用名—值对的语法,你只需要在括号中给出 **value** 元素的值即可。这可以应用于任何合法类型的元素。这也限制了你必须将元素命名为 **value**,不过在上面的例子中,这样的注解语句也更易于理解: + +```java +@SQLString(30) +``` + +处理器将在穿件表的时候使用该值设置 SQL 列的大小。 + +默认值的语法虽然很灵巧,但是它很快就变的复杂起来。以 **reference** 字段的注解为例,上面拥有 **@SQLString** 注解,但是这个字段也将成为表的主键,因此在嵌入的 **@Constraint** 注解中设定 **primaryKey** 元素的值。这时事情就变的复杂了。你不得不为这个嵌入的注解使用很长的名—值对的形式,来指定元素名称和 **@interface** 的名称。同时,由于有特殊命名的 **value** 也不是唯一需要赋值的元素,因此不能再使用快捷方式特性。如你所见,最终结果不算清晰易懂。 + +### 替代方案 + +可以使用多种不同的方式来定义自己的注解用于上述任务。例如,你可以使用一个单一的注解类 **@TableColumn**,它拥有一个 **enum** 元素,元素值定义了 **STRING**,**INTEGER**,**FLOAT** 等类型。这消除了每个 SQL 类型都需要定义一个 **@interface** 的负担,不过也使得用额外信息修饰 SQL 类型变的不可能,这些额外的信息例如长度或精度等,都可能是非常有用的。 + +你也可以使用一个 **String** 类型的元素来描述实际的 SQL 类型,比如 “VARCHAR(30)” 或者 “INTEGER”。这使得你可以修饰 SQL 类型,但是这也将 Java 类型到 SQL 类型的映射绑在了一起,这不是一个好的设计。你并不想在数据库更改之后重新编译你的代码;如果我们只需要告诉注解处理器,我们正在使用的是什么“口味(favor)”的 SQL,然后注解助力器来为我们处理 SQL 类型的细节,那将是一个优雅的设计。 + +第三种可行的方案是一起使用两个注解,**@Constraints** 和相应的 SQL 类型(例如,**@SQLInteger**)去注解同一个字段。这可能会让代码有些混乱,但是编译器允许你对同一个目标使用多个注解。在 Java 8,在使用多个注解的时候,你可以重复使用同一个注解。 + +### 注解不支持继承 + +你不能使用 **extends** 关键字来继承 **@interfaces**。这真是一个遗憾,如果可以定义**@TableColumn** 注解(参考前面的建议),同时嵌套一个 **@SQLType** 类型的注解,讲究将成为一个优雅的设计。按照这种方式,你可以通过继承 **@SQLType** 来创造各种 SQL 类型。例如 **@SQLInteger** 和 **@SQLString**。如果支持继承,就会大大减少打字的工作量并且使得语法更整洁。在 Java 的未来版本中,似乎没有任何关于让注解支持继承的提案,所以在当前情况下,上例中的解决方案可能已经是最佳方案了。 + +### 实现处理器 + +下面是一个注解处理器的例子,他将读取一个类文件,检查上面的数据库注解,并生成用于创建数据库的 SQL 命令: + +```java +// annotations/database/TableCreator.java +// Reflection-based annotation processor +// {java annotations.database.TableCreator +// annotations.database.Member} +package annotations.database; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +public class TableCreator { + public static void + main(String[] args) throws Exception { + if (args.length < 1) { + System.out.println( + "arguments: annotated classes"); + System.exit(0); + } + for (String className : args) { + Class cl = Class.forName(className); + DBTable dbTable = cl.getAnnotation(DBTable.class); + if (dbTable == null) { + System.out.println( + "No DBTable annotations in class " + + className); + continue; + } + String tableName = dbTable.name(); + // If the name is empty, use the Class name: + if (tableName.length() < 1) + tableName = cl.getName().toUpperCase(); + List columnDefs = new ArrayList<>(); + for (Field field : cl.getDeclaredFields()) { + String columnName = null; + Annotation[] anns = + field.getDeclaredAnnotations(); + if (anns.length < 1) + continue; // Not a db table column + if (anns[0] instanceof SQLInteger) { + SQLInteger sInt = (SQLInteger) anns[0]; + // Use field name if name not specified + if (sInt.name().length() < 1) + columnName = field.getName().toUpperCase(); + else + columnName = sInt.name(); + columnDefs.add(columnName + " INT" + + getConstraints(sInt.constraints())); + } + if (anns[0] instanceof SQLString) { + SQLString sString = (SQLString) anns[0]; + // Use field name if name not specified. + if (sString.name().length() < 1) + columnName = field.getName().toUpperCase(); + else + columnName = sString.name(); + columnDefs.add(columnName + " VARCHAR(" + + sString.value() + ")" + + getConstraints(sString.constraints())); + } + StringBuilder createCommand = new StringBuilder( + "CREATE TABLE " + tableName + "("); + for (String columnDef : columnDefs) + createCommand.append( + "\n " + columnDef + ","); + // Remove trailing comma + String tableCreate = createCommand.substring( + 0, createCommand.length() - 1) + ");"; + System.out.println("Table Creation SQL for " + + className + " is:\n" + tableCreate); + } + } + } + + private static String getConstraints(Constraints con) { + String constraints = ""; + if (!con.allowNull()) + constraints += " NOT NULL"; + if (con.primaryKey()) + constraints += " PRIMARY KEY"; + if (con.unique()) + constraints += " UNIQUE"; + return constraints; + } +} +``` + +输出为: + +```sql +Table Creation SQL for annotations.database.Member is: +CREATE TABLE MEMBER( + FIRSTNAME VARCHAR(30)); +Table Creation SQL for annotations.database.Member is: +CREATE TABLE MEMBER( + FIRSTNAME VARCHAR(30), + LASTNAME VARCHAR(50)); +Table Creation SQL for annotations.database.Member is: +CREATE TABLE MEMBER( + FIRSTNAME VARCHAR(30), + LASTNAME VARCHAR(50), + AGE INT); +Table Creation SQL for annotations.database.Member is: +CREATE TABLE MEMBER( + FIRSTNAME VARCHAR(30), + LASTNAME VARCHAR(50), + AGE INT, + REFERENCE VARCHAR(30) PRIMARY KEY); +``` + +`main()` 方法会循环处理命令行传入的每一个类名。每一个类都是用 ` forName()` 方法进行加载,并使用 `getAnnotation(DBTable.class)` 来检查该类是否带有 **@DBTable** 注解。如果存在,将表名存储起来。然后读取这个类的所有字段,并使用 `getDeclaredAnnotations()` 进行检查。这个方法返回一个包含特定字段上所有注解的数组。然后使用 **instanceof** 操作符判断这些注解是否是 **@SQLInteger** 或者 **@SQLString** 类型。如果是的话,在对应的处理块中将构造出相应的数据库列的字符串片段。注意,由于注解没有继承机制,如果要获取近似多台的行为,使用 `getDeclaredAnnotations()` 似乎是唯一的方式。 + +嵌套的 **@Constraint** 注解被传递给 `getConstraints()`方法,并用它来构造一个包含 SQL 约束的 String 对象。 + +需要提醒的是,上面演示的技巧对于真实的对象/映射关系而言,是十分幼稚的。使用 **@DBTable** 的注解来获取表的的名称,这使得如果要修改表的名字,则迫使你重新编译 Java 代码。这种效果并不理想。现在已经有了很多可用的框架,用于将对象映射到数据库中,并且越来越多的框架开始使用注解了。 + ## 使用javac处理注解 +通过 **javac**,你可以通过创建编译时(compile-time)注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。但是这里有一个重大限制:你不能通过处理器来改变源代码。唯一影响输出的方式就是创建新的文件。 + +如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它编译所有的源文件。 + +每一个你编写的注解都需要处理器,但是 **javac** 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。 + +本节中的示例将帮助您开始学习,但如果您必须深入学习,请做好反复学习,大量访问 Google 和StackOverflow 的准备。 + +### 最简单的处理器 + +让我们开始定义我们能想到的最简单的处理器,只是为了编译和测试。如下是注解的定义: + +```java +// annotations/simplest/Simple.java +// A bare-bones annotation +package annotations.simplest; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE, ElementType.METHOD, + ElementType.CONSTRUCTOR, + ElementType.ANNOTATION_TYPE, + ElementType.PACKAGE, ElementType.FIELD, + ElementType.LOCAL_VARIABLE}) +public @interface Simple { + String value() default "-default-"; +} +``` + +**@Retention** 的参数现在为 **SOURCE**,这意味着注解不会再存留在编译后的代码。这在编译时处理注解是没有必要的,它只是指出,在这里,**javac** 是唯一有机会处理注解的代理。 + +**@Target** 声明了几乎所有的目标类型(除了 **PACKAGE**) ,同样是为了演示。下面是一个测试示例。 + +```java +// annotations/simplest/SimpleTest.java +// Test the "Simple" annotation +// {java annotations.simplest.SimpleTest} +package annotations.simplest; +@Simple +public class SimpleTest { + @Simple + int i; + @Simple + public SimpleTest() {} + @Simple + public void foo() { + System.out.println("SimpleTest.foo()"); + } + @Simple + public void bar(String s, int i, float f) { + System.out.println("SimpleTest.bar()"); + } + @Simple + public static void main(String[] args) { + @Simple + SimpleTest st = new SimpleTest(); + st.foo(); + } +} +``` + +输出为: + +```java +SimpleTest.foo() +``` + +在这里我们使用 **@Simple** 注解了所有 **@Target** 声明允许的地方。 + +**SimpleTest.java** 只需要 **Simple.java** 就可以编译成功。当我们编译的时候什么都没有发生。 + +**javac** 允许 **@Simple** 注解(只要它存在)在我们创建处理器并将其 hook 到编译器之前,不做任何事情。 + +如下是一个十分简单的处理器,其所作的事情就是把注解相关的信息打印出来: + +```java +// annotations/simplest/SimpleProcessor.java +// A bare-bones annotation processor +package annotations.simplest; +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import java.util.*; +@SupportedAnnotationTypes( + "annotations.simplest.Simple") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class SimpleProcessor + extends AbstractProcessor { + @Override + public boolean process( + Set annotations, + RoundEnvironment env) { + for(TypeElement t : annotations) + System.out.println(t); + for(Element el : + env.getElementsAnnotatedWith(Simple.class)) + display(el); + return false; + } + private void display(Element el) { + System.out.println("==== " + el + " ===="); + System.out.println(el.getKind() + + " : " + el.getModifiers() + + " : " + el.getSimpleName() + + " : " + el.asType()); + if(el.getKind().equals(ElementKind.CLASS)) { + TypeElement te = (TypeElement)el; + System.out.println(te.getQualifiedName()); + System.out.println(te.getSuperclass()); + System.out.println(te.getEnclosedElements()); + } + if(el.getKind().equals(ElementKind.METHOD)) { + ExecutableElement ex = (ExecutableElement)el; + System.out.print(ex.getReturnType() + " "); + System.out.print(ex.getSimpleName() + "("); + System.out.println(ex.getParameters() + ")"); + } + } +} +``` + +(旧的,失效的)**apt** 版本的处理器需要额外的方法来确定支持哪些注解以及支持的 Java 版本。不过,你现在可以简单的使用 **@SupportedAnnotationTypes** 和 **@SupportedSourceVersion** 注解(这是一个很好的示例关于注解如何简化你的代码)。 + +你唯一需要实现的方法就是 `process()`,这里是所有行为发生的地方。第一个参数告诉你哪个注解是存在的,第二个参数保留了剩余信息。我们所做的事情只是打印了注解(这里只存在一个),可以看 **TypeElement** 文档中的其他行为。通过使用 `process()` 的第二个操作,我们循环所有被 **@Simple** 注解的元素,并且针对每一个元素调用我们的 `display()` 方法。所有 **Element** 展示了本身的基本信息;例如,`getModifiers()` 告诉你它是否为 **public** 和 **static** 的。 + +**Element** 只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。所以(如果你阅读了正确的文档,但是我没有在任何文档中找到——我不得不通过 StackOverflow 寻找线索)你检查它是哪种 **ElementKind**,让后将其向下转换为更具体的元素类型,注入针对 CLASS 的 TypeElement 和 针对 METHOD 的ExecutableElement。此时,可以为这些元素调用其他方法。 + +动态向下转型(在编译期不进行检查)并不像是 Java 的做事方式,这非常不直观这也是为什么我从未想过要这样做事。相反,我花了好几天的时间,试图发现你应该如何访问这些信息,而这些信息至少在某种程度上是用不起作用的恰当方法简单明了的。我还没有遇到任何东西说上面是规范的形式,但在我看来是。 + +如果只是通过平常的方式来编译 **SimpleTest.java**,你不会得到任何结果。为了得到注解输出,你必须增加一个 **processor** 标志并且连接注解处理器类 + +```shell +javac -processor annotations.simplest.SimpleProcessor SimpleTest.java +``` + +现在编译器有了输出 + +```shell +annotations.simplest.Simple +==== annotations.simplest.SimpleTest ==== +CLASS : [public] : SimpleTest : annotations.simplest.SimpleTest +annotations.simplest.SimpleTest +java.lang.Object +i,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[]) +==== i ==== +FIELD : [] : i : int +==== SimpleTest() ==== +CONSTRUCTOR : [public] : : ()void +==== foo() ==== +METHOD : [public] : foo : ()void +void foo() +==== bar(java.lang.String,int,float) ==== +METHOD : [public] : bar : (java.lang.String,int,float)void +void bar(s,i,f) +==== main(java.lang.String[]) ==== +METHOD : [public, static] : main : (java.lang.String[])void +void main(args) +``` + +这给了你一些可以发现的东西,包括参数名和类型、返回值等。 + +### 更复杂的处理器 + +当你创建用于 javac 注解处理器时,你不能使用 Java 的反射特性,因为你处理的是源代码,而并非是编译后的 class 文件。各种 mirror[^3 ] 解决这个问题的方法是,通过允许你在未编译的源代码中查看方法、字段和类型。 + +如下是一个用于提取类中方法的注解,所以它可以被抽取成为一个接口: + +```java +// annotations/ifx/ExtractInterface.java +// javac-based annotation processing +package annotations.ifx; +import java.lang.annotation.*; +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.SOURCE) +public @interface ExtractInterface { + String interfaceName() default "-!!-"; +} +``` + +**RetentionPolicy** 的值为 **SOURCE**,这是为了在提取类中的接口之后不再将注解信息保留在 class 文件中。接下来的测试类提供了一些公用方法,这些方法可以成为接口的一部分: + +```java +// annotations/ifx/Multiplier.java +// javac-based annotation processing +// {java annotations.ifx.Multiplier} +package annotations.ifx; +@ExtractInterface(interfaceName="IMultiplier") +public class Multiplier { + public boolean flag = false; + private int n = 0; + public int multiply(int x, int y) { + int total = 0; + for(int i = 0; i < x; i++) + total = add(total, y); + return total; + } + public int fortySeven() { return 47; } + private int add(int x, int y) { + return x + y; + } + public double timesTen(double arg) { + return arg * 10; + } + public static void main(String[] args) { + Multiplier m = new Multiplier(); + System.out.println( + "11 * 16 = " + m.multiply(11, 16)); + } +} +``` + +输出为: + +```java +11 * 16 = 176 +``` + +**Multiplier** 类(只能处理正整数)拥有一个 `multiply()` 方法,这个方法会多次调用私有方法 `add()` 来模拟乘法操作。` add()` 是私有方法,因此不能成为接口的一部分。其他的方法提供了语法多样性。注解被赋予 **IMultiplier** 的 **InterfaceName** 作为要创建的接口的名称。 + +这里有一个编译时处理器用于提取有趣的方法,并创建一个新的 interface 源代码文件(这个源文件将会在下一轮中被自动编译): + +```java +// annotations/ifx/IfaceExtractorProcessor.java +// javac-based annotation processing +package annotations.ifx; +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.util.*; +import java.util.*; +import java.util.stream.*; +import java.io.*; +@SupportedAnnotationTypes( + "annotations.ifx.ExtractInterface") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class IfaceExtractorProcessor + extends AbstractProcessor { + private ArrayList + interfaceMethods = new ArrayList<>(); + Elements elementUtils; + private ProcessingEnvironment processingEnv; + @Override + public void init( + ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + elementUtils = processingEnv.getElementUtils(); + } + @Override + public boolean process( + Set annotations, + RoundEnvironment env) { + for(Element elem:env.getElementsAnnotatedWith( + ExtractInterface.class)) { + String interfaceName = elem.getAnnotation( + ExtractInterface.class).interfaceName(); + for(Element enclosed : + elem.getEnclosedElements()) { + if(enclosed.getKind() + .equals(ElementKind.METHOD) && + enclosed.getModifiers() + .contains(Modifier.PUBLIC) && + !enclosed.getModifiers() + .contains(Modifier.STATIC)) { + interfaceMethods.add(enclosed); + } + } + if(interfaceMethods.size() > 0) + writeInterfaceFile(interfaceName); + } + return false; + } + private void + writeInterfaceFile(String interfaceName) { + try( + Writer writer = processingEnv.getFiler() + .createSourceFile(interfaceName) + .openWriter() + ) { + String packageName = elementUtils + .getPackageOf(interfaceMethods + .get(0)).toString(); + writer.write( + "package " + packageName + ";\n"); + writer.write("public interface " + + interfaceName + " {\n"); + for(Element elem : interfaceMethods) { + ExecutableElement method = + (ExecutableElement)elem; + String signature = " public "; + signature += method.getReturnType() + " "; + signature += method.getSimpleName(); + signature += createArgList( + method.getParameters()); + System.out.println(signature); + writer.write(signature + ";\n"); + } + writer.write("}"); + } catch(Exception e) { + throw new RuntimeException(e); + } + } + private String createArgList( + List parameters) { + String args = parameters.stream() + .map(p -> p.asType() + " " + p.getSimpleName()) + .collect(Collectors.joining(", ")); + return "(" + args + ")"; + } +} +``` + +**Elements** 对象实例 **elementUtils** 是一组静态方法的工具;我们用它来寻找 **writeInterfaceFile()** 中含有的包名。 + +`getEnclosedElements()`方法会通过指定的元素生成所有的“闭包”元素。在这里,这个类闭包了它的所有元素。通过使用 `getKind()` 我们会找到所有的 **public** 和 **static** 方法,并将其添加到 **interfaceMethods** 列表中。接下来 `writeInterfaceFile()` 使用 **interfaceMethods** 列表里面的值生成新的接口定义。注意,在 `writeInterfaceFile()` 使用了向下转型到 **ExecutableElement**,这使得我们可以获取所有的方法信息。**createArgList()** 是一个帮助方法,用于生成参数列表。 + +**Filer**是 `getFiler()` 生成的,并且是 **PrintWriter** 的一种实例,可以用于创建新文件。我们使用 **Filer** 对象,而不是原生的 **PrintWriter** 原因是,这个对象可以运行 **javac** 追踪你创建的新文件,这使得它可以在新一轮中检查新文件中的注解并编译文件。 + +如下是一个命令行,可以在编译的时候使用处理器: + +```shell +javac -processor annotations.ifx.IfaceExtractorProcessor Multiplier.java +``` + +新生成的 **IMultiplier.java** 的文件,正如你通过查看上面处理器的 `println()` 语句所猜测的那样,如下所示: + +```java +package annotations.ifx; +public interface IMultiplier { + public int multiply(int x, int y); + public int fortySeven(); + public double timesTen(double arg); +} +``` + +这个类同样会被 **javac** 编译(在某一轮中),所以你会在同一个目录中看到 **IMultiplier.class** 文件。 + ## 基于注解的单元测试 +单元测试是对类中每个方法提供一个或者多个测试的一种时间,其目的是为了有规律的测试一个类中每个部分是否具备正确的行为。在 Java 中,最著名的单元测试工具就是 **JUnit**。**JUnit** 4 版本已经包含了注解。在注解版本之前的 JUnit 一个最主要的问题是,为了启动和运行 **JUnit** 测试,有大量的“仪式”需要标注。这种负担已经减轻了一些,**但是**注解使得测试更接近“可以工作的最简单的测试系统”。 + +在注解版本之前的 JUnit,你必须创建一个单独的文件来保存单元测试。通过注解,我们可以将单元测试集成在需要被测试的类中,从而将单元测试的时间和麻烦降到了最低。这种方式有额外的好处,就是使得测试私有方法和公有方法变的一样容易。 + +这个基于注解的测试框架叫做 **@Unit**。其最基本的测试形式,可能也是你使用的最多的一个注解是 **@Test**,我们使用 **@Test** 来标记测试方法。测试方法不带参数,并返回 **boolean** 结果来说明测试方法成功或者失败。你可以任意命名它的测试方法。同时 **@Unit** 测试方法可以是任意你喜欢的访问修饰方法,包括 **private**。 + +要使用 **@Unit**,你必须导入 **onjava.atunit** 包,并且使用 **@Unit** 的测试标记为合适的方法和字段打上标签(在接下来的例子中你会学到),然后让你的构建系统对编译后的类运行 **@Unit**,下面是一个简单的例子: + +```java +// annotations/AtUnitExample1.java +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/AtUnitExample1.class} +package annotations; +import onjava.atunit.*; +import onjava.*; +public class AtUnitExample1 { + public String methodOne() { + return "This is methodOne"; + } + public int methodTwo() { + System.out.println("This is methodTwo"); + return 2; + } + @Test + boolean methodOneTest() { + return methodOne().equals("This is methodOne"); + } + @Test + boolean m2() { return methodTwo() == 2; } + @Test + private boolean m3() { return true; } + // Shows output for failure: + @Test + boolean failureTest() { return false; } + @Test + boolean anotherDisappointment() { + return false; + } +} +``` + +输出为: + +```java +annotations.AtUnitExample1 +. m3 +. methodOneTest +. m2 This is methodTwo +. failureTest (failed) +. anotherDisappointment (failed) +(5 tests) +>>> 2 FAILURES <<< +annotations.AtUnitExample1: failureTest +annotations.AtUnitExample1: anotherDisappointment +``` + +使用 **@Unit** 进行测试的类必须定义在某个包中(即必须包括 **package** 声明)。 + +**@Test** 注解被置于 `methodOneTest()`、 `m2()`、`m3()`、`failureTest()` 以及 a`notherDisappointment()` 方法之前,它们告诉 **@Unit** 方法作为单元测试来运行。同时 **@Test** 确保这些方法没有任何参数并且返回值为 **boolean** 或者 **void**。当你填写单元测试时,唯一需要做的就是决定测试是成功还是失败,(对于返回值为 **boolean** 的方法)应该返回 **ture** 还是 **false**。 + +如果你熟悉 **JUnit**,你还将注意到 **@Unit** 输出的信息更多。你会看到现在正在运行的测试的输出更有用,最后它会告诉你导致失败的类和测试。 + +你并非必须将测试方法嵌入到原来的类中,有时候这种事情根本做不到。要生产一个非嵌入式的测试,最简单的方式就是继承: + +```java +// annotations/AUExternalTest.java +// Creating non-embedded tests +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/AUExternalTest.class} +package annotations; +import onjava.atunit.*; +import onjava.*; +public class AUExternalTest extends AtUnitExample1 { + @Test + boolean _MethodOne() { + return methodOne().equals("This is methodOne"); + } + @Test + boolean _MethodTwo() { + return methodTwo() == 2; + } +} +``` + +输出为: + +```java +annotations.AUExternalTest +. tMethodOne +. tMethodTwo This is methodTwo +OK (2 tests) +``` + +这个示例还表现出灵活命名的价值。在这里,**@Test** 方法被命名为下划线前缀加上要测试的方法名称(我并不认为这是一种理想的命名形式,这只是表现一种可能性罢了)。 + +你也可以使用组合来创建非嵌入式的测试: + +```java +// annotations/AUComposition.java +// Creating non-embedded tests +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/AUComposition.class} +package annotations; +import onjava.atunit.*; +import onjava.*; +public class AUComposition { + AtUnitExample1 testObject = new AtUnitExample1(); + @Test + boolean tMethodOne() { + return testObject.methodOne() + .equals("This is methodOne"); + } + @Test + boolean tMethodTwo() { + return testObject.methodTwo() == 2; + } +} +``` + +输出为: + +```java +annotations.AUComposition +. tMethodTwo This is methodTwo +. tMethodOne +OK (2 tests) +``` + +因为在每一个测试里面都会创建 **AUComposition** 对象,所以创建新的成员变量 **testObject** 用于以后的每一个测试方法。 + +因为 **@Unit** 中没有 **JUnit** 中特殊的 **assert** 方法,不过另一种形式的 **@Test** 方法仍然允许返回值为 **void**(如果你还想使用 **true** 或者 **false** 的话,也可以使用 **boolean** 作为方法返回值类型)。为了表示测试成功,可以使用 Java 的 **assert** 语句。Java 断言机制需要你在 java 命令行行加上 **-ea** 标志来开启,但是 **@Unit** 已经自动开启了该功能。要表示测试失败的话,你甚至可以使用异常。**@Unit** 的设计目标之一就是尽可能减少添加额外的语法,而 Java 的 **assert** 和异常对于报告错误而言,即已经足够了。一个失败的 **assert** 或者从方法从抛出的异常都被视为测试失败,但是 **@Unit** 不会在这个失败的测试上卡住,它会继续运行,直到所有测试完毕,下面是一个示例程序: + +```java +// annotations/AtUnitExample2.java +// Assertions and exceptions can be used in @Tests +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/AtUnitExample2.class} +package annotations; +import java.io.*; +import onjava.atunit.*; +import onjava.*; +public class AtUnitExample2 { + public String methodOne() { + return "This is methodOne"; + } + public int methodTwo() { + System.out.println("This is methodTwo"); + return 2; + } + @Test + void assertExample() { + assert methodOne().equals("This is methodOne"); + } + @Test + void assertFailureExample() { + assert 1 == 2: "What a surprise!"; + } + @Test + void exceptionExample() throws IOException { + try(FileInputStream fis = + new FileInputStream("nofile.txt")) {} // Throws + } + @Test + boolean assertAndReturn() { + // Assertion with message: + assert methodTwo() == 2: "methodTwo must equal 2"; + return methodOne().equals("This is methodOne"); + } +} +``` + +输出为: + +```java +annotations.AtUnitExample2 +. exceptionExample java.io.FileNotFoundException: +nofile.txt (The system cannot find the file specified) +(failed) +. assertExample +. assertAndReturn This is methodTwo +. assertFailureExample java.lang.AssertionError: What +a surprise! +(failed) +(4 tests) +>>> 2 FAILURES <<< +annotations.AtUnitExample2: exceptionExample +annotations.AtUnitExample2: assertFailureExample +``` + +如下是一个使用非嵌入式测试的例子,并且使用了断言,它将会对 **java.util.HashSet** 进行一些简单的测试: + +```java +// annotations/HashSetTest.java +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/HashSetTest.class} +package annotations; +import java.util.*; +import onjava.atunit.*; +import onjava.*; +public class HashSetTest { + HashSet testObject = new HashSet<>(); + @Test + void initialization() { + assert testObject.isEmpty(); + } + @Test + void _Contains() { + testObject.add("one"); + assert testObject.contains("one"); + } + @Test + void _Remove() { + testObject.add("one"); + testObject.remove("one"); + assert testObject.isEmpty(); + } +} +``` + +采用继承的方式可能会更简单,也没有一些其他的约束。 + +对每一个单元测试而言,**@Unit** 都会使用默认的无参构造器,为该测试类所属的类创建出一个新的实例。并在此新创建的对象上运行测试,然后丢弃该对象,以免对其他测试产生副作用。如此创建对象导致我们依赖于类的默认构造器。如果你的类没有默认构造器,或者对象需要复杂的构造过程,那么你可以创建一个 **static** 方法专门负责构造对象,然后使用 **@TestObjectCreate** 注解标记该方法,例子如下: + +```java +// annotations/AtUnitExample3.java +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/AtUnitExample3.class} +package annotations; +import onjava.atunit.*; +import onjava.*; +public class AtUnitExample3 { + private int n; + public AtUnitExample3(int n) { this.n = n; } + public int getN() { return n; } + public String methodOne() { + return "This is methodOne"; + } + public int methodTwo() { + System.out.println("This is methodTwo"); + return 2; + } + @TestObjectCreate + static AtUnitExample3 create() { + return new AtUnitExample3(47); + } + @Test + boolean initialization() { return n == 47; } + @Test + boolean methodOneTest() { + return methodOne().equals("This is methodOne"); + } + @Test + boolean m2() { return methodTwo() == 2; } +} +``` + +输出为: + +```java +annotations.AtUnitExample3 +. initialization +. m2 This is methodTwo +. methodOneTest +OK (3 tests) +``` + +**@TestObjectCreate** 修饰的方法必须声明为 **static** ,且必须返回一个你正在测试的类型对象,这一切都由 **@Unit** 负责确保成立。 + +有的时候,你需要向单元测试中增加一些字段。这时候可以使用 **@TestProperty** 注解,由它注解的字段表示只在单元测试中使用(因此,在你将产品发布给客户之前,他们应该被删除)。在下面的例子中,一个 **String** 通过 `String.split()` 方法进行分割,从其中读取一个值,这个值将会被生成测试对象: + +```java +// annotations/AtUnitExample4.java +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/AtUnitExample4.class} +// {VisuallyInspectOutput} +package annotations; +import java.util.*; +import onjava.atunit.*; +import onjava.*; +public class AtUnitExample4 { + static String theory = "All brontosauruses " + + "are thin at one end, much MUCH thicker in the " + + "middle, and then thin again at the far end."; + private String word; + private Random rand = new Random(); // Time-based seed + public AtUnitExample4(String word) { + this.word = word; + } + public String getWord() { return word; } + public String scrambleWord() { + List chars = Arrays.asList( + ConvertTo.boxed(word.toCharArray())); + Collections.shuffle(chars, rand); + StringBuilder result = new StringBuilder(); + for(char ch : chars) + result.append(ch); + return result.toString(); + } + @TestProperty + static List input = + Arrays.asList(theory.split(" ")); + @TestProperty + static Iterator words = input.iterator(); + @TestObjectCreate + static AtUnitExample4 create() { + if(words.hasNext()) + return new AtUnitExample4(words.next()); + else + return null; + } + @Test + boolean words() { + System.out.println("'" + getWord() + "'"); + return getWord().equals("are"); + } + @Test + boolean scramble1() { +// Use specific seed to get verifiable results: + rand = new Random(47); + System.out.println("'" + getWord() + "'"); + String scrambled = scrambleWord(); + System.out.println(scrambled); + return scrambled.equals("lAl"); + } + @Test + boolean scramble2() { + rand = new Random(74); + System.out.println("'" + getWord() + "'"); + String scrambled = scrambleWord(); + System.out.println(scrambled); + return scrambled.equals("tsaeborornussu"); + } +} +``` + +输出为: + +```java +annotations.AtUnitExample4 +. words 'All' +(failed) +. scramble1 'brontosauruses' +ntsaueorosurbs +(failed) +. scramble2 'are' +are +(failed) +(3 tests) +>>> 3 FAILURES <<< +annotations.AtUnitExample4: words +annotations.AtUnitExample4: scramble1 +annotations.AtUnitExample4: scramble2 +``` + +**@TestProperty** 也可以用来标记那些只在测试中使用的方法,但是它们本身不是测试方法。 + +如果你的测试对象需要执行某些初始化工作,并且使用完成之后还需要执行清理工作,那么可以选择使用 **static** 的 **@TestObjectCleanup** 方法,当测试对象使用结束之后,该方法会为你执行清理工作。在下面的示例中,**@TestObjectCleanup** 为每一个测试对象都打开了一个文件,因此必须在丢弃测试的时候关闭该文件: + +```java +// annotations/AtUnitExample5.java +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/AtUnitExample5.class} +package annotations; +import java.io.*; +import onjava.atunit.*; +import onjava.*; +public class AtUnitExample5 { + private String text; + public AtUnitExample5(String text) { + this.text = text; + } + @Override + public String toString() { return text; } + @TestProperty + static PrintWriter output; + @TestProperty + static int counter; + @TestObjectCreate + static AtUnitExample5 create() { + String id = Integer.toString(counter++); + try { + output = new PrintWriter("Test" + id + ".txt"); + } catch(IOException e) { + throw new RuntimeException(e); + } + return new AtUnitExample5(id); + } + @TestObjectCleanup + static void cleanup(AtUnitExample5 tobj) { + System.out.println("Running cleanup"); + output.close(); + } + @Test + boolean test1() { + output.print("test1"); + return true; + } + @Test + boolean test2() { + output.print("test2"); + return true; + } + @Test + boolean test3() { + output.print("test3"); + return true; + } +} +``` + +输出为: + +```java +annotations.AtUnitExample5 +. test1 +Running cleanup +. test3 +Running cleanup +. test2 +Running cleanup +OK (3 tests) +``` + +在输出中我们可以看到,清理方法会在每个测试方法结束之后自动运行。 + +### 在 @Unit 中使用泛型 + +泛型为 **@Unit** 出了一个难题,因为我们不可能“通用测试”。我们必须针对某个特定类型的参数或者参数集才能进行测试。解决方法十分简单,让测试类继承自泛型类的一个特定版本即可: + +下面是一个 **stack** 的简单实现: + +```java +// annotations/StackL.java +// A stack built on a LinkedList +package annotations; +import java.util.*; +public class StackL { + private LinkedList list = new LinkedList<>(); + public void push(T v) { list.addFirst(v); } + public T top() { return list.getFirst(); } + public T pop() { return list.removeFirst(); } +} +``` + +为了测试 String 版本,我们直接让测试类继承一个 Stack\ : + +```java +// annotations/StackLStringTst.java +// Applying @Unit to generics +// {java onjava.atunit.AtUnit +// build/classes/main/annotations/StackLStringTst.class} +package annotations; +import onjava.atunit.*; +import onjava.*; +public class +StackLStringTst extends StackL { + @Test + void tPush() { + push("one"); + assert top().equals("one"); + push("two"); + assert top().equals("two"); + } + @Test + void tPop() { + push("one"); + push("two"); + assert pop().equals("two"); + assert pop().equals("one"); + } + @Test + void tTop() { + push("A"); + push("B"); + assert top().equals("B"); + assert top().equals("B"); + } +} +``` + +输出为: + +```java +annotations.StackLStringTst +. tTop +. tPush +. tPop +OK (3 tests) +``` + +这种方法存在的唯一缺点是,继承使我们失去了访问被测试的类中 **private** 方法的能力。这对你非常重要,那你要么把 private 方法变为 **protected**,要么添加一个非 **private** 的 **@TestProperty** 方法,由它来调用 **private** 方法(稍后我们会看到,**AtUnitRemover** 会删除产品中的 **@TestProperty** 方法)。 + +**@Unit** 搜索那些包含合适注解的类文件,然后运行 **@Test** 方法。我的主要目标就是让 **@Unit** 测试系统尽可能的透明,使得人们使用它的时候只需要添加 **@Test** 注解,而不需要特殊的编码和知识(现在版本的 **JUnit** 符合这个实践)。不过,如果说编写测试不会遇到任何困难,也不太可能,因此 **@Unit** 会尽量让这些困难变的微不足道,希望通过这种方式,你们会更乐意编写测试。 + +### 实现 @Unit + +首先我们需要定义所有的注解类型。这些都是简单的标签,并且没有任何字段。@Test 标签在本章开头已经定义过了,这里是其他所需要的注解: + +```java +// onjava/atunit/TestObjectCreate.java +// The @Unit @TestObjectCreate tag +package onjava.atunit; +import java.lang.annotation.*; +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestObjectCreate {} +``` + +```java +// onjava/atunit/TestObjectCleanup.java +// The @Unit @TestObjectCleanup tag +package onjava.atunit; +import java.lang.annotation.*; +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestObjectCleanup {} +``` + +```java +// onjava/atunit/TestProperty.java +// The @Unit @TestProperty tag +package onjava.atunit; +import java.lang.annotation.*; +// Both fields and methods can be tagged as properties: +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestProperty {} +``` + +所有测试的保留属性都为 **RUNTIME**,这是因为 **@Unit** 必须在编译后的代码中发现这些注解。 + +要实现系统并运行测试,我们还需要反射机制来提取注解。下面这个程序通过注解中的信息,决定如何构造测试对象,并在测试对象上运行测试。正是由于注解帮助,这个程序才会如此短小而直接: + +```java +// onjava/atunit/AtUnit.java +// An annotation-based unit-test framework +// {java onjava.atunit.AtUnit} +package onjava.atunit; +import java.lang.reflect.*; +import java.io.*; +import java.util.*; +import java.nio.file.*; +import java.util.stream.*; +import onjava.*; +public class AtUnit implements ProcessFiles.Strategy { + static Class testClass; + static List failedTests= new ArrayList<>(); + static long testsRun = 0; + static long failures = 0; + public static void + main(String[] args) throws Exception { + ClassLoader.getSystemClassLoader() + .setDefaultAssertionStatus(true); // Enable assert + new ProcessFiles(new AtUnit(), "class").start(args); + if(failures == 0) + System.out.println("OK (" + testsRun + " tests)"); + else { + System.out.println("(" + testsRun + " tests)"); + System.out.println( + "\n>>> " + failures + " FAILURE" + + (failures > 1 ? "S" : "") + " <<<"); + for(String failed : failedTests) + System.out.println(" " + failed); + } + } + @Override + public void process(File cFile) { + try { + String cName = ClassNameFinder.thisClass( + Files.readAllBytes(cFile.toPath())); + if(!cName.startsWith("public:")) + return; + cName = cName.split(":")[1]; + if(!cName.contains(".")) + return; // Ignore unpackaged classes + testClass = Class.forName(cName); + } catch(IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + TestMethods testMethods = new TestMethods(); + Method creator = null; + Method cleanup = null; + for(Method m : testClass.getDeclaredMethods()) { + testMethods.addIfTestMethod(m); + if(creator == null) + creator = checkForCreatorMethod(m); + if(cleanup == null) + cleanup = checkForCleanupMethod(m); + } + if(testMethods.size() > 0) { + if(creator == null) + try { + if(!Modifier.isPublic(testClass + .getDeclaredConstructor() + .getModifiers())) { + System.out.println("Error: " + testClass + + " no-arg constructor must be public"); + System.exit(1); + } + } catch(NoSuchMethodException e) { +// Synthesized no-arg constructor; OK + } + System.out.println(testClass.getName()); + } + for(Method m : testMethods) { + System.out.print(" . " + m.getName() + " "); + try { + Object testObject = createTestObject(creator); + boolean success = false; + try { + if(m.getReturnType().equals(boolean.class)) + success = (Boolean)m.invoke(testObject); + else { + m.invoke(testObject); + success = true; // If no assert fails + } + } catch(InvocationTargetException e) { +// Actual exception is inside e: + System.out.println(e.getCause()); + } + System.out.println(success ? "" : "(failed)"); + testsRun++; + if(!success) { + failures++; + failedTests.add(testClass.getName() + + ": " + m.getName()); + } + if(cleanup != null) + cleanup.invoke(testObject, testObject); + } catch(IllegalAccessException | + IllegalArgumentException | + InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + public static + class TestMethods extends ArrayList { + void addIfTestMethod(Method m) { + if(m.getAnnotation(Test.class) == null) + return; + if(!(m.getReturnType().equals(boolean.class) || + m.getReturnType().equals(void.class))) + throw new RuntimeException("@Test method" + + " must return boolean or void"); + m.setAccessible(true); // If it's private, etc. + add(m); + } + } + private static + Method checkForCreatorMethod(Method m) { + if(m.getAnnotation(TestObjectCreate.class) == null) + return null; + if(!m.getReturnType().equals(testClass)) + throw new RuntimeException("@TestObjectCreate " + + "must return instance of Class to be tested"); + if((m.getModifiers() & + java.lang.reflect.Modifier.STATIC) < 1) + throw new RuntimeException("@TestObjectCreate " + + "must be static."); + m.setAccessible(true); + return m; + } + private static + Method checkForCleanupMethod(Method m) { + if(m.getAnnotation(TestObjectCleanup.class) == null) + return null; + if(!m.getReturnType().equals(void.class)) + throw new RuntimeException("@TestObjectCleanup " + + "must return void"); + if((m.getModifiers() & + java.lang.reflect.Modifier.STATIC) < 1) + throw new RuntimeException("@TestObjectCleanup " + + "must be static."); + if(m.getParameterTypes().length == 0 || + m.getParameterTypes()[0] != testClass) + throw new RuntimeException("@TestObjectCleanup " + + "must take an argument of the tested type."); + m.setAccessible(true); + return m; + } + private static Object + createTestObject(Method creator) { + if(creator != null) { + try { + return creator.invoke(testClass); + } catch(IllegalAccessException | + IllegalArgumentException | + InvocationTargetException e) { + throw new RuntimeException("Couldn't run " + + "@TestObject (creator) method."); + } + } else { // Use the no-arg constructor: + try { + return testClass.newInstance(); + } catch(InstantiationException | + IllegalAccessException e) { + throw new RuntimeException( + "Couldn't create a test object. " + + "Try using a @TestObject method."); + } + } + } +} +``` + +虽然它可能是“过早的重构”(因为它只在书中使用过一次),**AtUnit.java** 使用了 **ProcessFiles** 工具逐步判断命令行中的参数,决定它是一个目录还是文件,并采取相应的行为。这可以应用于不同的解决方法,是因为它包含了一个 可用于自定义的 **Strategy** 接口: + +```java +// onjava/ProcessFiles.java +package onjava; +import java.io.*; +import java.nio.file.*; +public class ProcessFiles { + public interface Strategy { + void process(File file); + } + private Strategy strategy; + private String ext; + public ProcessFiles(Strategy strategy, String ext) { + this.strategy = strategy; + this.ext = ext; + } + public void start(String[] args) { + try { + if(args.length == 0) + processDirectoryTree(new File(".")); + else + for(String arg : args) { + File fileArg = new File(arg); + if(fileArg.isDirectory()) + processDirectoryTree(fileArg); + else { +// Allow user to leave off extension: + if(!arg.endsWith("." + ext)) + arg += "." + ext; + strategy.process( + new File(arg).getCanonicalFile()); + } + } + } catch(IOException e) { + throw new RuntimeException(e); + } + } + public void processDirectoryTree(File root) throws IOException { + PathMatcher matcher = FileSystems.getDefault() + .getPathMatcher("glob:**/*.{" + ext + "}"); + Files.walk(root.toPath()) + .filter(matcher::matches) + .forEach(p -> strategy.process(p.toFile())); + } +} +``` + +**AtUnit** 类实现了 **ProcessFiles.Strategy**,其包含了一个 `process()` 方法。在这种方式下,**AtUnit** 实例可以作为参数传递给 **ProcessFiles** 构造器。第二个构造器的参数告诉 **ProcessFiles** 如寻找所有包含 “class” 拓展名的文件。 + +如下是一个简单的使用示例: + +```java +// annotations/DemoProcessFiles.java +import onjava.ProcessFiles; +public class DemoProcessFiles { + public static void main(String[] args) { + new ProcessFiles(file -> System.out.println(file), + "java").start(args); + } +} +``` + +输出为: + +```java +.\AtUnitExample1.java +.\AtUnitExample2.java +.\AtUnitExample3.java +.\AtUnitExample4.java +.\AtUnitExample5.java +.\AUComposition.java +.\AUExternalTest.java +.\database\Constraints.java +.\database\DBTable.java +.\database\Member.java +.\database\SQLInteger.java +.\database\SQLString.java +.\database\TableCreator.java +.\database\Uniqueness.java +.\DemoProcessFiles.java +.\HashSetTest.java +.\ifx\ExtractInterface.java +.\ifx\IfaceExtractorProcessor.java +.\ifx\Multiplier.java +.\PasswordUtils.java +.\simplest\Simple.java +.\simplest\SimpleProcessor.java +.\simplest\SimpleTest.java +.\SimulatingNull.java +.\StackL.java +.\StackLStringTst.java +.\Testable.java +.\UseCase.java +.\UseCaseTracker.java +``` + +如果没有命令行参数,这个程序会遍历当前的目录树。你还可以提供多个参数,这些参数可以是类文件(带或不带.class扩展名)或目录。 + +回到我们对 **AtUnit.java** 的讨论,因为 **@Unit** 会自动找到可测试的类和方法,所以不需要“套件”机制。 + +**AtUnit.java** 中存在的一个我们必须要解决的问题是,当它发现类文件时,类文件名中的限定类名(包括包)不明显。为了发现这个信息,必须解析类文件 - 这不是微不足道的,但也不是不可能的。 找到 .class 文件时,会打开它并读取其二进制数据并将其传递给 `ClassNameFinder.thisClass()`。 在这里,我们正在进入“字节码工程”领域,因为我们实际上正在分析类文件的内容: + +```java +// onjava/atunit/ClassNameFinder.java +// {java onjava.atunit.ClassNameFinder} +package onjava.atunit; +import java.io.*; +import java.nio.file.*; +import java.util.*; +import onjava.*; +public class ClassNameFinder { + public static String thisClass(byte[] classBytes) { + Map offsetTable = new HashMap<>(); + Map classNameTable = new HashMap<>(); + try { + DataInputStream data = new DataInputStream( + new ByteArrayInputStream(classBytes)); + int magic = data.readInt(); // 0xcafebabe + int minorVersion = data.readShort(); + int majorVersion = data.readShort(); + int constantPoolCount = data.readShort(); + int[] constantPool = new int[constantPoolCount]; + for(int i = 1; i < constantPoolCount; i++) { + int tag = data.read(); + // int tableSize; + switch(tag) { + case 1: // UTF + int length = data.readShort(); + char[] bytes = new char[length]; + for(int k = 0; k < bytes.length; k++) + bytes[k] = (char)data.read(); + String className = new String(bytes); + classNameTable.put(i, className); + break; + case 5: // LONG + case 6: // DOUBLE + data.readLong(); // discard 8 bytes + i++; // Special skip necessary + break; + case 7: // CLASS + int offset = data.readShort(); + offsetTable.put(i, offset); + break; + case 8: // STRING + data.readShort(); // discard 2 bytes + break; + case 3: // INTEGER + case 4: // FLOAT + case 9: // FIELD_REF + case 10: // METHOD_REF + case 11: // INTERFACE_METHOD_REF + case 12: // NAME_AND_TYPE + case 18: // Invoke Dynamic + data.readInt(); // discard 4 bytes + break; + case 15: // Method Handle + data.readByte(); + data.readShort(); + break; + case 16: // Method Type + data.readShort(); + break; + default: + throw + new RuntimeException("Bad tag " + tag); + } + } + short accessFlags = data.readShort(); + String access = (accessFlags & 0x0001) == 0 ? + "nonpublic:" : "public:"; + int thisClass = data.readShort(); + int superClass = data.readShort(); + return access + classNameTable.get( + offsetTable.get(thisClass)).replace('/', '.'); + } catch(IOException | RuntimeException e) { + throw new RuntimeException(e); + } + } + // Demonstration: + public static void main(String[] args) throws Exception { + PathMatcher matcher = FileSystems.getDefault() + .getPathMatcher("glob:**/*.class"); +// Walk the entire tree: + Files.walk(Paths.get(".")) + .filter(matcher::matches) + .map(p -> { + try { + return thisClass(Files.readAllBytes(p)); + } catch(Exception e) { + throw new RuntimeException(e); + } + }) + .filter(s -> s.startsWith("public:")) +// .filter(s -> s.indexOf('$') >= 0) + .map(s -> s.split(":")[1]) + .filter(s -> !s.startsWith("enums.")) + .filter(s -> s.contains(".")) + .forEach(System.out::println); + } +} +``` + +输出为: + +```java +onjava.ArrayShow +onjava.atunit.AtUnit$TestMethods +onjava.atunit.AtUnit +onjava.atunit.ClassNameFinder +onjava.atunit.Test +onjava.atunit.TestObjectCleanup +onjava.atunit.TestObjectCreate +onjava.atunit.TestProperty +onjava.BasicSupplier +onjava.CollectionMethodDifferences +onjava.ConvertTo +onjava.Count$Boolean +onjava.Count$Byte +onjava.Count$Character +onjava.Count$Double +onjava.Count$Float +onjava.Count$Integer +onjava.Count$Long +onjava.Count$Pboolean +onjava.Count$Pbyte +onjava.Count$Pchar +onjava.Count$Pdouble +onjava.Count$Pfloat +onjava.Count$Pint +onjava.Count$Plong +onjava.Count$Pshort +onjava.Count$Short +onjava.Count +onjava.CountingIntegerList +onjava.CountMap +onjava.Countries +onjava.Enums +onjava.FillMap +onjava.HTMLColors +onjava.MouseClick +onjava.Nap +onjava.Null +onjava.Operations +onjava.OSExecute +onjava.OSExecuteException +onjava.Pair +onjava.ProcessFiles$Strategy +onjava.ProcessFiles +onjava.Rand$Boolean +onjava.Rand$Byte +onjava.Rand$Character +onjava.Rand$Double +onjava.Rand$Float +onjava.Rand$Integer +onjava.Rand$Long +onjava.Rand$Pboolean +onjava.Rand$Pbyte +onjava.Rand$Pchar +onjava.Rand$Pdouble +onjava.Rand$Pfloat +onjava.Rand$Pint +onjava.Rand$Plong +onjava.Rand$Pshort +onjava.Rand$Short +onjava.Rand$String +onjava.Rand +onjava.Range +onjava.Repeat +onjava.RmDir +onjava.Sets +onjava.Stack +onjava.Suppliers +onjava.TimedAbort +onjava.Timer +onjava.Tuple +onjava.Tuple2 +onjava.Tuple3 +onjava.Tuple4 +onjava.Tuple5 +onjava.TypeCounter +``` + + 虽然无法在这里介绍其中所有的细节,但是每个类文件都必须遵循一定的格式,而我已经尽力用有意义的字段来表示这些从 **ByteArrayInputStream** 中提取出来的数据片段。通过施加在输入流上的读操作,你能看出每个信息片的大小。例如每一个类的头 32 个 bit 总是一个 “神秘数字” **0xcafebabe**,而接下来的两个 **short** 值是版本信息。常量池包含了程序的常量,所以这是一个可变的值。接下来的 **short** 告诉我们这个常量池有多大,然后我们为其创建一个尺寸合适的数组。常量池中的每一个元素,其长度可能是固定式,也可能是可变的值,因此我们必须检查每一个常量的起始标记,然后才能知道该怎么做,这就是 switch 语句的工作。我们并不打算精确的分析类中所有的数据,仅仅是从文件的起始一步一步的走,直到取得我们所需的信息,因此你会发现,在这个过程中我们丢弃了大量的数据。关于类的信息都保存在 **classNameTable** 和 **offsetTable** 中。在读取常量池之后,就找到了 **this_class** 信息,这是 **offsetTable** 的一个坐标,通过它可以找到进入 **classNameTable** 的坐标,然后就可以得到我们所需的类的名字了。 + +现在让我们回到 **AtUtil.java** 中,process() 方法中拥有了类的名字,然后检查它是否包含“.”,如果有就表示该类定义于一个包中。没有包的类会被忽略。如果一个类在包中,那么我们就可以使用标准的类加载器通过 `Class.forName()` 将其加载进来。现在我们可以对这个类进行 **@Unit** 注解的分析工作了。 + +我们只需要关注三件事:首先是 **@Test** 方法,它们被保存在 **TestMehtods** 列表中,然后检查其是否具有 @TestObjectCreate 和 **@TestObjectCleanup****** 方法。从代码中可以看到,我们通过调用相应的方法来查询注解从而找到这些方法。 + +每找到一个 @Test 方法,就打印出来当前类的名字,于是观察者立刻就可以知道发生了什么。接下来开始执行测试,也就是打印出方法名,然后调用 createTestObject() (如果存在一个加了 @TestObjectCreate 注解的方法),或者调用默认构造器。一旦创建出来测试对象,如果调用其上的测试方法。如果测试的返回值为 boolean,就捕获该结果。如果测试方法没有返回值,那么就没有异常发生,我们就假设测试成功,反之,如果当 assert 失败或者有任何异常抛出的时候,就说明测试失败,这时将异常信息打印出来以显示错误的原因。如果有失败的测试发生,那么还要统计失败的次数,并将失败所属的类和方法加入到 failedTests 中,以便最后报告给用户。 + ## 本章小结 +注解是 Java 引入的一项非常受欢迎的补充,它提供了一种结构化,并且具有类型检查能力的新途径,从而使得你能够为代码中加入元数据,而且不会导致代码杂乱并难以阅读。使用注解能够帮助我们避免编写累赘的部署描述性文件,以及其他的生成文件。而 Javadoc 中的 @deprecated 被 @Deprecated 注解所替代的事实也说明,与注释性文字相比,注解绝对更适用于描述类相关的信息。 +Java 提供了很少的内置注解。这意味着如果你在别处找不到可用的类库,那么就只能自己创建新的注解以及相应的处理器。通过将注解处理器链接到 javac,你可以一步完成编译新生成的文件,简化了构造过程。 + +API 的提供方和框架将会将注解作为他们工具的一部分。通过 @Unit 系统,我们可以想象,注解会极大的改变我们的 Java 编程体验。 -
\ No newline at end of file +
+ +[^3 ]: The Java designers coyly suggest that a mirror is where you find a reflection. \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 930cc41..673a7d6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -367,8 +367,6 @@ } - - @@ -380,5 +378,7 @@ + +