知识点上本书需要会Java语法和lang、util、io库,涉及concurrent和function包。
内容上主要和设计模式相关,代码风格力求清晰+简洁,代码尽量复用,组件尽量少依赖,错误尽早发现。
第1个经验法则:用静态工厂方法代替构造器(consider static factory methods instead of constructors)
下面是Boolean的静态工厂方法:
一、静态工厂方法与构造器不同的第一大优势在于:它们有名称。
如果构造器的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读。 什么意思呢?我们看一个例子:
假设我们有一个表示图书的类,它需要标题和作者作为创建对象的参数。
当我们想创建一本新书时,我们会这样调用构造器:
这个调用很直接,但方法名Book跟类名一样是个名词,没有传达出创建动作的任何特殊含义或上下文信息。
现在我们改用静态工厂方法来创建 Book 实例,并给这个方法一个有意义的名字,比如 createFromTitleAndAuthor ,代码看起来会是这样的:
创建同样一本《Effective Java》的书,现在代码变成了:
对比两种方法的效果,静态工厂方法 createFromTitleAndAuthor 直观地表明了这个方法是用来根据书的标题和作者创建 Book 对象的,相比构造器,它提供了更多的语境信息,使得代码的意图更加清晰,而无需查看方法的具体实现或文档注释。
所以:当一个类需要多个带有相同签名的构造器时,推荐用静态工厂方法代替构造器。
并且在语法上,当你尝试定义多个构造函数,而这些构造函数仅在参数顺序上有所不同时,Java 编译器会报错,因为它无法根据参数顺序唯一确定应该调用哪个构造函数。这是不合法的Java代码。
二、静态工厂方法与构造器不同的第二大优势在于:不必在每次调用它们的时候都创建一个新对象
静态工厂方法能够像单例模式一样为重复的调用返回相同对象。
在下面的例子中,每次调用 new Car() 都会创建一个新的。如果程序中频繁创建同一型号的车对象,这可能会导致不必要的资源消耗。
现在,我们修改 Car类代码。如果程序中频繁创建同一Car类,引入一个静态工厂方法 createCar ,它可以决定是否复用已有的同类对象或根据条件创建新对象:
在这个例子中,createCar方法首先检查是否有已经存在的同型号汽车对象存储在carPool中。如果有,则直接返回该对象,避免了重复创建;如果没有,则创建新的Car 实例并保存在池中,供后续可能的复用。这种方法在处理大量相同或相似配置对象的场景下,可以显著减少对象创建的开销,提高性能。
通过上述对比,可以看到静态工厂方法提供了更多的灵活性,允许我们在创建对象时实施逻辑判 断,比如复用对象、优化资源分配等,这是直接使用构造器所不具备的优势。尤其是在需要控制 对象实例化策略,如单例模式、多例模式或池化对象的场景下,静态工厂方法展现了其独特的价 值。
三、静态工厂方法与构造器不同的第三大优势在于:它们可以返回原返回类型的任何子类型的对象
这点其实就是说的多态。这种灵活性的一种应用是API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁。
假设我们正在设计一个图书馆管理系统,其中有一个功能是管理不同类型的图书。图书可以是电 子书( EBook )、纸质书( Paperback )或其他未来可能增加的类型。为了保持API的简洁性和 未来的可扩展性,我们不希望客户端代码直接依赖于具体的图书实现类。
如果不使用静态工厂方法,客户端可能会这样直接通过构造器创建书籍对象:
这里的问题是, EBook 类必须是公共的,这暴露了系统的内部实现细节给客户端,降低了系统的封装性。
相反,如果用静态工厂方法,我们可以做到隐藏实现类,只暴露基类型(如Book类)给客户端:
现在,客户端代码通过工厂方法请求书籍,而无需了解具体的实现类:
通过上述例子,可以看到静态工厂方法如何帮助我们在API设计中实现了以下几点:
1. 隐藏实现细节:客户端不需要直接访问或依赖于具体的子类(如 EBook 或 Paperback), 这些类可以是包级私有或完全私有,从而增强了封装性。
2.简化API:API接口变得更加简洁,客户端仅需与 Book 类交互,无论底层实现如何变化。
3. 提高灵活性和扩展性:新增书籍类型(如有声书Audiobook )时,只需在工厂方法内部添加 新的分支逻辑,而无需修改客户端代码。
四、静态工厂的第四大优势在于:所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
只要是已声明的返回类型的子类型,都是允许的。返回对象的类也可能随着发行版本的不同而不同。
想象一下,我们正在开发一个日志记录框架,该框架允许用户记录日志到不同媒介(控制台、文 件、数据库等)。框架定义了一个基础接口 Logger ,以及几个实现该接口的类(如 ConsoleLogger , FileLogger , DatabaseLogger )。随着时间的推移,我们可能需要对这些日志记录方式做升级或优化,甚至引入全新的日志存储技术,比如云存储服务。
日志的接口和实现类如下:
早期版本中,我们的工厂方法可能很简单,直接根据用户的选择返回相应的日志记录器:
随着框架升级到新版本,我们可能引入了更高效的 CloudLogger 类来替代原有的 DatabaseLogger,以支持日志存储到云服务。但是,为了保持向后兼容,我们不希望老的客户端代码因为直接依赖于 DatabaseLogger 而无法工作。
此时,我们可以在工厂方法内部调整逻辑,根据版本信息或配置来决定返回哪个实现类,即使接 口调用保持不变:
在这个场景中,尽管客户端代码始终通过 LoggerFactory.getLogger("database") 请求日志记录 器,但随着框架版本的演进,工厂方法能够根据版本兼容性检查或其他逻辑,动态返回更优的实 现类(如 CloudLogger),从而实现了版本间的平滑过渡,保证了向后兼容性,同时无需客户端代码做出改变。这就是静态工厂方法在支持版本兼容与演进方面的强大之处。
五、静态工厂的第五大优势在于,方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础,例如JDBC(Java数据库连接) API。服务提供者框架是指这样一个系统:多个服务提供者实现一 个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。
JDBC是Java语言中用来与数据库交互的一组接口和类。想象一下,它就像是一个“翻译官”, 让Java程序能够和各种数据库(比如MySQL、Oracle、SQL Server等)“对话”。
不同的数据库有自己的“语言”(协议),所以为了让Java程序能和它们沟通,就需要针对每个数据库专门编写的驱动程序。但是,我们不希望每次换数据库时,都要重写所有与数据库交互的代码。这就引入了服务提供者框架的概念。
JDBC工作流程如下:
1.接口定义:JDBC定义了一套标准接口(比如Connection、Statement、ResultSet等), 这些接口就是Java程序和数据库“交流”的规则。所有的数据库驱动都必须实现这些接口, 确保它们能以一种统一的方式被Java程序调用。
2.服务提供者(驱动程序):MySQL、Oracle等数据库厂商提供符合JDBC标准的驱动程序。 这些驱动就是具体的服务提供者,它们实现了JDBC接口,并且包含了与特定数据库通信的 详细逻辑。
3.动态加载与注册:当你在代码中通过Class.forName("com.mysql.jdbc.Driver")这样的语句 时,实际上是在告诉Java:“嘿,去找到这个驱动类并加载它。”这个驱动类在加载过程中,会自动把自己注册到JDBC的DriverManager中。DriverManager就像是一个调度中心, 它知道所有可用的驱动,并能在需要时为你提供一个数据库连接。
4.无感知切换:由于你的代码是基于JDBC接口编写的,而不是直接依赖于某个特定数据库的 API,因此,如果你决定从MySQL切换到PostgreSQL,你只需要更换加载的驱动类名,而 无需修改所有与数据库交互的代码。这就是解耦带来的灵活性。
总结来说,JDBC的服务提供者框架通过标准化接口和动态加载驱动的方式,让你能够在不知道 具体数据库细节的情况下编写代码,从而达到数据库无关性的目标,提高了代码的可维护性和可 移植性。
下面分别是连接MySQL和Oracle的代码:
在这两个示例中,尽管数据库不同,但代码的结构非常相似。主要区别在于数据库URL的格式、 驱动类名以及可能的连接属性。这正是JDBC服务提供者框架的价值所在:它允许你以统一的方 式处理不同数据库的连接和操作,降低了切换数据库的成本。