初尝 Java 的滋味

作者:Brian Christeson

当我们告诉一位新手朋友,我们刚刚合著了一本关于 Java 的书时,他说:“您可能会惊讶地发现我听说过 Java。” 我们并不惊讶。在短短一年多的时间里,Java 已经从 Sun 公司一个默默无闻的幕后项目,发展成为计算社区几乎普遍关注和猜测的话题——如果我们没有预料到这一点,那才令人惊讶。

如果 Java 仅仅是另一种功能强大、富有表现力的面向对象编程语言,那么一些开发人员可能会看它一眼,说一句“不错”,然后回去继续埋头苦干 C++ 代码。如果它仅仅提供了一种更灵活、安全和透明的方式,用小程序来丰富网页内容,那么一些网络冲浪者可能会像对“插件”一样感到兴奋。如果它仅仅提供了一种更方便的方式,在异构平台上分发大型应用程序,那么一些大型公司可能会开始研究它的潜力。

然而,Java 结合了所有这些特性以及更多特性,正是这种非凡的组合,使 Java 独一无二地能够吸引整个行业的兴趣,并要求行业中的大多数大牌公司进行大量投资:Borland、Intel、Microsoft、Novell、Oracle、SGI 和 Symantec 等等。即使是一个相当简短的技术概述也应该足以解释人们对 Java 空前的热情。

语言特性

与 C++ 一样,Java 利用了 C 语言的普及性,并保留了其紧凑、富有表现力的特点。与 C++ 不同,它没有试图保持向后兼容性,因此,以及其他原因,它比目前流行的表亲具有许多优势。

虽然 C++ 提供了实现封装、继承、多态性和其他面向对象编程特性的语法,但它也支持传统的结构化(和非结构化)编程。Java 则不然。我们创建的每个 Java 程序,即使是第一个“hello world”,都是面向对象的,这防止了我们倒退到旧的、熟悉的——且效率较低的——方式。

需要支持两种完全不同的编程类型,这使得 C++ 不必要地复杂化。例如,参数可以是内置类型或类类型,并且可以通过值、通过引用或通过指针传递:总共有六种可能的组合。在 Java 中,内置参数始终按值传递,类类型参数始终按引用传递:只有两种组合,并且不需要决策。简单得多。此外,更强的类型化消除了许多引入意外歧义的隐式类型转换。

“等等!没有指针?” 是的。没有指针。C 语言需要它们来支持数组处理和原始形式的按引用调用。Java 支持真正的按引用调用(如 C++ 一样),并且将数组实现为内置类型。所以,没有指针。没有未初始化的指针。没有忘记检查 NULL 指针。没有午夜时分试图找到错误目标的指针的眼泪。

全局变量也遭遇了残酷但罪有应得的命运。在 Java 中,每个数据都整齐地封装起来,只能通过它所属类的操作来访问。因此,编程错误的另一个主要来源完全消失了。自动垃圾回收消除了另一个错误来源:当我们未能释放动态分配的内存时导致的“内存泄漏”。

Java 还抛弃了一种“当时看起来是个好主意”的机制:预处理器。

早期的 C 编译器通过支持函数和数据的前向声明来避免多次解析。程序员可以通过将声明放在单独的头文件中,然后指示预处理器“#include”它们到源文件中,来确保声明、定义和使用之间的一致性。C 和 C++ 实现已经将这种传统延续到一个系统如此庞大的时代,以至于我们必须预编译头文件本身才能获得可接受的重建时间——而头文件维护本身也成为一项主要任务。

在 Java 中,类定义不会在头文件和源文件之间拆分。简单的“import”语句告诉编译器当前源文件需要哪些类,剩下的就由编译器来完成。声明顺序变得不重要。这种方案需要编译器具有更高的复杂性,但消除预处理器的一个结果是,Java 程序通常比 C++ 程序花费更少的时间来构建。另一个结果是,开发人员的生活变得轻松得多。

当然,C/C++ 预处理器解决了声明集中化以外的其他问题,但 Java 编译器可以轻松处理这些其他问题中的大多数。而另一个主要问题完全消失了

需要支持多个平台迫使 C 和 C++ 开发人员投入大量精力来维护备用代码块,并使用条件编译指令来确保在重建特定系统版本时只编译正确的代码块。

Java 让所有这些乏味的工作都消失了,因为该语言独立于任何特定的机器架构。Java 程序是完全可移植的,不仅源代码如此,可执行代码也是如此,这要归功于 Java 虚拟机 (JVM)。

虚拟机

大多数现代语言都是“完全编译”的。编译器生成特定目标平台的“本机代码”,即适用于在特定处理器中运行的特定操作系统的机器语言。一旦程序安装在用户的机器上,操作系统就会直接执行其指令——这种安排以可移植性为代价实现了效率。例如,如果您在基于奔腾的 PC 上运行 Linux,并使用 GNU 的 gcc 编译器创建 C 程序,则生成的 Executable 将在您的机器和类似的机器上运行良好,但不会在运行 OS/2 的奔腾上运行,也不会在运行 Linux 的 DEC Alpha 上运行。

如果您想广泛分发您的程序,您将需要为数量惊人的平台重新编译它,可能需要使用许多不同的开发工具。哦,您还想在销售后继续支持您的软件吗?您真好——开始招聘吧。经验表明,在多个平台上维护软件产品的长期努力远远超过了最初开发它们的努力。而成本与努力成正比——最好寻找一些巨额融资来支付所有这些人的工资。

Java 通过依赖“虚拟机”消除了跨平台开发和支持的复杂性。

正如“虚拟”一词所暗示的那样,Java 编译器的目标是一台实际上不存在的机器。它不是生成特定平台的本机代码,而是生成“字节码”,这是一系列 8 位代码,任何实际机器都无法直接执行。然而,您的程序将运行,不仅在您的 Linux 机器上运行,而且在任何支持 Java 的平台上运行——如今,这与说“在任何流行的平台上”是相同的。

运行时系统

要执行 Java 程序,机器必须具有 Java 运行时系统 (JRTS),这是 JVM 在该平台上的实现——但这仅仅是它运行任何用 Java 编写的程序所需要的全部。JRTS 执行字节码,就像操作系统执行本机机器代码一样。由于运行时系统为每个程序处理所有那些令人讨厌的机器特定问题,因此程序本身不必处理。

将运行时系统与虚拟机混淆是一个常见的错误。即使是那些应该更了解的人有时也会提到“程序在[特定计算机的]虚拟机上运行”——从而掩盖了一个至关重要的区别。Java 独特之处的一部分是,只有一块软件,即 JRTS,了解有关特定平台的任何信息。程序本身仍然愉快地忽略硬件依赖性——程序员也是如此。他们为一台不存在的机器编写代码,并泰然自若地确信这样做会使其可移植到任何流行的平台。

JRTS 根据需要加载已编译的类,执行安全检查,并动态绑定对方法的调用。在这一点上,许多运行时系统开始执行字节码,解释遇到的每个字节码。这种持续的解释限制了执行速度,并且是早期许多关于性能不佳的抱怨的来源。越来越多的 Java 实现通过执行第二个编译步骤“即时编译”来解决这个问题。

即时编译

本机代码编译器以牺牲可移植性为代价来生成快速可执行文件。生成字节码的 Java 编译器以牺牲速度为代价来实现可移植性——如果 JRTS 每次遇到指令时都解释该指令。

许多运行时系统并非如此。它们没有解释器,而是包含一个即时 (JIT) 编译器。JRTS 第一次加载一部分字节码时,JIT 编译器会将其转换为本机代码。此后,运行时系统执行本机代码而不是解释字节码;执行速度显着提高。

值得强调的是,用户获得了完全编译程序的速度而无需牺牲可移植性。JIT 编译器是 JRTS 的一部分,而不是 Java 源代码编译器,因此所有平台特定的知识都仅存在于用户机器上的运行时系统中,这是它所属的位置。软件开发人员继续编译和分发相同的可移植、体系结构中立的字节码文件。

第二个编译步骤并不像听起来那么昂贵。JIT 编译在实践中实际上非常快,因为最耗时的任务在第一次翻译中完成,从原始 Java 源代码到字节码。JIT 编译的代码目前比解释的字节码快 20 到 30 倍;这种性能水平与用 C++ 编写的面向对象代码的性能相当。未来的改进可能会将这个比率提高到 50 或更高,这将使 Java 可执行文件与优化的 C 代码相提并论。

安全问题

Java 的虚拟机概念在多个层面上提高了安全性以及可移植性。

由于传统的完全编译程序是本机代码,因此它处于一个令人不安的有利位置,可以利用操作系统或硬件中的弱点并造成严重损害。相比之下,Java 字节码是体系结构中立的,它对平台一无所知,因此无法利用平台。然而,这种“被动保护”仅仅是开始。

强类型化,包括添加布尔类型,用类型安全的引用替换指针,以及消除其他麻烦的功能,使得执行运行时检查以验证程序的正确性成为可能。

此外,运行时系统的字节码验证器在加载时以多种方式验证每个程序:它只是拒绝任何不符合独特字节码文件格式的文件,从而避免执行可能看起来是有效的 Java 指令但实际上不是的指令。当确信文件格式正确时,验证器会检查字节码本身是否存在格式错误的结构。然后,它继续搜索通常在运行时之前未检测到的错误,例如堆栈溢出。

JRTS 的另一个部分,类加载器,通过将类彼此隔离在单独的安全域中,进一步增强了安全性。为了防范恶意代码,它将内置于运行时系统本身的类与用户帐户本地的类分开,并将这两者与来自其他用户和其他系统的类分开。因此,恶意“外部类”无法伪装成更受信任的类。

用户理所当然地担心病毒或特洛伊木马会通过从 Internet 下载的 applet 进入他们的系统。为了保护用户的系统,运行时系统采用了 Java 使之成为可能的安全功能的组合,除了字节码验证和类分区之外。Web 浏览器或其他软件包通常允许用户从多个安全级别中进行选择,以便他们可以拒绝或限制“不受信任的”applet 访问网络连接和本地文件存储。清晰可见的标记区分了受信任和不受信任的 applet 创建的窗口,以便后者无法伪装成前者。

关于通过臭名昭著的不安全互联网下载可执行代码所固有的风险,已经有很多讨论。使用“插件”的经验已经造成了一些合理的担忧,但重要的是要向马克·吐温那句谚语中的猫学习,不要仅仅因为我们曾经跳到热炉子上就避开凉爽的炉子。Java 太新了,我们不能轻率地驳斥所有这些担忧,但其许多安全功能使其比同类技术安全得多。

有些人不会满足于高于零的任何风险级别;对于他们来说,唯一的建议只能是完全戒除互联网的乐趣。其他人意识到,某些风险是这个世界生活中不可避免的特征,他们可以通过从可靠的供应商处获得 Java 运行时系统来保护自己,其方式与他们用来获取其他软件的方式一样安全。这样做应该将风险降低到大多数人可以接受的水平。

结论

任何新技术的最初用途通常都是非常琐碎的。如果我们对 Java 的唯一接触是可爱的动画和下载的计算器,那么很容易低估它的潜力。我们希望这篇简短的概述表明,Java 提供的远不止跳动的脑袋——即使我们没有空间来描述 Java 分离实现继承和接口继承的巧妙方式,以及它对多线程的内置支持,等等……

Brian Christeson 与 John Mitchell 合著了 Making Sense of Java。他们正在从事与 Java、Tcl/Tk 和其他语言相关的专业课程、其他书籍、编译器以及咨询/开发项目。Brian 在美国和国外的主要公司讲授 OO 分析、设计和编程。

John Mitchell 与 Brian Christeson 合著了 Making Sense of Java。他们正在从事与 Java、Tcl/Tk 和其他语言相关的专业课程、其他书籍、编译器以及咨询/开发项目。John 用 OO 汇编语言开发了 PDA 软件,并为 JavaWorld 杂志撰写两个专栏。

加载 Disqus 评论