From e8e2374f660f1e758598e258e1f0c11572d58476 Mon Sep 17 00:00:00 2001 From: xiangflight Date: Thu, 14 Nov 2019 20:06:49 +0800 Subject: [PATCH] =?UTF-8?q?revision[19]=20=E7=BB=93=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/19-Type-Information.md | 39 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md index eef85d32..75dfa68e 100644 --- a/docs/book/19-Type-Information.md +++ b/docs/book/19-Type-Information.md @@ -1446,7 +1446,7 @@ java ShowMethods ShowMethods ## 动态代理 -*代理*是基本的设计模式之一。它是你插入的对象,代替“真实”对象以提供其他或不同的操作---这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。这是一个简单的示例,显示代理的结构: +*代理*是基本的设计模式之一。一个对象封装真实对象,代替其提供其他或不同的操作---这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。这是一个简单的示例,显示代理的结构: ```java // typeinfo/SimpleProxyDemo.java @@ -1514,12 +1514,12 @@ SimpleProxy somethingElse bonobo somethingElse bonobo ``` -因为`consumer()`接受`Interface`,所以它不知道获得的是`RealObject`还是`SimpleProxy`,因为两者都实现了`Interface`。 -但是,在客户端和`RealObject`之间插入的`SimpleProxy`执行操作,然后在`RealObject`上调用相同的方法。 +因为 `consumer()` 接受 `Interface`,所以它不知道获得的是 `RealObject` 还是 `SimpleProxy`,因为两者都实现了 `Interface`。 +但是,在客户端和 `RealObject` 之间插入的 `SimpleProxy` 执行操作,然后在 `RealObject` 上调用相同的方法。 -当你希望将额外的操作与“真实对象”做分离时,代理可能会有所帮助,尤其是当你想要轻松地启用额外的操作时,反之亦然(设计模式就是封装变更---所以你必须改变一些东西以证明模式的合理性)。例如,如果你想跟踪对`RealObject`中方法的调用,或衡量此类调用的开销,该怎么办?这不是你要写入到程序中的代码,而且代理使你可以很轻松地添加或删除它。 +当你希望将额外的操作与“真实对象”做分离时,代理可能会有所帮助,尤其是当你想要轻松地启用额外的操作时,反之亦然(设计模式就是封装变更---所以你必须改变一些东西以证明模式的合理性)。例如,如果你想跟踪对 `RealObject` 中方法的调用,或衡量此类调用的开销,该怎么办?你不想这部分代码耦合到你的程序中,而代理能使你可以很轻松地添加或删除它。 -Java的*动态代理*更进一步,不仅动态创建代理对象而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个*调用处理程序*,该处理程序负责发现调用的内容并决定如何处理。这是`SimpleProxyDemo.java`使用动态代理重写的例子: +Java 的*动态代理*更进一步,不仅动态创建代理对象而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个*调用处理程序*,该处理程序负责发现调用的内容并决定如何处理。这是 `SimpleProxyDemo.java` 使用动态代理重写的例子: ```java // typeinfo/SimpleDynamicProxy.java @@ -1581,11 +1581,11 @@ Interface.somethingElse(java.lang.String), args: somethingElse bonobo ``` -可以通过调用静态方法`Proxy.newProxyInstance()`来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取),希望代理实现的接口列表(不是类或抽象类),以及接口`InvocationHandler`的一个实现。动态代理会将所有调用重定向到调用处理程序,因此通常为调用处理程序的构造函数提供对“真实”对象的引用,以便一旦执行中介任务便可以转发请求。 +可以通过调用静态方法 `Proxy.newProxyInstance()` 来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取),希望代理实现的接口列表(不是类或抽象类),以及接口 `InvocationHandler` 的一个实现。动态代理会将所有调用重定向到调用处理程序,因此通常为调用处理程序的构造函数提供对“真实”对象的引用,以便一旦执行中介任务便可以转发请求。 -`invoke()`方法被传递给代理对象,以防万一你必须区分请求的来源---但是在很多情况下都无需关心。但是,在`invoke()`内的代理上调用方法时要小心,因为通过接口的调用是通过代理重定向的。 +`invoke()` 方法被传递给代理对象,以防万一你必须区分请求的来源---但是在很多情况下都无需关心。但是,在 `invoke()` 内的代理上调用方法时要小心,因为接口的调用是通过代理重定向的。 -通常执行代理操作,然后使用`Method.invoke()`传递必要的参数将请求转发给代理对象。这在一开始看起来是有限制的,好像你只能执行一般的操作。但是,可以过滤某些方法调用,同时传递其他方法调用: +通常执行代理操作,然后使用 `Method.invoke()` 将请求转发给被代理对象,并携带必要的参数。这在一开始看起来是有限制的,好像你只能执行一般的操作。但是,可以过滤某些方法调用,同时传递其他方法调用: ```java // typeinfo/SelectingMethods.java @@ -1670,16 +1670,17 @@ boring3 在这个示例里,我们只是在寻找方法名,但是你也可以寻找方法签名的其他方面,甚至可以搜索特定的参数值。 -动态代理不是你每天都会使用的工具,但是它可以很好地解决某些类型的问题。你可以在Erich Gamma等人的*设计模式*中了解有关*代理*和其他设计模式的更多信息。 (Addison-Wesley,1995年),以及[设计模式](./25-Patterns.md)一章。 +动态代理不是你每天都会使用的工具,但是它可以很好地解决某些类型的问题。你可以在 Erich Gamma 等人的*设计模式*中了解有关*代理*和其他设计模式的更多信息。 (Addison-Wesley,1995年),以及[设计模式](./25-Patterns.md)一章。 + ## Optional类 如果你使用内置的 `null` 来表示没有对象,每次使用引用的时候就必须测试一下引用是否为 `null`,这显得有点枯燥,而且势必会产生相当乏味的代码。问题在于 `null` 没什么自己的行为,只会在你想用它执行任何操作的时候产生 `NullPointException`。`java.util.Optional`(首次出现是在[函数式编程](docs/book/13-Functional-Programming.md)这章)为 `null` 值提供了一个轻量级代理,`Optional` 对象可以防止你的代码直接抛出 `NullPointException`。 虽然 `Optional` 是 Java 8 为了支持流式编程才引入的,但其实它是一个通用的工具。为了证明这点,在本节中,我们会把它用在普通的类中。因为涉及一些运行时检测,所以把这一小节放在了本章。 -实际上,在所有地方都使用 `Optional` 是没有意义的,有时候检查一下是不是 `null` 也挺好的,或者有时我们可以合理的假设不会出现 `null`,甚至有时候检查 `NullPointException` 异常也是可以接受的。`Optional` 最有用武之地的是在那些“更接近数据”的地方,在问题空间中代表实体的对象上。举个简单的例子,很多系统中都有 `Person` 类型,代码中有些情况下你可能没有一个实际的 `Person` 对象(或者可能有,但是你还没用关于那个人的所有信息)。这时,在传统方法下,你会用到一个 `null` 引用,并且在使用的时候测试它是不是 `null`。而现在,我们可以使用 `Optional`: +实际上,在所有地方都使用 `Optional` 是没有意义的,有时候检查一下是不是 `null` 也挺好的,或者有时我们可以合理地假设不会出现 `null`,甚至有时候检查 `NullPointException` 异常也是可以接受的。`Optional` 最有用武之地的是在那些“更接近数据”的地方,在问题空间中代表实体的对象上。举个简单的例子,很多系统中都有 `Person` 类型,代码中有些情况下你可能没有一个实际的 `Person` 对象(或者可能有,但是你还没用关于那个人的所有信息)。这时,在传统方法下,你会用到一个 `null` 引用,并且在使用的时候测试它是不是 `null`。而现在,我们可以使用 `Optional`: ```java // typeinfo/Person.java @@ -1821,13 +1822,13 @@ caught EmptyTitleException 这里使用 `Optional` 的方式不太一样。请注意,`title` 和 `person` 都是普通字段,不受 `Optional` 的保护。但是,修改这些字段的唯一途径是调用 `setTitle()` 和 `setPerson()` 方法,这两个都借助 `Optional` 对字段进行了严格的限制。 -同时,我们想保证 `title` 字段永远不会变成 `null` 值。为此,我们可以自己在 `setTitle()` 方法里边检查参数 `newTitle` 的值。但其实还有更好的做法,函数式编程一大优势就是可以让我们重用经过验证的功能(即便是个很小的功能),以减少自己手动编写代码可能产生的一些小错误。所以在这里,我们用 `ofNullable()` 把 `newTitle` 转换一个 `Optional`(如果传入的值为 `null`,`ofNullable()` 返回的将是 `Optional.empty()`)。紧接着我们调用了 `orElseThrow()` 方法,所以如果 `newTitle` 的值是 `null`,你将会得到一个异常。这里我们并没有把 `title` 保存成 `Optional`,但通过利 `Optional` 的功能,我们仍然如愿以偿的对这个字段施加了约束。 +同时,我们想保证 `title` 字段永远不会变成 `null` 值。为此,我们可以自己在 `setTitle()` 方法里边检查参数 `newTitle` 的值。但其实还有更好的做法,函数式编程一大优势就是可以让我们重用经过验证的功能(即便是个很小的功能),以减少自己手动编写代码可能产生的一些小错误。所以在这里,我们用 `ofNullable()` 把 `newTitle` 转换一个 `Optional`(如果传入的值为 `null`,`ofNullable()` 返回的将是 `Optional.empty()`)。紧接着我们调用了 `orElseThrow()` 方法,所以如果 `newTitle` 的值是 `null`,你将会得到一个异常。这里我们并没有把 `title` 保存成 `Optional`,但通过应用 `Optional` 的功能,我们仍然如愿以偿地对这个字段施加了约束。 `EmptyTitleException` 是一个 `RuntimeException`,因为它意味着程序存在错误。在这个方案里边,你仍然可能会得到一个异常。但不同的是,在错误产生的那一刻(向 `setTitle()` 传 `null` 值时)就会抛出异常,而不是发生在其它时刻,需要你通过调试才能发现问题所在。另外,使用 `EmptyTitleException` 还有助于定位 BUG。 `Person` 字段的限制又不太一样:如果你把它的值设为 `null`,程序会自动把将它赋值成一个空的 `Person` 对象。先前我们也用过类似的方法把字段转换成 `Option`,但这里我们是在返回结果的时候使用 `orElse(new Person())` 插入一个空的 `Person` 对象替代了 `null`。 -在 `Position` 里边,我们没有创建一个表示“空”的标志位或者方法,因为 `person` 字段如果是空 `Person` 对象就表示这个 `Position` 是个空缺位置。之后,你可能会发现你必须添加一个显示的表示“空位”的方法,但是 YAGNI[^2] (You Aren't Going to Need It,你永远不需要它)。 +在 `Position` 里边,我们没有创建一个表示“空”的标志位或者方法,因为 `person` 字段的 `Person` 对象为空,就表示这个 `Position` 是个空缺位置。之后,你可能会发现你必须添加一个显式的表示“空位”的方法,但是正如 YAGNI[^2] (You Aren't Going to Need It,你永远不需要它)所言,在初稿时“实现尽最大可能的简单”,直到程序在某些方面要求你为其添加一些额外的特性,而不是假设这是必要的。 请注意,虽然你清楚你使用了 `Optional`,可以免受 `NullPointerExceptions` 的困扰,但是 `Staff` 类却对此毫不知情。 @@ -2117,7 +2118,7 @@ Mock 对象和桩之间的的差别在于程度不同。Mock 对象往往是轻 ## 接口和类型 -`interface` 关键字的一个重要目标就是允许程序员隔离构件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。比如我们先写一个接口: +`interface` 关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。比如我们先写一个接口: ```java // typeinfo/interfacea/A.java @@ -2164,9 +2165,9 @@ public class InterfaceViolation { B ``` -通过使用 RTTI,我们发现 `a` 是被当做 `B` 实现的。通过将其转型为 `B`,我们可以调用不在 `A` 中的方法。 +通过使用 RTTI,我们发现 `a` 是用 `B` 实现的。通过将其转型为 `B`,我们可以调用不在 `A` 中的方法。 -这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 `interface` 关键字正在保护你,但其实并没有。另外,在本例中使用 `B` 来实现 `A` 这中情况是有公开案例可查的[^3]。 +这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 `interface` 关键字正在保护你,但其实并没有。另外,在本例中使用 `B` 来实现 `A` 这种情况是有公开案例可查的[^3]。 一种解决方案是直接声明,如果开发者决定使用实际的类而不是接口,他们需要自己对自己负责。这在很多情况下都是可行的,但“可能”还不够,你或许希望能有一些更严格的控制方式。 @@ -2462,22 +2463,22 @@ i = 47, I'm totally safe, No, you're not! 但实际上 `final` 字段在被修改时是安全的。运行时系统会在不抛出异常的情况下接受任何修改的尝试,但是实际上不会发生任何修改。 -通常,所有这些违反访问权限的操作并不是什么十恶不赦的。如果有人使用这样的技术去调用标志为 `private` 或包访问权限的方法(很明显这些访问权限表示这些人不应该调用它们),那么对他们来说,如果你修改了这些方法的某些地方,他们不应该抱怨。另一方面,总是在类中留下后门,也许会帮助你解决某些特定类型的问题(这些问题往往除此之外,别无它法)。总之,不可否认,发射给我们带来了很多好处。 +通常,所有这些违反访问权限的操作并不是什么十恶不赦的。如果有人使用这样的技术去调用标志为 `private` 或包访问权限的方法(很明显这些访问权限表示这些人不应该调用它们),那么对他们来说,如果你修改了这些方法的某些地方,他们不应该抱怨。另一方面,总是在类中留下后门,也许会帮助你解决某些特定类型的问题(这些问题往往除此之外,别无它法)。总之,不可否认,反射给我们带来了很多好处。 程序员往往对编程语言提供的访问控制过于自信,甚至认为 Java 在安全性上比其它提供了(明显)更宽松的访问控制的语言要优越[^4]。然而,正如你所看到的,事实并不是这样。 ## 本章小结 -RTTI 允许通过匿名类的引用来获取类型信息。初学者极易误用它,因为在学会使用多态调用方法之前,这么做也很有效。有过程化编程背景的人很容易把程序组织成一系列 `switch` 语句,你可以用 RTTI 和 `switch` 实现功能,但这样就损失了多态机制在代码开发和维护过程中的重要价值。面向对象编程语言是想让我们尽可能的使用多态机制,只在非用不可的时候才使用 RTTI。 +RTTI 允许通过匿名类的引用来获取类型信息。初学者极易误用它,因为在学会使用多态调用方法之前,这么做也很有效。有过程化编程背景的人很容易把程序组织成一系列 `switch` 语句,你可以用 RTTI 和 `switch` 实现功能,但这样就损失了多态机制在代码开发和维护过程中的重要价值。面向对象编程语言是想让我们尽可能地使用多态机制,只在非用不可的时候才使用 RTTI。 然而使用多态机制的方法调用,要求我们拥有基类定义的控制权。因为在你扩展程序的时候,可能会发现基类并未包含我们想要的方法。如果基类来自别人的库,这时 RTTI 便是一种解决之道:可继承一个新类,然后添加你需要的方法。在代码的其它地方,可以检查你自己特定的类型,并调用你自己的方法。这样做不会破坏多态性以及程序的扩展能力,因为这样添加一个新的类并不需要修改程序中的 `switch` 语句。但如果想在程序中增加具有新特性的代码,你就必须使用 RTTI 来检查这个特定的类型。 如果只是为了方便某个特定的类,就将某个特性放进基类里边,这将使得从那个基类派生出的所有其它子类都带有这些可能毫无意义的东西。这会导致接口更加不清晰,因为我们必须覆盖从基类继承而来的所有抽象方法,事情就变得很麻烦。举个例子,现在有一个表示乐器 `Instrument` 的类层次结构。假设我们想清理管弦乐队中某些乐器残留的口水,一种办法是在基类 `Instrument` 中放入 `clearSpitValve()` 方法。但这样做会导致类结构混乱,因为这意味着打击乐器 `Percussion`、弦乐器 `Stringed` 和电子乐器 `Electronic` 也需要清理口水。在这个例子中,RTTI 可以提供一种更合理的解决方案。可以将 `clearSpitValve()` 放在某个合适的类中,在这个例子中是管乐器 `Wind`。不过,在这里你可能会发现还有更好的解决方法,就是将 `prepareInstrument()` 放在基类中,但是初次面对这个问题的读者可能想不到还有这样的解决方案,而误认为必须使用 RTTI。 -最后一点,RTTI 有时候也能解决效率问题。假设你的代码运用了多态,但是为了实现多态,导致其中某个对象的效率非常低。这时候,你就可以挑出那个类,使用 RTTI 为它编写一段特别的代码以提高效率。然而必须注意的是,不要太早的关注程序的效率问题,这是个诱人的陷阱。最好先让程序能跑起来,然后再去看看程序能不能跑得更快,下一步才是去解决效率问题(比如使用 Profiler)[^5]。 +最后一点,RTTI 有时候也能解决效率问题。假设你的代码运用了多态,但是为了实现多态,导致其中某个对象的效率非常低。这时候,你就可以挑出那个类,使用 RTTI 为它编写一段特别的代码以提高效率。然而必须注意的是,不要太早地关注程序的效率问题,这是个诱人的陷阱。最好先让程序能跑起来,然后再去看看程序能不能跑得更快,下一步才是去解决效率问题(比如使用 Profiler)[^5]。 -我们已经看到,反射,因其更加动态的编程风格,为我们开创了编程的新世界。但对有些人来说,反射的动态特性却是一种困扰。对那些已经习惯于静态类型检查的安全性的人来说,Java 中允许这种动态类型检查(只在运行时才能检查到,并以异常的形式上报检查结果)的操作似乎是一种错误的方向。有些人想的更远,他们认为引入运行时异常本身就是一种指示,指示我们应该避免这种代码。我发现这种意义的安全是一种错觉,因为总是有些事情是在运行时才发生并抛出异常的,即使是在那些不包含任何 `try` 语句块或异常声明的程序中也是如此。因此,我认为一致性错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检查的代码是有价值的,只有你有这样的能力。但是我相信动态代码是将 Java 与其它诸如 C++ 这样的语言区分开的重要工具之一。 +我们已经看到,反射,因其更加动态的编程风格,为我们开创了编程的新世界。但对有些人来说,反射的动态特性却是一种困扰。对那些已经习惯于静态类型检查的安全性的人来说,Java 中允许这种动态类型检查(只在运行时才能检查到,并以异常的形式上报检查结果)的操作似乎是一种错误的方向。有些人想得更远,他们认为引入运行时异常本身就是一种指示,指示我们应该避免这种代码。我发现这种意义的安全是一种错觉,因为总是有些事情是在运行时才发生并抛出异常的,即使是在那些不包含任何 `try` 语句块或异常声明的程序中也是如此。因此,我认为一致性错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检查的代码是有价值的,只要你有这样的能力。但是我相信动态代码是将 Java 与其它诸如 C++ 这样的语言区分开的重要工具之一。 [^1]: 特别是在过去。但现在 Java 的 HTML 文档有了很大的提升,要查看基类的方法已经变得很容易了。