像古代诗人一样,我们把Java编程的发展分为4个不同的风格时期——原始风格、Bean风格、企业风格和现代风格。
Java最初用于家电和交互式电视等领域,直到Netscape在其广受欢迎的Navigator浏览器中采用Java applet后,Java才开始流行。Sun公司发布了JDK(Java开发工具包)1.0,随着微软在IE浏览器中加入了Java,一瞬间,每个拥有Web浏览器的人都拥有了Java运行环境。人们对Java编程语言的兴趣激增。
此时,Java的基础已经奠定:
● Java虚拟机及其字节码和类文件格式。
● 原始类型和引用类型、空引用、垃圾回收。
● 类、接口、方法和控制流语句。
● 用于错误处理的检查异常和抽象的视窗化工具箱。
● 用于与互联网和网络协议联网的类。
● 代码在运行时的加载和链接,由安全管理器进行沙箱管理。
然而,Java还没有准备好支持通用编程:JVM的运行速度很慢,标准库稀少。
Java看起来像是C++和Smalltalk的混合品,这两种语言影响了当时的Java编程风格。备受其他语言程序员嘲笑的“getFoo/setFoo”和“AbstractSingletonProxyFactoryBean”约定还没有普及。
Java的一项无名创新是一种官方编码约定,它规定了程序员应该如何命名包、类、方法和变量。C和C++程序员遵循着看似无限多样的编码约定,而在将多个库组合在一起时,代码最终看起来却有些杂乱不一致。Java唯一的编码约定是Java程序员可以将陌生人的库无缝地集成到自己的程序中,这推动了一个生机勃勃的开源社区的发展,这个社区一直持续到今天。
在Java取得初步的成功之后,Sun公司开始将其作为构建应用程序的实践工具。Java 1.1(1996)添加了新的语言特性(最显著的是内部类),改进了运行时的主要特性(最显著的是即时编译和反射),并扩展了标准库。Java 1.2(1998)添加了标准的Collections API和跨平台GUI框架Swing,这确保了Java应用程序在每个桌面操作系统上看起来和感觉都是一样的笨拙。
此时,Sun公司正紧盯着微软和Borland在企业软件开发方面的主导地位。Java有可能成为Visual Basic和Delphi的有力竞争者。受Microsoft API的启发,Sun添加了大量的API:用于数据库访问的JDBC(相当于Microsoft ODBC)、用于桌面GUI编程的Swing(相当于Microsoft MFC)以及对Java编程风格影响最大的框架JavaBeans。
JavaBeans API是Sun公司对Microsoft ActiveX组件模型的回应,用于低代码、图形化和拖放式编程。Windows程序员可以在其Visual Basic程序中使用ActiveX组件,或将其嵌入公司内网上的Office文档或网页中。尽管ActiveX组件的使用非常简单,但它们非常难写。JavaBeans要容易得多,你只需遵循一些额外的编码约定,就可以将Java类视为可以在图形设计器中实例化和配置的Bean。“一次编写,随处运行”的承诺意味着你能够在任何操作系统上使用JavaBean组件,而不仅仅在Windows上。
一个类如果想要成为JavaBean,需要有一个不带参数的构造函数,可序列化,并声明一个API,API中包括一组可读且部分可写的公共属性、可调用的方法以及该类对象所发出的事件。这样,程序员就可以在图形化的应用程序设计器中实例化Bean,通过设置属性来配置它们,并将Bean发出的事件连接到其他Bean的方法上。默认情况下,Bean API通过以get和set开头的一对方法定义其属性。这种默认的方式可以被重写,但这样做需要程序员编写更多的模板代码类。程序员通常只是改造现有的类,使其成为JavaBean时才会这么做。在新的代码中遵循JavaBean的“纹理”会让你的工作容易得多。
Bean风格的缺点在于它很大程度上依赖于可变的状态,并且要求有比POJO(Plain Old Java Object,普通旧Java对象)更多的公开状态,因为可视化构建工具不能向对象的构造函数传递参数,而是必须设置其属性。Bean用于用户界面组件表现良好,因为可以用默认的内容和样式安全地初始化它们,并在构造后进行调整。当一些类没有设置合理的默认值时,以同样的方式处理它们很容易出错,因为类型检查器不能告诉我们Bean所需的值是否已全部提供。Bean约定使编写正确的代码变得更加困难,而且依赖项的修改会悄无声息地破坏调用端代码。
最终,JavaBean的图形化组合并没有成为主流,但它的编码约定流传了下来。Java程序员遵循JavaBean约定,即使他们并不打算将自己的类用作JavaBean。Bean对Java编程风格产生了巨大的、持久的但并不完全是积极的影响。
Java最终在企业中传播开来。它并没有像预期的那样取代企业桌面上的Visual Basic,而是取代了C++,成为服务器端的首选语言。1998年,Sun公司发布了Java 2企业版(当时称为J2EE,现在称为JakartaEE),这是一套用于编写服务器端事务处理系统的标准API。
J2EE API存在抽象反转问题。JavaBeans和applet API也存在抽象反转问题,例如,它们都不允许将参数传递给构造函数,但在J2EE中问题更为严重。J2EE应用程序没有单一的入口点。它由很多小组件组成,其生命周期由应用程序容器管理,并通过JNDI名称服务互相暴露。应用程序需要大量的模板代码和可变的状态来查找它们所依赖的资源。程序员发明了依赖注入(D I)框架来应对这个问题,这些框架可以完成所有资源的查找和绑定,并管理生命周期。其中最成功的是Spring。它建立在JavaBeans的编码约定之上,并使用反射来组成类似Bean对象的应用程序。
抽象反转是一种架构上的缺陷,即软件平台阻止调用端代码使用它所需要的底层机制。这就迫使程序员使用平台API所暴露的高层设施来重新实现这些底层机制,而这些设施又使用了被重新实现的功能。其结果是不必要的代码、糟糕的性能以及额外的维护和测试成本。
以J2EE Servlet为例。在Servlet API中,负责实例化Servlet对象的是Servlet容器,而不是Web应用程序。而事实上,Web应用程序根本不是用Java写的,它是一个XML文件,列出了Servlet容器需要实例化的类。因此,每个Servlet都必须有一个无参构造函数,而Web应用程序无法通过将对象传递给其构造函数来初始化Servlet。
相反,你必须编写一个ServletContextListener来创建应用程序Servlet所需的对象,并将其存储为ServletContext的具名、非类型化属性:
Servlet通过在上下文中寻找它们所需要的对象并将其转换为预期的类型来初始化自身:
如果Servlet API没有阻止你这样做,这比调用构造函数所付出的代价要大得多。而且构造函数的调用会进行类型检查。难怪Java程序员会发现依赖注入框架是一种进步。
在Servlet API首次发布的20多年后,Servlet API终于在3.0版本中允许Web应用程序实例化Servlet并将依赖关系传递给其构造函数。
在编程风格方面,DI框架鼓励程序员避免直接使用new关键字,而是依靠框架来实例化对象。Android的API也表现出抽象反转,Android的程序员也转向DI框架帮助他们编写API。DI框架专注于面向机制而非领域进行建模的方式导致了大量的企业级类名,如Spring中臭名昭著的AbstractSingletonProxyFactoryBean。
不过,从好的方面来看,企业风格时期发布了Java 5,它为Java语言增加了泛型和自动装箱特性,这是迄今为止最重要的变化。在这个时代,有赖于Maven打包约定和中央软件库的推动,Java社区也大量采用了开源库。顶级开源库的出现使得Java被大量用于关键业务应用开发,从而诞生了更多的开源库,形成了一个良性循环。随后是一流的开发工具,包括我们在本书中使用的IntelliJ IDE。
Java 8给语言带来了下一个重大变化——lambda,并对标准库进行了重大补充。Streams API鼓励使用函数式编程风格,在这种风格中,数据的处理是采用流来转换不可变值实现的,而不是改变可变对象的状态。新的日期/时间API忽略了JavaBeans对属性访问器的编码约定,而遵循原始时代通用的编码约定。
云平台的发展意味着程序员不必将服务器部署到Java EE应用容器中。轻量级的Web应用框架让程序员能够编写一个主函数来构建其应用。许多服务器端的程序员不再使用DI框架——函数和对象组合已经足够好了,所以DI框架与时俱进地发布了极度简化的API。由于没有DI框架和可变状态,就不太需要遵循JavaBeans的编码约定。在单个代码库中,暴露不可变值的字段效果良好,因为IDE可以在需要时将字段即时地封装在属性访问器后面。
Java 9中引入了模块,但到目前为止,它们还没有在JDK本身之外得到广泛的应用。在最近的Java版本中,最令人兴奋的是JDK的模块化,以及将那些很少使用的模块从JDK中移除,变成可选择的扩展,例如CORBA。
Java未来有望实现更多的功能,使现代风格更容易应用:record、模式匹配、用户定义的值类型,以及最终将原始类型和引用类型统一到一个类型系统中。
然而,这是一项具有挑战性的工作,需要很多年才能完成。Java从一开始就有一些根深蒂固的矛盾和边缘场景,很难在保持向后兼容的同时将其统一成清晰的抽象。Kotlin有25年的前瞻性,而且是一张干净的“白纸”,具有重新开始的优势。