企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 总结:类型转换真的如此之糟吗? 自从 C++ 模版出现以来,我就一直在致力于解释它,我可能比大多数人都更早地提出了下面的论点。直到最近,我才停下来,去思考这个论点到底在多少时间内是有效的——我将要描述的问题到底有多少次可以穿越障碍得以解决。 这个论点就是:使用泛型类型机制的最吸引人的地方,就是在使用集合类的地方,这些类包括诸如各种 **List** 、各种 **Set** 、各种 **Map** 等你在 [集合](book/12-Collections.md) 和 [附录:集合主题](book/Appendix-Collection-Topics.md) 这两章所见。在 Java 5 之前,当你将一个对象放置到集合中时,这个对象就会被向上转型为 **Object** ,因此你会丢失类型信息。当你想要将这个对象从集合中取回,用它去执行某些操作时,必须将其向下转型回正确的类型。我用的示例是持有 **Cat** 的 **List** (这个示例的一种使用苹果和桔子的变体在 [集合](book/12-Collections.md) 章节的开头展示过)。如果没有 Java 5 泛型版本的集合,你放到容集里和从集合中取回的都是 **Object** 。因此,我们很可能会将一个 **Dog** 放置到 **Cat** 的 **List** 中。 但是,泛型出现之前的 Java 并不会让你误用放入到集合中的对象。如果将一个 **Dog** 扔到 **Cat** 的集合中,并且试图将这个集合中的所有东西都当作 **Cat** 处理,那么当你从这个 **Cat** 集合中取回那个 **Dog** 引用,并试图将其转型为 **Cat** 时,就会得到一个 **RuntimeException** 。你仍旧可以发现问题,但是是在运行时而非编译期发现它的。 在本书以前的版本中,我曾经说过: > 这不止令人恼火,它还可能会产生难以发现的缺陷。如果这个程序的某个部分(或数个部分)向集合中插入了对象,并且通过异常,你在程序的另一个独立的部分中发现有不良对象被放置到了集合中,那么必须发现这个不良插入到底是在何处发生的。 > 但是,随着对这个论点的进一步检查,我开始怀疑它了。首先,这会多么频繁地发生呢?我记得这类事情从未发生在我身上,并且当我在会议上询问其他人时,我也从来没有听说过有人碰上过。另一本书使用了一个称为 **files** 的 list 示例,它包含 **String** 对象。在这个示例中,向 **files** 中添加一个 **File** 对象看起来相当自然,因此这个对象的名字可能叫 **fileNames** 更好。无论 Java 提供了多少类型检查,仍旧可能会写出晦涩的程序,而编写差劲儿的程序即便可以编译,它仍旧是编写差劲儿的程序。可能大多数人都会使用命名良好的集合,例如 **cats** ,因为它们可以向试图添加非 **Cat** 对象的程序员提供可视的警告。并且即便这类事情发生了,它真正又能潜伏多久呢?只要你开始用真实数据来运行测试,就会非常快地看到异常。 有一位作者甚至断言,这样的缺陷将“*潜伏数年*”。但是我不记得有任何大量的相关报告,来说明人们在查找“狗在猫列表中”这类缺陷时困难重重,或者是说明人们会非常频繁地产生这种错误。然而,你将在 [多线程编程](book/24-Concurrent-Programming.md) 章节中看到,在使用线程时,出现那些可能看起来极罕见的缺陷,是很寻常并容易发生的事,而且,对于到底出了什么错,这些缺陷只能给你一个很模糊的概念。因此,对于泛型是添加到 Java 中的非常显著和相当复杂的特性这一点,“狗在猫列表中”这个论据真的能够成为它的理由吗? 我相信被称为*泛型*的通用语言特性(并非必须是其在 Java 中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的集合。类型安全的集合是能够创建更通用代码这一能力所带来的副作用。 因此,即便“狗在猫列表中”这个论据经常被用来证明泛型是必要的,但是它仍旧是有问题的。就像我在本章开头声称的,我不相信这就是泛型这个概念真正的含义。相反,泛型正如其名称所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码段可以应用到更多的类型上。正如你在本章中看到的,编写真正泛化的“持有器”类( Java 的容器就是这种类)相当简单,但是编写出能够操作其泛型类型的泛化代码就需要额外的努力了,这些努力需要类创建者和类消费者共同付出,他们必须理解这些代码的概念和实现。这些额外的努力会增加使用这种特性的难度,并可能会因此而使其在某些场合缺乏可应用性,而在这些场合中,它可能会带来附加的价值。 还要注意到,因为泛型是后来添加到 Java 中,而不是从一开始就设计到这种语言中的,所以某些容器无法达到它们应该具备的健壮性。例如,观察一下 **Map** ,在特定的方法 `containsKey(Object key) `和 `get(Object key)` 中就包含这类情况。如果这些类是使用在它们之前就存在的泛型设计的,那么这些方法将会使用参数化类型而不是 **Object** ,因此也就可以提供这些泛型假设会提供的编译期检查。例如,在 C++ 的 **map** 中,键的类型总是在编译期检查的。 有一件事很明显:在一种语言已经被广泛应用之后,在其较新的版本中引入任何种类的泛型机制,都会是一项非常非常棘手的任务,并且是一项不付出艰辛就无法完成的任务。在 C++ 中,模版是在其最初的 ISO 版本中就引入的(即便如此,也引发了阵痛,因为在第一个标准 C++ 出现之前,有很多非模版版本在使用),因此实际上模版一直都是这种语言的一部分。在 Java 中,泛型是在这种语言首次发布大约 10 年之后才引入的,因此向泛型迁移的问题特别多,并且对泛型的设计产生了明显的影响。其结果就是,程序员将承受这些痛苦,而这一切都是由于 Java 设计者在设计 1.0 版本时所表现出来的短视造成的。当 Java 最初被创建时,它的设计者们当然了解 C++ 的模版,他们甚至考虑将其囊括到 Java 语言中,但是出于这样或那样的原因,他们决定将模版排除在外(其迹象就是他们过于匆忙)。因此, Java 语言和使用它的程序员都将承受这些痛苦。只有时间将会说明 Java 的泛型方式对这种语言所造成的最终影响。 某些语言,已经融入了更简洁、影响更小的方式,来实现参数化类型。我们不可能不去想象这样的语言将会成为 Java 的继任者,因为它们采用的方式,与 C++ 通过 C 来实现的方式相同:按原样使用它,然后对其进行改进。