@@ -4834,33 +4834,234 @@ Square 11 rotate
4834
4834
*/
4835
4835
```
4836
4836
4837
- < 未完待续>
4838
-
4837
+ 由于使用 Java 8 ,因此不需要 `Apply . apply()` 。
4839
4838
4839
+ 我们首先生成两个 ** Stream ** : 一个是 ** Shape ** ,一个是 ** Square ** ,并将它们展平为单个流。 尽管 Java 缺少功能语言中经常出现的 `flatten()` ,但是我们可以使用 `flatMap(c- > c)` 产生相同的结果,后者使用身份映射将操作简化为“ ** flatten** ”。
4840
4840
4841
+ 我们使用 `peek()` 当做对 `rotate()` 的调用,因为 `peek()` 执行一个操作(此处是出于副作用),并在未更改的情况下传递对象。
4841
4842
4843
+ 注意,使用 ** FilledList ** 和 ** shapeQ** 调用 `forEach()` 比 `Apply . apply()` 代码整洁得多。 在代码简单性和可读性方面,结果比以前的方法好得多。 并且,现在也不可能从 `main()` 引发异常。
4842
4844
4843
4845
< ! -- Assisted Latent Typing in Java 8 -- >
4844
4846
4845
4847
## Java8 中的辅助潜在类型
4846
4848
4849
+ 先前声明关于 Java 缺乏对潜在类型的支持在 Java 8 之前是完全正确的。但是,Java 8 中的非绑定方法引用使我们能够产生一种潜在类型的形式,该形式可以满足创建可在不相关类型上工作的单段代码的要求。 由于 Java 最初并不是设计用于执行此操作的,因此,正如现在可能期望的那样,其结果比其他语言要尴尬得多。
4850
+
4851
+ 我没有在其他地方遇到过这种技术,因此我将其称为辅助潜在类型。
4852
+
4853
+ 我们将重写 ** DogsAndRobots . java** 来演示该技术。 为使外观看起来与原始示例尽可能相似,我仅向每个原始类名添加了 ** A ** :
4854
+
4855
+ ```java
4856
+ // generics/DogsAndRobotMethodReferences.java
4857
+
4858
+ // "Assisted Latent Typing"
4859
+ import typeinfo. pets. * ;
4860
+ import java. util. function. * ;
4861
+
4862
+ class PerformingDogA extends Dog {
4863
+ public void speak () { System . out. println(" Woof!" ); }
4864
+ public void sit () { System . out. println(" Sitting" ); }
4865
+ public void reproduce () {}
4866
+ }
4867
+
4868
+ class RobotA {
4869
+ public void speak () { System . out. println(" Click!" ); }
4870
+ public void sit () { System . out. println(" Clank!" ); }
4871
+ public void oilChange () {}
4872
+ }
4873
+
4874
+ class CommunicateA {
4875
+ public static <P > void perform (P performer ,
4876
+ Consumer<P > action1 , Consumer<P > action2 ) {
4877
+ action1. accept(performer);
4878
+ action2. accept(performer);
4879
+ }
4880
+ }
4881
+
4882
+ public class DogsAndRobotMethodReferences {
4883
+ public static void main (String [] args ) {
4884
+ CommunicateA . perform(new PerformingDogA (),
4885
+ PerformingDogA :: speak, PerformingDogA :: sit);
4886
+ CommunicateA . perform(new RobotA (),
4887
+ RobotA :: speak, RobotA :: sit);
4888
+ CommunicateA . perform(new Mime (),
4889
+ Mime :: walkAgainstTheWind,
4890
+ Mime :: pushInvisibleWalls);
4891
+ }
4892
+ }
4893
+ /* Output:
4894
+ Woof!
4895
+ Sitting
4896
+ Click!
4897
+ Clank!
4898
+ */
4899
+ ```
4900
+
4901
+ ** PerformingDogA ** 和 ** RobotA ** 与 ** DogsAndRobots . java** 中的相同,不同之处在于它们不继承通用接口 ** Performs ** ,因此它们没有通用性。
4902
+
4903
+ `CommunicateA . perform()` 在没有约束的 ** P ** 上生成。 只要可以使用 `Consumer < P > `,它在这里就可以是任何东西,这些 `Consumer < P > ` 代表不带参数的 ** P ** 方法的未绑定方法引用。 当您调用消费者的 `accept()` 方法时,它将方法引用绑定到执行者对象并调用该方法。 由于“函数式编程”一章中描述的“魔术”,我们可以将任何符合签名的未绑定方法引用传递给 `CommunicateA . perform()` 。
4904
+
4905
+ 之所以称其为“辅助”,是因为您必须显式地为 `perform()` 提供要使用的方法引用。 它不能只按名称调用方法。
4906
+
4907
+ 尽管传递未绑定的方法引用似乎要花很多力气,但潜在类型的最终目标还是可以实现的。 我们创建了一个代码片段 `CommunicateA . perform()` ,该代码可用于任何具有符合签名的方法引用的类型。 请注意,这与我们看到的其他语言中的潜在类型有所不同,因为这些语言不仅需要签名以符合规范,还需要方法名称。 因此,该技术可以说产生了更多的通用代码。
4908
+
4909
+ 为了证明这一点,我还从 ** LatentReflection . java** 中引入了 ** Mime ** 。
4910
+
4911
+ ### 使用** Suppliers ** 类的通用方法
4912
+
4913
+ 通过辅助潜在类型,我们可以定义本章其他部分中使用的 ** Suppliers ** 类。 此类包含使用生成器填充 ** Collection ** 的实用程序方法。 “通用化”这些操作很有意义:
4914
+
4915
+ ```java
4916
+ // onjava/Suppliers.java
4917
+
4918
+ // A utility to use with Suppliers
4919
+ package onjava;
4920
+ import java. util. * ;
4921
+ import java. util. function. * ;
4922
+ import java. util. stream. * ;
4923
+
4924
+ public class Suppliers {
4925
+ // Create a collection and fill it:
4926
+ public static < T , C extends Collection<T > > C
4927
+ create(Supplier<C > factory, Supplier<T > gen, int n) {
4928
+ return Stream . generate(gen)
4929
+ .limit(n)
4930
+ .collect(factory, C :: add, C :: addAll);
4931
+ }
4932
+ // Fill an existing collection:
4933
+ public static < T , C extends Collection<T > >
4934
+ C fill(C coll, Supplier<T > gen, int n) {
4935
+ Stream . generate(gen)
4936
+ .limit(n)
4937
+ .forEach(coll:: add);
4938
+ return coll;
4939
+ }
4940
+ // Use an unbound method reference to
4941
+ // produce a more general method:
4942
+ public static < H , A > H fill(H holder,
4943
+ BiConsumer<H , A > adder, Supplier<A > gen, int n) {
4944
+ Stream . generate(gen)
4945
+ .limit(n)
4946
+ .forEach(a - > adder. accept(holder, a));
4947
+ return holder;
4948
+ }
4949
+ }
4950
+ ```
4951
+
4952
+ `create()` 为你创建一个新的 ** Collection ** 子类型,而 `fill()` 的第一个版本将元素放入 ** Collection ** 的现有子类型中。 请注意,还会返回传入的容器的确切类型,因此不会丢失类型信息。
4953
+
4954
+ 前两种方法一般都受约束以与 ** Collection ** 子类型一起使用。`fill()` 的第二个版本适用于任何类型的 ** holder** 。 它需要一个附加参数:未绑定方法引用 `adder. fill()` ,使用辅助潜在类型来使其与任何具有添加元素方法的 ** holder** 类型一起使用。因为此未绑定方法 ** adder** 必须带有一个参数(要添加到 ** holder** 的元素),所以 ** adder** 必须是 `BiConsumer < H ,A > ` ,其中 ** H ** 是要绑定到的 ** holder** 对象的类型,而 ** A ** 是要被添加的绑定元素类型。 对 `accept()` 的调用将使用参数a调用对象 ** holder** 上的未绑定方法 ** holder** 。
4955
+
4956
+ 在一个稍作模拟的测试中对 ** Suppliers ** 实用程序进行了测试,该仿真还使用了本章前面定义的 ** RandomList ** :
4957
+
4958
+ ```java
4959
+ // generics/BankTeller.java
4960
+
4961
+ // A very simple bank teller simulation
4962
+ import java.util.*;
4963
+ import onjava. * ;
4964
+
4965
+ class Customer {
4966
+ private static long counter = 1 ;
4967
+ private final long id = counter++ ;
4968
+ @Override
4969
+ public String toString () {
4970
+ return " Customer " + id;
4971
+ }
4972
+ }
4973
+
4974
+ class Teller {
4975
+ private static long counter = 1 ;
4976
+ private final long id = counter++ ;
4977
+ @Override
4978
+ public String toString () {
4979
+ return " Teller " + id;
4980
+ }
4981
+ }
4982
+
4983
+ class Bank {
4984
+ private List<BankTeller > tellers =
4985
+ new ArrayList<> ();
4986
+ public void put (BankTeller bt ) {
4987
+ tellers. add(bt);
4988
+ }
4989
+ }
4990
+
4991
+ public class BankTeller {
4992
+ public static void serve (Teller t , Customer c ) {
4993
+ System . out. println(t + " serves " + c);
4994
+ }
4995
+ public static void main (String [] args ) {
4996
+ // Demonstrate create():
4997
+ RandomList<Teller > tellers =
4998
+ Suppliers . create(
4999
+ RandomList :: new , Teller::new, 4);
5000
+ // Demonstrate fill():
5001
+ List<Customer > customers = Suppliers . fill(
5002
+ new ArrayList<> (), Customer :: new , 12);
5003
+ customers. forEach(c - >
5004
+ serve(tellers. select(), c));
5005
+ // Demonstrate assisted latent typing:
5006
+ Bank bank = Suppliers . fill(
5007
+ new Bank (), Bank :: put, BankTeller :: new , 3);
5008
+ // Can also use second version of fill():
5009
+ List<Customer > customers2 = Suppliers . fill(
5010
+ new ArrayList<> (),
5011
+ List :: add, Customer :: new , 12);
5012
+ }
5013
+ }
5014
+ /* Output:
5015
+ Teller 3 serves Customer 1
5016
+ Teller 2 serves Customer 2
5017
+ Teller 3 serves Customer 3
5018
+ Teller 1 serves Customer 4
5019
+ Teller 1 serves Customer 5
5020
+ Teller 3 serves Customer 6
5021
+ Teller 1 serves Customer 7
5022
+ Teller 2 serves Customer 8
5023
+ Teller 3 serves Customer 9
5024
+ Teller 3 serves Customer 10
5025
+ Teller 2 serves Customer 11
5026
+ Teller 4 serves Customer 12
5027
+ */
5028
+ ```
5029
+
5030
+ 可以看到 `create()` 生成一个新的 ** Collection ** 对象,而 `fill()` 添加到现有 ** Collection ** 中。第二个版本`fill()` 显示,它不仅与新的和无关的类型 ** Bank ** 一起使用,还与 ** List ** 一起使用。因此,从技术上讲,`fill()` 的第一个版本在技术上不是必需的,但在使用 ** Collection ** 时提供了较短的语法。
4847
5031
4848
5032
< ! -- Summary : Is Casting Really So Bad ? -- >
5033
+
4849
5034
## 总结:类型转换真的如此之糟吗?
4850
5035
5036
+ 自从C ++ 模版出现以来,我就一直在致力于解释它,我可能比大多数人都更早地提出了下面的论点。直到最近,我才停下来,去思考这个论点到底在多少时间内是有效的——我将要描述的问题到底有多少次可以穿越障碍得以解决。
5037
+
5038
+ 这个论点就是:使用泛型类型机制的最吸引人的地方,就是在使用容器类的地方,这些类包括诸如各种 ** List ** 、各种 ** Set ** 、各种 ** Map ** 等你在集合章节和附件:集合主题章节中看到的各种类。在 Java SE 5 之前,当你将一个对象放置到容器中时,这个对象就会被向上转型为 ** Object ** ,因此你会丢失类型信息。当你想要将这个对象从容器中取回,用它去执行某些操作时,必须将其向下转型回正确的类型。我用的示例是持有 ** Cat ** 的 ** List ** (这个示例的一种使用苹果和桔子的变体在集合章节的开头展示过)。如果没有 Java SE 5 的泛型版本的容器,你放到容器里的和从容器中取回的,都是 ** Object ** 。因此,我们很可能会将一个 ** Dog ** 放置到 ** Cat ** 的 ** List ** 中。
5039
+
5040
+ 但是,泛型出现之前的 Java 并不会让你误用放入到容器中的对象。如果将一个 ** Dog ** 扔到 ** Cat ** 的容器中,并且试图将这个容器中的所有东西都当作 ** Cat ** 处理,那么当你从这个 ** Cat ** 容器中取回那个 ** Dog ** 引用,并试图将其转型为** Cat ** 时,就会得到一个 ** RuntimeException ** 。你仍旧可以发现问题,但是是在运行时而非编译期发现它的。
4851
5041
5042
+ 在本书以前的版本中,我曾经说过:
4852
5043
5044
+ > 这不止是令人恼火,它还可能会产生难以发现的缺陷。如果这个程序的某个部分(或数个部分)向容器中插入了对象,并且通过异常,你在程序的另一个独立的部分中发现有不良对象被放置到了容器中,那么必须发现这个不良插入到底是在何处发生的。
5045
+ >
4853
5046
5047
+ 但是,随着对这个论点的进一步检查,我开始怀疑它了。首先,这会多么频繁地发生呢?我记得这类事情从未发生在我身上,并且当我在会议上询问其他人时,我也从来没有听说过有人碰上过。另一本书使用了一个示例,它是一个包含 ** String ** 对象的被称为 ** files** 的列表在这个示例中,向 ** files** 中添加一个 ** File ** 对象看起来相当自然,因此这个对象的名字可能叫 ** fileNames** 更好。无论 Java 提供了多少类型检查,仍旧可能会写出晦涩的程序,而编写差劲儿的程序即便可以编译,它仍旧是编写差劲儿的程序。可能大多数人都会使用命名良好的容器,例如 ** cats** ,因为它们可以向试图添加非 ** Cat ** 对象的程序员提供可视的警告。并且即便这类事情发生了,它真正又能潜伏多久呢?只要你开始用真实数据来运行测试,就会非常快地看到异常。
4854
5048
5049
+ 有一位作者甚至断言,这样的缺陷将“* 潜伏数年* ”。但是我不记得有任何大量的相关报告,来说明人们在查找“狗在猫列表中”这类缺陷时困难重重,或者是说明人们会非常频繁地产生这种错误。然而,你将在第多线程编程章节中看到,在使用线程时,出现那些可能看起来极罕见的缺陷,是很寻常并容易发生的事,而且,对于到底出了什么错,这些缺陷只能给你一个很模糊的概念。因此,对于泛型是添加到 Java 中的非常显著和相当复杂的特性这一点,“狗在猫列表中”这个论据真的能够成为它的理由吗?
5050
+ 我相信被称为* 泛型* 的通用语言特性(并非必须是其在 Java 中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。类型安全的容器是能够创建更通用代码这一能力所带来的副作用。
5051
+ 因此,即便“狗在猫列表中”这个论据经常被用来证明泛型是必要的,但是它仍旧是有问题的。就像我在本章开头声称的,我不相信这就是泛型这个概念真正的含义。相反,泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用到更多的类型上。正如你在本章中看到的,编写真正泛化的“持有器”类( Java 的容器就是这种类)相当简单,但是编写出能够操作其泛型类型的泛化代码就需要额外的努力了,这些努力需要类创建者和类消费者共同付出,他们必须理解适配器设计模式的概念和实现。这些额外的努力会增加使用这种特性的难度,并可能会因此而使其在某些场合缺乏可应用性,而在这些场合中,它可能会带来附加的价值。
4855
5052
5053
+ 还要注意到,因为泛型是后来添加到 Java 中,而不是从一开始就设计到这种语言中的,所以某些容器无法达到它们应该具备的健壮性。例如,观察一下 ** Map ** ,在特定的方法 `containsKey(Object key) `和 `get(Object key)` 中就包含这类情况。如果这些类是使用在它们之前就存在的泛型设计的,那么这些方法将会使用参数化类型而不是 ** Object ** ,因此也就可以提供这些泛型假设会提供的编译期检查。例如,在 C ++ 的 ** map** 中,键的类型总是在编译期检查的。
5054
+ 有一件事很明显:在一种语言已经被广泛应用之后,在其较新的版本中引入任何种类的泛型机制,都会是一项非常非常棘手的任务,并且是一项不付出艰辛就无法完成的任务。在 C++ 中,模版是在其最初的 ISO 版本中就引入的(即便如此,也引发了阵痛,因为在第一个标准 C++ 出现之前,有很多非模版版本在使用),因此实际上模版一直都是这种语言的一部分。在 Java 中,泛型是在这种语言首次发布大约 10 年之后才引入的,因此向泛型迁移的问题特别多,并且对泛型的设计产生了明显的影响。其结果就是,程序员将承受这些痛苦,而这一切都是由于 Java 设计者在设计 1.0 版本时所表现出来的短视造成的。当 Java 最初被创建时,它的设计者们当然了解 C++ 的模版,他们甚至考虑将其囊括到 Java 语言中,但是出于这样或那样的原因,他们决定将模版排除在外(其迹象就是他们过于匆忙)。因此, Java 语言和使用它的程序员都将承受这些痛苦。只有时间将会说明 Java 的泛型方式对这种语言所造成的最终影响。
5055
+ 某些语言,已经融入了更简洁、影响更小的方式,来实现参数化类型。我们不可能不去想象这样的语句将会成为 Java 的继任者,因为它们采用的方式,与 C ++ 通过 C 来实现的方式相同:按原样使用它,然后对其进行改进。
5056
+
5057
+ ## 进阶阅读
4856
5058
4857
5059
[^ 1 ]: 在编写本章期间,Angelika Langer 的 Java 泛型常见问题解答以及她的其他著作(与Klaus Kreft 一起)是非常宝贵的。
4858
5060
[^ 2 ]: [http: // gafter.blogspot.com/2004/09/puzzling-through-erasureanswer.html](http://gafter.blogspot.com/2004/09/puzzling-through-erasureanswer.html)
4859
5061
[^ 3 ]: 参见本章章末引文。
4860
5062
4861
5063
4862
5064
4863
-
4864
5065
< ! -- 分页 -- >
4865
5066
4866
5067
< div style= " page-break-after: always;" >< / div>
0 commit comments