Merge pull request #1 from LingCoder/master

获取更新
This commit is contained in:
1326670425
2019-04-28 10:51:13 +08:00
committed by GitHub
8 changed files with 2391 additions and 187 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
<span id="busuanzi_container_site_pv">
本页总访问次数:<span id="busuanzi_value_site_pv"></span>
👁️本页总访问次数:<span id="busuanzi_value_site_pv"></span>
</span>
<span id="busuanzi_container_site_uv" >
| 🧑总访客数: <span id="busuanzi_value_site_uv"></span>
</span>
[GitHub](https://github.com/lingcoder/onJava8/)

View File

@@ -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)

View File

@@ -27,7 +27,7 @@ OOobject oriented面向对象是抽象数据FPfunctional 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 {
| **特征** |**函数式方法名**|**示例**|
| :---- | :----: | :----: |
|无参; <br> 无返回值|**Runnable** <br> (java.lang) <br> `run()`|**Runnable**|
|无参; <br> 返回类型任意|**Supplier** <br> `get()` <br> `getAs类型()`| **Supplier<T> <br> BooleanSupplier <br> IntSupplier <br> LongSupplier <br> DoubleSupplier**|
|无参数; <br> 返回类型任意|**Callable** <br> (java.util.concurrent) <br> `call()`|**Callable\<V>**|
|1 参数; <br> 无返回值|**Consumer** <br> `accept()`|**Consumer<T> <br> IntConsumer <br> LongConsumer <br> DoubleConsumer**|
|2 参数 **Consumer**|**BiConsumer** <br> `accept()`|**BiConsumer\<T,U>**|
|2 参数 **Consumer** <br> 1 引用; <br> 1 基本类型|**Obj类型Consumer** <br> `accept()`|**ObjIntConsumer<T> <br> ObjLongConsumer\<T> <br> ObjDoubleConsumer\<T>**|
|1 参数; <br> 返回类型不同|**Function** <br> `apply()` <br> **To类型****类型To类型** <br> `applyAs类型()`|**Function\<T,R> <br> IntFunction\<R> <br> LongFunction\<R> <br> DoubleFunction<R> <br> ToIntFunction<T> <br> ToLongFunction\<T> <br> ToDoubleFunction\<T> <br> IntToLongFunction <br> IntToDoubleFunction <br> LongToIntFunction <br> LongToDoubleFunction <br> DoubleToIntFunction <br> DoubleToLongFunction**|
|1 参数; <br> 返回类型相同|**UnaryOperator** <br> `apply()`|**UnaryOperator<T> <br> IntUnaryOperator <br> LongUnaryOperator <br> DoubleUnaryOperator**|
|2 参数类型相同; <br> 返回类型相同|**BinaryOperator** <br> `apply()`|**BinaryOperator\<T> <br> IntBinaryOperator <br> LongBinaryOperator <br> DoubleBinaryOperator**|
|2 参数类型相同; <br> 返回整型|Comparator <br> (java.util) <br> `compare()`|**Comparator\<T>**|
|2 参数; <br> 返回布尔型|**Predicate** <br> `test()`|**Predicate\<T> <br> BiPredicate\<T,U> <br> IntPredicate <br> LongPredicate <br> DoublePredicate**|
|无参 <br> 无返回值|**Runnable** <br> (java.lang) <br> `run()`|**Runnable**|
|无参 <br> 返回类型任意|**Supplier** <br> `get()` <br> `getAs类型()`| **Supplier\<T\> <br> BooleanSupplier <br> IntSupplier <br> LongSupplier <br> DoubleSupplier**|
|无参数; <br> 返回类型任意|**Callable** <br> (java.util.concurrent) <br> `call()`|**Callable\<V\>**|
|1 参数; <br> 无返回值|**Consumer** <br> `accept()`|**Consumer\<T\> <br> IntConsumer <br> LongConsumer <br> DoubleConsumer**|
|2 参数 **Consumer**|**BiConsumer** <br> `accept()`|**BiConsumer\<T,U\>**|
|2 参数 **Consumer** <br> 1 引用; <br> 1 基本类型|**Obj类型Consumer** <br> `accept()`|**ObjIntConsumer\<T\> <br> ObjLongConsumer\<T\> <br> ObjDoubleConsumer\<T\>**|
|1 参数; <br> 返回类型不同|**Function** <br> `apply()` <br> **To类型****类型To类型** <br> `applyAs类型()`|**Function\<T,R\> <br> IntFunction\<R\> <br> LongFunction\<R\> <br> DoubleFunction\<R\> <br> ToIntFunction\<T\> <br> ToLongFunction\<T\> <br> ToDoubleFunction\<T\> <br> IntToLongFunction <br> IntToDoubleFunction <br> LongToIntFunction <br> LongToDoubleFunction <br> DoubleToIntFunction <br> DoubleToLongFunction**|
|1 参数; <br> 返回类型相同|**UnaryOperator** <br> `apply()`|**UnaryOperator\<T\> <br> IntUnaryOperator <br> LongUnaryOperator <br> DoubleUnaryOperator**|
|2 参数类型相同; <br> 返回类型相同|**BinaryOperator** <br> `apply()`|**BinaryOperator\<T\> <br> IntBinaryOperator <br> LongBinaryOperator <br> DoubleBinaryOperator**|
|2 参数类型相同; <br> 返回整型|Comparator <br> (java.util) <br> `compare()`|**Comparator\<T\>**|
|2 参数; <br> 返回布尔型|**Predicate** <br> `test()`|**Predicate\<T\> <br> BiPredicate\<T,U\> <br> IntPredicate <br> LongPredicate <br> DoublePredicate**|
|参数基本类型; <br> 返回基本类型|**类型To类型Function** <br> `applyAs类型()`|**IntToLongFunction <br> IntToDoubleFunction <br> LongToIntFunction <br> LongToDoubleFunction <br> DoubleToIntFunction <br> DoubleToLongFunction**|
|2 参数类型不同|**Bi操作** <br> (不同方法名)|**BiFunction\<T,U,R> <br> BiConsumer\<T,U> <br> BiPredicate<T,U> <br> ToIntBiFunction\<T,U> <br> ToLongBiFunction\<T,U> <br> ToDoubleBiFunction<T**|
|2 参数类型不同|**Bi操作** <br> (不同方法名)|**BiFunction\<T,U,R\> <br> BiConsumer\<T,U\> <br> BiPredicate\<T,U\> <br> ToIntBiFunction\<T,U\> <br> ToLongBiFunction\<T,U\> <br> ToDoubleBiFunction\<T\>**|
此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出更多行的函数式接口。
  此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出更多行的函数式接口。
能看出来在创建 `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()` 参数的动作结合起来。
<!-- Closures -->
## 闭包
在上一节的 `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 时存在。
<!-- Inner Classes as Closures -->
### 作为闭包的内部类
我们可以复制我们的例子使用匿名内部类:
@@ -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 效果**
<!-- Function Composition -->
## 函数组合
函数组合基本上意味着“将函数粘贴在一起以创建新函数”它通常被认为是函数编程的一部分。你在`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)` <br> 根据参数执行原始操作 | **Function <br> BiFunction <br> Consumer <br> BiConsumer <br> IntConsumer <br> LongConsumer <br> DoubleConsumer <br> UnaryOperator <br> IntUnaryOperator <br> LongUnaryOperator <br> DoubleUnaryOperator <br> BinaryOperator** |
| `compose(argument)` <br> 根据参数执行原始操作 | **Function <br> UnaryOperator <br> IntUnaryOperator <br> LongUnaryOperator <br> DoubleUnaryOperator** |
| `and(argument)` <br> 短路**逻辑与**原始断言和参数断言 | **Predicate <br> BiPredicate <br> IntPredicate <br> LongPredicate <br> DoublePredicate** |
| `or(argument)` <br> 短路**逻辑或**原始断言和参数断言 | **Predicate <br> BiPredicate <br> IntPredicate <br> LongPredicate <br> DoublePredicate** |
| `negate()` <br> 该断言的**逻辑否**断言| **Predicate <br> BiPredicate <br> IntPredicate <br> LongPredicate <br> 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` 剥离了前三个字符。 这是因为对 `composef2`的调用意味着在 `f1` 之前调用 `f2`
```
AFTER ALL AMBULANCES
_fter _ll _mbul_nces
```
是Predicate逻辑运算的演示
里我们重点看正在创建的新函数 `f4`。它调用 `apply()` 的方式与常规几乎无异[^8]。
`f1` 获得字符串时,它已经被`f2` 剥离了前三个字符。这是因为对 `composef2`的调用意味着在 `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 and Partial Evaluation -->
## 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<String, Function<String, String>> 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<String, String> 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 以外的语言,理解它们的基本概念,就应该很容易翻译。
<!-- Pure Functional Programming -->
## 纯函数式编程
没有函数支持的情况下即使用像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` 之类的语言。
<!-- Summary -->
## 本章小结
Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。它们对 Java 是一个巨大的改进因为它们允许你编写更简洁、更干净、更容易理解的代码。在下一章中,你看到它们如何启用流。如果你像我一样,你会喜欢流媒体
Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。对 Java 来说是一个巨大的改进因为允许你编写更简洁明了,易于理解的代码。在下一章中,你看到它们在流式编程中的应用。相信你会像我一样,喜欢上流式编程
这些特性可能会满足大部分 Java 程序员的需求他们已经对 ClojureScala 等新的、功能更强的语言感到不安和嫉妒,并阻止 Java 程序员流向这些语言 (或者,如果他们仍然决定迁移,至少会为他们做好更好的准备)
这些特性满足大部分 Java 程序员的需求他们开始羡慕嫉妒 ClojureScala 这类新语言的功能,并试图阻止 Java 程序员流失到其他阵营 (就算不能阻止,起码提供了更好的选择)
但是Lambdas 和方法引用远非完美,我们永远要为 Java 设计人员在早期令人兴奋的语言中做出的草率决定付出代价。特别是没有泛型 Lambda所以 Lambda 实际上不是 Java 中的第一类公民。这并不意味着 Java 8不是一个很大的改进但它确实意味着就像许多 Java 特性一样,你最终会感到沮丧
但是Lambdas 和方法引用远非完美,我们永远要为 Java 设计者早期的草率决定付出代价。特别是没有泛型 Lambda所以 Lambda Java 中并非一等公民。虽然我不否认 Java 8 的巨大改进,但这意味着和许多 Java 特性一样,它的使用还是会让人感觉沮丧和鸡肋
当你遇到学习困难时,请记住,你可以从 ide (如 NetBeans、IntelliJ Idea 和 Eclipse )获得帮助,这些 ide 将建议你何时可以使用 Lambda 表达式或方法引用 (并且经常为你重写代码!)
当你遇到学习困难时,请记住通过 IDENetBeans、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/)。
<!-- 分页 -->
<div style="page-break-after: always;"></div>

View File

@@ -3,7 +3,7 @@
<!-- Type Information -->
# 第十九章 类型信息
> RTTIRunTime Type Information, 运行时类型信息)能够在程序运行时发现和使用类型信息
> RTTIRunTime 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<Shape>`中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对`steam`而言,它们只是`Shape`对象。
这个例子中,在把 `Shape` 对象放入 `Stream<Shape>` 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 `steam` 而言,它们只是 `Shape` 对象。
严格来说,`Stream<Shape>`实际上是把放入其中的所有对象都当做`Object`对象来持有,只是取元素时会自动将其类型转为`Shape`。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。
严格来说,`Stream<Shape>` 实际上是把放入其中的所有对象都当做 `Object` 对象来持有,只是取元素时会自动将其类型转为 `Shape`。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。
另外在这个例子中,类型转换并不彻底:`Object`被转型为`Shape`,而不是`Circle``Square`或者`Triangle`。这是因为目前我们只能确保这个 `Stream<Shape>`保存的都是`Shape`
另外在这个例子中,类型转换并不彻底:`Object` 被转型为 `Shape` ,而不是 `Circle``Square` 或者 `Triangle`。这是因为目前我们只能确保这个 `Stream<Shape>` 保存的都是 `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<Shape>`里边的形状具体是什么类型而Java 实际上也满足了我们的这种需求。使用 RTTI我们可以查询某个`Shape`引用所指向对象的确切类型,然后选择或者剔除特例。
但是,有时你会碰到一些编程问题,在这些问题中如果你能知道某个泛化引用的具体类型,就可以把问题轻松解决。例如,假设我们允许用户将某些几何形状高亮显示,现在希望找到屏幕上所有高亮显示的三角形;或者,我们现在需要旋转所有图形,但是想跳过圆形(因为圆形旋转没有意义)。这时我们就希望知道 `Stream<Shape>` 里边的形状具体是什么类型而Java 实际上也满足了我们的这种需求。使用 RTTI我们可以查询某个 `Shape` 引用所指向对象的确切类型,然后选择或者剔除特例。
<!-- The Class Object -->
## `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` 对象,如下所示:
<figure>
<table style="text-align:center;">
<thead>
<tr>
<th colspan="2">...等价于...</th>
</tr>
</thead>
<tbody>
<tr>
<td>boolean.class</td>
<td>Boolean.TYPE</td>
</tr>
<tr>
<td>char.class</td>
<td>Character.TYPE</td>
</tr>
<tr>
<td>byte.class</td>
<td>Byte.TYPE</td>
</tr>
<tr>
<td>short.class</td>
<td>Short.TYPE</td>
</tr>
<tr>
<td>int.class</td>
<td>Integer.TYPE</td>
</tr>
<tr>
<td>long.class</td>
<td>Long.TYPE</td>
</tr>
<tr>
<td>float.class</td>
<td>Float.TYPE</td>
</tr>
<tr>
<td>double.class</td>
<td>Double.TYPE</td>
</tr>
<tr>
<td>void.class</td>
<td>Void.TYPE</td>
</tr>
</tbody>
</table>
</figure>
我的建议是使用 `.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` 类的一个对象。
<!-- > 译者的理解: `Class` 对象是 `Class` 类产生的对象,而再往深一点说,`Class` 类的 `Class` 对象(`Class.class`)也是其本类产生的对象。即一切皆对象,类也是一种对象。 -->
但是Java设计者看准机会将它的类型变得更具体了一些。Java引入泛型语法之后我们可以使用泛型对 `Class` 引用所指向的 `Class` 对象的类型进行限定。在下面的实例中,两种语法都是正确的:
```java
// typeinfo/GenericClassReferences.java
public class GenericClassReferences {
public static void main(String[] args) {
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // Same thing
intClass = double.class;
// genericIntClass = double.class; // Illegal
}
}
```
普通的类引用不会产生警告信息。你可以看到,普通的类引用可以重新赋值指向任何其他的 `Class` 对象,但是使用泛型限定的类引用只能指向其声明的类型。通过使用泛型语法,我们可以让编译器强制执行额外的类型检查。
那如果我们希望稍微放松一些限制,应该怎么办呢?乍一看,下面的操作好像是可以的:
```java
Class<Number> 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<Number>` 不同,现在做如下声明:
```java
// typeinfo/BoundedClassReferences.java
public class BoundedClassReferences {
public static void main(String[] args) {
Class<? extends Number> 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<T> implements Supplier<T> {
private Class<T> type;
public DynamicSupplier(Class<T> 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<FancyToy> ftClass = FancyToy.class;
// Produces exact type:
FancyToy fancyToy = ftClass.newInstance();
Class<? super FancyToy> up =
ftClass.getSuperclass();
// This won't compile:
// Class<Toy> up2 = ftClass.getSuperclass();
// Only produces Object:
Object obj = up.newInstance();
}
}
```
如果你手头的是超类,那编译期将只允许你声明超类引用为“某个类,它是 `FancyToy` 的超类”,就像在表达式 `Class<? super FancyToy>` 中所看到的那样。而不会接收 `Class<Toy>` 这样的声明。这看上去显得有些怪,因为 `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<House> 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` 对象转型为更加具体的类型。
<!-- Checking Before a Cast -->
@@ -171,8 +563,8 @@ Class.forName("Gum");
<!-- Instanceof vs. Class Equivalence -->
## 类的等价比较
<!-- Reflection: Runtime Class Information -->
## 反射运行时类信息

File diff suppressed because it is too large Load Diff

View File

@@ -367,8 +367,6 @@
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<!--统计访问量插件-->
<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
<!--代码块复制插件-->
<script src="//unpkg.com/docsify-copy-code"></script>
<!-- 图片缩放插件 -->
@@ -380,5 +378,7 @@
<script src="//unpkg.com/prismjs/components/prism-bash.min.js"></script>
<!--分页插件-->
<script src="//unpkg.com/docsify-pagination/dist/docsify-pagination.min.js"></script>
<!--统计访问量插件-->
<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
</body>
</html>