“我的语言之局限,即我的世界之局限。”
——Ludwig Wittgenstein(1889-1951)
这句话不仅适用于我们日常读写的语言,也适用于编程语言。很微妙的一件事是,一门语言会悄然无息地引导你进入某种思维模式,同时远离其他思维模式。Java尤其如此。
Java是一门派生语言。当时的情况是,早期的语言设计师不想用C++来开发项目,于是创建了一门和C++极为相似的新编程语言,不过也做出了一些改进。这种新编程语言最主要的改动是加入了 虚拟机 和 垃圾收集 机制,本书后续章节会对这两点进行详细介绍。此外,Java还在其他方面推动着行业的持续发展。
Java最主要的概念之一来自SmallTalk,这门语言强调“对象”(详见第1章)是编程的基本单位,所以任何东西都必须是对象。经历过长时间的洗礼之后,这个概念被证明是有些激进的,有些人甚至断定对象的概念是彻头彻尾的失败,应该果断丢弃。我个人认为,把所有内容都封装为对象不仅是一种负担,而且还会将许多程序设计推向错误的方向。然而不可否认的是,在一些情况下对象依然十分有用。所以,将一切都封装为对象(尤其是深入到最底层的时候)是一种设计失误,但完全抛弃对象同样太过极端。
Java还有一些设计决策也没有达成预期目标。关于这一点,本书中会陆续加以说明,以便你能够理解这些语言特性如鲠在喉的原因何在。但是,我并不是要将Java盖棺定论为一门优秀或拙劣的语言。我想表达的是,如果你了解了一门语言的不足之处和局限性,当你遇到某个语言特性不可用时,就不会被卡住,以致无法继续。同时,因为你已经知晓其局限性,所以就可以更好地进行程序设计。
编程是一门管理复杂性的艺术,而问题的复杂程度取决于机器的复杂程度。由于这种复杂性的存在,导致了大多数编程项目的失败。
许多编程语言在设计时充分考虑了复杂性的问题,然而有时候,其他问题才是更为本质的问题。几乎不可避免的是,那些“其他问题”才是让使用该语言的程序员最终碰壁的原因。例如,C++语言不得不向后兼容C语言(这是为了让C语言程序员更容易上手),同时还要保证运行效率。不可否认的是,这两者都是非常实用的设计目标,并且成了C++语言获得成功的功臣,但是随之也带来了大量额外的复杂性。
Visual Basic(VB)语言依赖于BASIC语言,而BASIC语言本身并不是一种扩展性良好的语言。这导致VB在扩展时经常出现各种非常难以维护的语法。Perl语言能够向后兼容awk、sed、grep以及其他UNIX工具,然而这些旧时代的工具本身就是需要被替换和更新的。结果就是,Perl程序里面充斥着大量的“只写代码”(意思是你自己都读不懂自己写的代码是什么意思)。不过话说回来,C++、VB、Perl以及其他一些语言(比如SmallTalk)都提供了一些能够处理复杂性的设计方案,并且从解决特定问题的角度来看,它们做得还相当不错。
信息革命让我们所有人可以更为便捷地交流,不管是一对一、在群组之内还是在全球范围内。我听说下一次革命将促生一个由足够多的人和连接组合而成的全球化的大脑。Java会不会成为这种革命所需的工具之一呢?一切皆有可能。
本书目标
本书每一章都会介绍一个或者一组互相关联的概念,同时这些概念不依赖于当前章节没有介绍的特性。因此,你可以结合当前获取的知识来充分理解上下文,然后再阅读下一章。
我个人为本书设定的目标如下。
1.循序渐进地呈现相关知识点,以便你充分理解每一个理念,之后再继续前行。同时,精心编排语言特性的介绍顺序,以便你在看到某个特性的运用之前,先对该特性的概念有所了解。然而我并不能保证百分之百可以做到这一点,当出现意外情况时,我也会提供一些简要的相关说明。
2.所使用的示例尽可能地浅显易懂。有时候我会因为这一条原则而放弃引入所谓“现实世界”的问题,然而我发现对于初学者而言,相比于因为示例解决了一个范围很大的问题而感到惊讶,当他们理解示例中所有细节的时候会觉得更有收获。对于这一点,也许有人会批评我只热衷于“简单示例”,但是为了产生更为明显的教育成效,我依然乐于接受目前的做法。
3.我相信有些细节对于95%的程序员而言是无关紧要的。这些细节只会让人们感到困惑,并且增加他们对于语言复杂度的认知。
4.为你打下坚实的编程语言基础,以便你之后学习难度更大的课程和图书时,可以充分理解自己所遇到的问题。
语言设计缺陷
每一种语言都存在设计缺陷。屡屡让新手程序员感到不安和挫败的是,他们必须“周旋”于各种语言特性之中,不断猜测应该用什么、不应该用什么。承认错误总是让人感到不快,但是相比承认错误所带来的不适感,这种糟糕的新手体验要严重得多。令人尴尬的是,所有失败的语言/库设计一直存在于Java的发布版本里。
诺贝尔经济学奖得主Joseph Stiglitz有一句生活哲言十分应景,也叫作“承诺升级理论”(The Theory of Escalating Commitment):
持续犯错的代价由别人承担,而承认错误的代价由你自己来承担。
当我发现编程语言的设计缺陷时,我倾向于指出这些问题。Java发展到今天,已拥有了许多热心的拥护者,其中有些人甚至将Java视为自家“孩子”,而非一种语言工具。因为我编写了一些关于Java的著作,所以他们以为我也会像他们一样袒护Java。于是,当我发现了某个语言缺陷并进行批判时,经常会出现以下两种情况:
1.起初会引起一阵类似于“我的孩子无论对错”的愤怒,到了最后(也许会经过许多年),该缺陷逐渐被大家广泛承认,从此被视为Java的历史遗留问题。
2.更为关键的是,新手程序员并没有经历过“想不通为什么会这样”的痛苦挣扎,尤其是发现了某个看起来不对劲儿的地方之后所产生的自我怀疑,在这种情况下人们会很自然地认为 要么是自己做错了,要么就是自己还没有搞明白 。更糟糕的是,有些教授该语言的人会直接引用一些错误的概念,而不是对问题进行更加深入的研究和分析。而如果能够理解语言的设计缺陷,即使是新手程序员也能够理解不对劲儿的地方是一个错误,从而绕过它继续前行。
我认为,理解语言和库的设计缺陷是必要的,因为它们会影响程序员的生产力。有些语言特性非常具有吸引力,但可能会在你毫无准备之时突然卡住你的工作进程。此外,设计缺陷也会影响新语言的采用。探索一门语言能做什么的过程十分有趣,然而设计缺陷能够告诉你该语言 不能 做什么。
多年以来,我真切地感受到Java语言的设计者不够关心用户。有些语言缺陷可谓太过明显,根本没有经过深思熟虑,看起来像是设计者的思绪早已飞到了九霄云外,对自己的用户不管不顾。而这种对程序员看似不尊重的态度,也是我当初放弃Java选择其他语言,并且在相当长的一段时间内都不想回头的主要原因。
而当我重新回过头来审视Java的时候,Java 8给我的感觉焕然一新,就好像是该语言的设计者对于语言和用户的态度发生了180度大转弯。比如,许多被用户诟病已久,甚至被视为语言毒瘤的特性和库都得到了修正。新引入的特性也让人耳目一新,就好像是设计团队中新加入了几位极其关注程序员使用体验的设计者。这些设计者终于行动了起来并致力于让Java语言变得更为出众,这明显好过在没有深入探究一个理念的本质时就急不可待地把它添加进来。此外,部分新特性十分优雅(至少可以说在考虑到Java局限性的情况下,已经尽可能地优雅了)。
得益于语言设计者的良苦用心(其实我并没有料想到这一点),编写本书的过程相比以往要顺利得多。Java 8包含了许多基础和重要的改进,而由于Java一直严格遵守自己的向后兼容性承诺,做出这些改进无疑需要花费相当多的精力。因此可以预料的是,将来也很难再见到如此重大的改进了(关于这一点,希望我是错的)。话虽如此,我依然要为那些把Java重新带入正确航道的人献上掌声。当终于能够用Java 8编写出某段代码时,我第一次下意识地喊出:“我爱死这个了!”
普及程度
Java的普及具有重要意义。我的意思是,如果你学会了Java,也许找工作会容易一些,而且市面上有大量的Java培训材料、课程以及其他学习资源等。另外,如果你开一家公司并且选择Java作为工作语言,招募Java程序员时也会容易一些。Java的这一点优势确实无可争辩。
话虽如此,目光短浅终归不是好事。如果你并不是真心喜爱Java,建议你还是远离它为好。我的意思是,如果学习Java只是为了找工作,无异于选择了一种不幸福的人生。而对于公司来说,如果你选择Java只是为了降低招聘难度,请务必三思而后行。根据你的实际需求,也许采用其他语言的话,你可以雇用更少的员工,但能达到更高的生产力(比如通过我的另一本书Atomic Kotlin学习Kotlin语言)。此外,使用一种更新也更激动人心的编程语言也许更容易吸引有志之士的加盟。
不过,如果你真的喜爱Java这门语言,那么欢迎你加入。同时,我希望本书可以进一步丰富你的编程经验。
Java新的“发布节奏”
Java的版本号总是显得十分怪异。比如Java早期的1.1~1.4版本使用带小数点的数字代表主版本号,到了Java 5则变成使用整数代表主版本号。
现在Java拥有了一套新的版本号规则,也可以称之为“发布节奏”,内容如下。
1.每隔6个月发布一个新版本,使用整数作为版本号。
2.发布的版本会包含一些试用功能,让用户可以体验和指出问题。而这种6个月的版本节奏,其主要目的可能就在于让用户尽早发现功能试用的相关问题。不过,由于无法保证这些功能之后能够长期存在,一旦这些功能出于某些原因没有达成预期的效果,它们就会被取缔。所以,你不应该依赖这些试用性质的功能。
3.区分清楚短期支持(Short-Term-Support,STS)版本和长期支持(Long-Term-Support,LTS)版本。Java 8、11、17都是LTS版本,其他版本则是支持周期只有6个月的STS版本。具体而言,只要有新版本问世,对STS版本的支持即宣告终止。类似地,一旦有新的LTS版本问世,(通常在一年以内)很快也会停止对原LTS版本的支持(这里指的是Oracle所提供的免费支持,也就是说,OpenJDK可能会支持更长时间)。
值得一提的是,STS版本和LTS版本都可能包含一些试用性质的功能。
此外,每一个Java版本都会包含不同类型的功能试用,举例如下。
●实验(Experimental):代表该功能仍处于早期阶段,可以认为完成度只有25%左右。
●预览(Preview):该功能已经完全实现,但是在最终确定之前仍然可能会有所调整。可以认为这些功能达到了beta版本,甚至是候选发布(Release Candidate,RC)版本的标准。有时候会看到,某些功能带有标注“预览2”(Preview 2),这大概表示此功能已经做出了一些修改,同时希望之后可以获得一些相关的反馈。
●孵化中(Incubating):代表一个 API或工具(相对于语言的核心功能而言)还不是Java发布内容的一部分。因为Java的标准下载包并不会包含这些内容,所以必须主动获取这些API或工具才能使用它们。比如jshell ,在Java 8里依然是一个孵化中的功能,然而从Java 9开始,它就成了正式发布版本的一部分。
实验和孵化中的功能被统称为“非正式”功能。非正式功能默认不会被启用,需要通过命令行或者IDE的设置菜单手动启用它们。本书之后的内容也会介绍一些Java 17所包含的非正式功能,并且相关示例也会指引如何通过命令行编译这些功能。
对于大多数公司和程序员来说,关注STS版本不仅可能需要付出额外的精力,而且使用这种生命周期较短的版本究竟有多少回报也让人存疑,所以我只推荐使用LTS版本。如果你只更新LTS版本,那就没什么问题了,而且无须担心STS版本的快速更新所带来的影响。
图形用户界面
对于Java而言,图形用户界面(GUI)和桌面编程代表着一段动荡甚至有些悲惨的历史。
在Java 1.0时代,GUI库最初的设计目的是让程序员可以创建一种在所有平台上看起来都光鲜亮丽的GUI。遗憾的是,这个目标并没有达成。取而代之的是,Java 1.0通过 抽象窗口工具集 (Abstract Windowing Toolkit,AWT)创建了一种在所有平台上都表现平平的GUI。不仅如此,这套GUI还有一些局限性。比如,你最多只能使用4种字体,而且你不能调用操作系统中任何成熟的GUI组件。此外,Java 1.0 AWT的编程模式最令人尴尬的是,它甚至不支持面向对象编程。我的研讨班中的一名学生(他曾经在Sun公司经历过最初创造Java语言的那段时光)曾经解释过这一情况:最初的AWT是在一个月之内构想、设计和实现出来的。这样的产能效率纵然让人称奇,却也是体现框架设计重要性的一份反面教材。
随后发展到Java 1.1 AWT事件模型的时期,情况终于有所改善。这次的AWT使用一种更为清晰且面向对象的编程方式,同时添加了一种名为JavaBeans的组件编程模式(现已不复存在),其目的是可以轻松创建可视化的编程环境。到了Java 2(也叫Java 1.2)时期,Java不再继续改进Java 1.0 AWT,而是用 Java基础类 (Java Foundation Classes,JFC)重写了一切,其中GUI部分称为“Swing”。通过JavaBeans及其丰富的代码库,用户可以创建出效果不错的GUI。
然而,Swing也不是Java语言GUI库的最终解决方案,随后Sun公司又做出了最后一次努力,推出了JavaFX。当Oracle公司收购Sun公司后,他们将这个曾经野心勃勃的项目(其中甚至还包含了一种脚本语言)调整为Java的一个库,现在它似乎是唯一一个得以继续开发的UI工具包(详情请参考维基百科关于JavaFX的文章)。然而即便是这种程度的开发力度也难以为继,于是JavaFX和它的几个前辈一样,最终也难逃覆灭的命运。
现如今,Swing依然是Java的一部分(不过只是维护,没有再开发新内容)。因为Java现在已经是开源项目,所以也可以轻松获取Swing。此外,Swing和JavaFX之间存在一些有限的交互,因其原本的目的是将Swing的功能移植到JavaFX中。
归根结底,Java在桌面领域从未真正强大过,甚至从未触及设计师的雄心壮志。至于其他,比如JavaBeans,也总是雷声大、雨点小(不幸的是,有不少人花费了大量心血来编写关于Swing的书,甚至是仅仅关于JavaBeans的书),始终没有获得大众的青睐。结果就是,Java在桌面领域的大多数应用场景是IDE以及一些企业内部的应用程序。虽然人们确实也会用Java开发用户界面,但要清楚地意识到,这只是Java语言的一个小众需求。
JDK HTML文档
Oracle公司为Java开发工具集(Java Development Kit,JDK)提供了电子文档,用Web浏览器即可查看。除非必要,本书不会重复文档的内容,因为你用浏览器查看一个类的详细说明要比在本书中查找快得多(此外,在线文档的内容还是即时更新的)。所以在本书中,通常我只会提及某处需要参考“JDK文档”。如果JDK文档的内容不足以让你理解某个特定的示例,我也会提供额外的说明。
经过测试的示例
本书提供的示例使用的是Java 8环境和Gradle编译工具。虽然我也使用新版本的Java测试过这些示例,但我依然推荐使用该语言的LTS版本:在我写这本书时,对应的是Java 11或Java 17。此外,本书所有示例都可以从GitHub仓库免费获取。
每当构建一个应用程序时,如果没有一套内置测试流程来测试你的代码,就无法判断该代码是否坚实可信。因此,我为本书创建了一套测试系统,用于展示和验证大多数示例的输出结果。具体而言,运行示例代码后的输出结果会包含一段注释,附加在代码的末尾处。有时候注释并不显示全部内容,而是只显示开头的几行,或者开头和末尾的几行。这种嵌入式的输出方式提升了代码可读性,降低了学习门槛,同时也提供了一种验证代码正确性的方式。
代码规范
在本书中,各种标识符(关键字、方法名、变量名、类名等)会以等宽字体显示。而例如“类”(class)等频繁出现的关键字,如果使用特殊字体的话反而可能会让人感到不适。因此,具备足够辨识度的词语将采用常规字体显示。
本书示例会采用一种特定的编程风格。在尽可能满足本书格式要求的前提下,这种编程风格和Oracle网站上提供的编程风格几乎完全一致,同时能够兼容大多数Java开发环境。鉴于编程风格这个话题足以引发长达数小时的激烈争论,我需要在此澄清的是,我并没有试图通过我的代码示例来表明何为正确的编程风格,我使用的编程风格完全只是根据自己的意愿而为之。由于Java是一种形态自由的编程语言,所以你可以按照自己的喜好选择编程风格。此外,在使用诸如IntelliJ IDEA或者Visual Studio Code(VSCode)等IDE(Integrated Development Environment,集成开发环境)时,你可以设置自己熟悉的编程风格,以此解决编程风格不一致的问题。
本书的源代码都通过了自动化测试,最新版本的Java应该可以正常运行这些源代码(除了被特别标识的内容)。
bug反馈
即使作者本人用尽各种办法来检测编程错误,依然可能会有漏网之鱼,通常新的读者可能会有所发现。在阅读本书的过程中,只要你确信自己发现了某处错误,不管是文字还是代码示例问题,请第一时间将该错误以及你修正后的内容提交到:https://github.com/BruceEckel/Onjava8-examples/issues。 感谢你的帮助!
源代码
本书所有源代码都可以在GitHub网站上获取:https://github.com/BruceEckel/Onjava8-examples。这些源代码可以用于在校学习或者其他教育类场景。
源代码的版权保护主要是为了确保这些源代码可以被正确地引用,以及防止在未经授权的情况下被随意发布。(只要是本书中引用了版权信息的源代码,在大多数情况下,使用是没有问题的。)
在所有源代码文件里,你都会发现类似以下的版权信息说明:
在编程过程中,只要你在每一个源代码文件里都保留了上面提及的版权信息,这些源代码就可以用于你的项目以及在校学习等教育用途(包括幻灯片演示等文件)。
获取随书资源
扫描下方二维码,获取“随书源码”和“导读指南”。