Effective Java 1 用静态工厂方法代替构造器

news2024/10/7 10:19:44

知识点上本书需要会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服务提供者框架的价值所在:它允许你以统一的方 式处理不同数据库的连接和操作,降低了切换数据库的成本。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1806105.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

rust学习(字节数组转string)

最新在写数据传输相关的操作,发现string一个有趣的现象,代码如下: fn main() {let mut data:[u8;32] [0;32];data[0] a as u8;let my_str1 String::from_utf8_lossy(&data);let my_str my_str1.trim();println!("my_str len is…

基于JSP技术的社区生活超市管理系统

你好呀,我是计算机学长猫哥!如果有相关需求,文末可以找到我的联系方式。 开发语言:Java 数据库:MySQL 技术:JSP技术 工具:MyEclipse开发环境、Tomcat服务器 系统展示 首页 管理员功能模块…

两款 IntelliJ IDEA 的 AI 编程插件

介绍两款 IntelliJ IDEA 的 AI 编程插件:通义灵码和 CodeGeeX。 通义灵码 这是由阿里推出的一个基于通义大模型的 AI 编码助手。 它提供了代码智能生成、研发智能问答等功能。通义灵码经过海量优秀开源代码数据训练,可以根据当前代码文件及跨文件的上下…

Spring运维之boo项目表现层测试匹配响应执行状态响应体JSON和响应头

匹配响应执行状态 我们创建了测试环境 而且发送了虚拟的请求 我们接下来要进行验证 验证请求和预期值是否匹配 MVC结果匹配器 匹配上了 匹配失败 package com.example.demo;import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Auto…

Golang | Leetcode Golang题解之第129题求根节点到叶节点数字之和

题目: 题解: type pair struct {node *TreeNodenum int }func sumNumbers(root *TreeNode) (sum int) {if root nil {return}queue : []pair{{root, root.Val}}for len(queue) > 0 {p : queue[0]queue queue[1:]left, right, num : p.node.Left, …

C++使用thread_local实现每个线程下的单例

对于一个类,想要在每个线程种有且只有一个实例对象,且线程之间不共享该实例,可以按照单例模式的写法,同时使用C11提供的thread_local关键字实现。 在单例模式的基础上,使用thread_local关键字修饰单例的instance&…

离散数学答疑 4

知识点:什么是可结合? 举例A选项: 知识点:可交换性? 知识点:什么是阿贝尔群? 可交换->运算表中的元素关于主对角线对称 二阶子群的表达式 二阶子群作为一个群的子群,其本质是一个包含单位元…

【深度学习】温故而知新4-手写体识别-多层感知机+CNN网络-完整代码-可运行

多层感知机版本 import torch import torch.nn as nn import numpy as np import torch.utils from torch.utils.data import DataLoader, Dataset import torchvision from torchvision import transforms import matplotlib.pyplot as plt import matplotlib import os # 前…

SpringBoot+Vue学科竞赛系统(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 角色对应功能 学生教师管理员 功能截图

IO流字符流(FileReader与FileWriter)

目录 FileReader 空参read方法 带参read方法👇 FileWriter void write(intc) 写出一个字符 void write(string str) 写出一个字符串 void write(string str,int off,int len) 写出一个字符串的一部分 void write(char[] cbuf) …

如何学习Golang语言!

第一部分:Go语言概述 起源与设计哲学:Go语言由Robert Griesemer、Rob Pike和Ken Thompson三位Google工程师设计,旨在解决现代编程中的一些常见问题,如编译速度、运行效率和并发编程。主要特点:Go语言的语法简单、编译…

Bootstrap框架集成ECharts教程

最新公司项目要在原有的基础上增加一些饼状图和柱状图来统计一些数据给客户,下面就是集成的一个过程,还是很简单的,分为以下几步 1、引入ECharts的包 2、通过ECharts官网或者菜鸟教程直接拿示例代码过来修修改改直接用就可以了 注意&#xf…

三维地图Cesium,加载一个模型,模型沿着给定的一组经纬度路线移动

目录 实现效果 实现思路 功能点 选择移动路线 加载模型和移动路线 重新运行 指定位置(经纬度点)开始移动 视角切换 到站提示 运行 停止 联动接口 完整代码 html js逻辑 trainOperation.js sourceData.js gitee仓库项目代码 疑问解答 实现效果 三维地图Cesiu…

pyqt QlineEdit内部增加按钮方法

按钮放在QlineEdit内部,界面更紧凑,体现了按钮和文本框的强关联。 def addButton(self,lineEdit):btn QtWidgets.QPushButton("")icon1 QtGui.QIcon()icon1.addPixmap(QtGui.QPixmap(":/image/images/th.png"), QtGui.QIcon.Norm…

C++【STL】改造红黑树简单模拟实现set map(带你了解set map的底层实现结构)

目录 一、学前铺垫(泛型编程) 二、改造红黑树 1.红黑树节点的改造 2.insert的改造 3.迭代器的实现 4.完整改造代码 三、set的模拟实现封装 四、map的模拟实现封装 五、完结撒❀ 前言: 下面为了简单模拟实现set map所出现的代码是以…

【JsDoc】JsDoc用法 | 巧妙用法

type type {other} other 接收表达式或字符 1、数组代码提示 1、效果图 1、码 /*** type {Array.<play|paush|next>} */ let music []2、字符串提示 2、效果图 2、码 /*** type {a|b|c}*/ let str

minio的一个基础使用案例:用户头像上传

文章目录 一、minio下载安装&#xff08;Windows&#xff09;二、案例需求分析三、后端接口开发 一、minio下载安装&#xff08;Windows&#xff09; 1. 下载minio服务端和客户端 minio下载地址 2. 手动搭建目录 /minio/binmc.exeminio.exe/data/logs手动创建minio应用程序目…

Linux入门学习(2)

1.相关复习新的指令学习 &#xff08;1&#xff09;我们需要自己创建一个用户&#xff0c;这个用户前期可以是一个root用户&#xff0c;后期使用创建的普通用户 &#xff08;2&#xff09;文件等于文件内容加上文件属性,对于文件的操作就包括对于文件内容的操作和文件属性&…

像素坐标系与图像坐标系

前言 在数字图像处理中&#xff0c;经常会看到使用 (x, y) 表示图像中的某个像素点。 在一些图像处理库&#xff0c;例如 Pillow 、OpenCV 、Numpy 中也会使用到坐标系处理图像的像素点。 介绍 无论是像素坐标系还是图像坐标系&#xff0c;其原理都是一样的&#xff1a; 以…

Freetype 介绍和使用

目录 一、矢量字体引入 二、Freetype 介绍 1.给定一个字符&#xff0c;怎么在字体文件中找到它的关键点&#xff1f; 2.文字显示过程 3.如何使用 freetype 库 三、在 LCD 上显示一个矢量字体 1.使用 wchar_t 获得字符的 UNICODE 值 2.使用 freetype 得到位图 3.在屏幕上…