当前位置: 首页 > news >正文

品读 Java 经典巨著《Effective Java》90条编程法则,第1条:用静态工厂方法代替构造器

《Effective Java》是 Java 开发领域的一本重要著作,对于 Java 开发人员来说尤为宝贵。作者 Joshua Bloch 在书中深入探讨了 Java 编程中的最佳实践,并提供了大量的示例和建议,旨在帮助开发人员写出更高效、可维护的代码。书中的内容包括了 Java 语言的核心特性和设计模式,涉及到的主题如创建和销毁对象、类和接口的设计、泛型、枚举、并发等。

如果你想要在 Java 开发中遇到难题或想要提高编程技巧,那么这本书绝对值得一读。书中每个条目都以简洁明了的方式解释了如何避免常见错误并提高代码质量。如果你是 Java 开发人员,阅读《Effective Java》将对你的职业发展大有裨益。

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

创建一个类的实例最传统的方式是让类提供一个公共构造器,并使用 new 关键字来创建对象。例如:

public final class Boolean {private final boolean value;// 公有构造器public Boolean(boolean value) {this.value = value;}
}// 使用构造器创建实例
Boolean b = new Boolean(true);

如果不通过公有构造器,或者说除了公有构造器之外,类还可以通过向外提供一个公有的静态工厂方法(static factory method)用于替代公有构造器。

例如 Java 的 Boolean 类中提供了一个静态工厂方法 valueOf 用于创建一个 Boolean 对象:

public static Boolean valueOf(boolean b) {return (b ? TRUE : FALSE);
}Boolean b = Boolean.valueOf(true);

静态工厂方法的五大优势

第一大优势:静态工厂方法可以有描述性的名称,使得创建实例的意图更加明确

在 Java 中,一个类禁止有两个及以上相同签名的构造器(即构造器名、参数类型和顺序完全相同),然而我们可以通过重载的方式定义参数顺序不同的构造器来绕过这个限制,但这种做法容易让调用人员混淆,如果参数类型和顺序都非常接近,调用者可能会因为记不清楚顺序而误用构造器,增加出错的风险。同时,这种设计也使得代码的理解和维护变得更为复杂。

public class Person {private String name;private int age;// 构造器 1public Person(String name, int age) {this.name = name;this.age = age;}// 构造器 2,参数顺序不同public Person(int age, String name) {this.name = name;this.age = age;}
}

静态工厂方法相比于构造器,静态工厂方法允许有描述性的名称,可以更明确地表明对象的创建方式,使得创建实例的意图更加明确。并且可以避免构造器参数的顺序问题。通过为不同的构造需求提供不同的静态方法,可以提高代码的可读性。例如:

public class User {private final String name;private final int age;private final String email;private User(String name, int age, String email) {this.name = name;this.age = age;this.email = email;}// 静态工厂方法,创建一个仅包含姓名和年龄的 User 实例,邮箱默认为 nullpublic static User createWithNameAndAge(String name, int age) {return new User(name, age, null);}// 静态工厂方法,创建一个仅包含姓名和邮箱的 User 实例,年龄默认为 0public static User createWithNameEmail(String name, String email) {return new User(name, 0, email);}
}

第二大优势:不必在每次调用它们的时候都创建一个新对象

在设计不可变类时,可以预先构建好实例,并将构建好的实例缓存起来,在需要时通过静态工厂方法重复利用这些实例,这样避免了每次请求时都创建新的对象,从而减少了内存使用和对象创建的开销。

例如 Boolean 类的静态工厂方法 valueOf(boolean b) 返回 Boolean.TRUEBoolean.FALSE 的实例,而不是每次都创建一个新的 Boolean 对象:

public final class Boolean {// 静态常量 TRUE,代表布尔值 true 的唯一实例public static final Boolean TRUE = new Boolean(true);// 静态常量 FALSE,代表布尔值 false 的唯一实例public static final Boolean FALSE = new Boolean(false);// 静态工厂方法,返回布尔值的唯一实例public static Boolean valueOf(boolean b) {return (b ? TRUE : FALSE);}}

第三大优势:可以返回原返回类型的任何子类型的对象

静态工厂方法提供了比构造器更大的灵活性,因为它能够返回接口类型的任意子类实例,而不仅仅是特定的类实例。这种灵活性特别适用于基于接口的框架(interface-based framework),允许 API 隐藏具体实现类,从而使 API 保持简洁。

用户调用静态工厂方法时只需要关注接口,而不用了解或依赖具体的实现,减少了对实现细节的依赖。这种设计也便于在未来对实现细节进行修改或扩展,而不会影响到依赖这个API的调用方的代码。

// 工厂类,用于创建 Shape 对象
public class ShapeFactory {// 静态工厂方法,根据输入的类型返回相应的 Shape 实例public static Shape createShape(String type) {if ("circle".equalsIgnoreCase(type)) {return new Circle();} else if ("rectangle".equalsIgnoreCase(type)) {return new Rectangle();}throw new IllegalArgumentException("Unknown shape type");}
}// Shape 接口,定义所有形状必须实现的公共方法
public interface Shape {void draw();
}// Circle 类,实现 Shape 接口,表示一个圆形
public class Circle implements Shape {@Overridepublic void draw() {System.out.println("Drawing a circle");}
}// Rectangle 类,实现 Shape 接口,表示一个矩形
public class Rectangle implements Shape {@Overridepublic void draw() {System.out.println("Drawing a rectangle");}
}// 测试 ShapeFactory 功能
public class Main {public static void main(String[] args) {// 使用工厂方法创建一个圆形对象Shape shape = ShapeFactory.createShape("circle");shape.draw();}
}

第四大优势:所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值

静态工厂方法能够根据每次调用时传入的不同参数返回不同的具体实现。例如,通过传递不同的参数,静态工厂方法可以返回不同的子类实例:

public class NumberFactory {public static Number getNumber(int type) {switch (type) {case 1:return new Integer(1);case 2:return new Double(2.0);default:return null;}}
}

注意:第三大优势关注的是静态工厂方法返回的对象类型可以是返回类型的任何子类型,而不是固定类型。第四大优势则关注静态工厂方法根据不同的参数值返回不同的具体实现,即对象的类可以根据参数值发生变化。

第五大优势:方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在

静态工厂方法的主要优势在于它允许我们在编写包含静态工厂方法的类时,不需要关心或直接知道具体的实现类,具体实现类可以在运行时动态决定。这使得系统能在运行时灵活地加载和使用不同的实现类,而不是在编译时就锁定特定的实现。

这个优势特别适用于需要支持多个实现且实现类可能动态变化的系统,尤其是服务提供者框架(Service Provider Framework),在这个框架中,系统在运行时决定和加载实际的实现类,而不需要在编译时确定。JDBC 的 DriverManager 就是服务提供者框架的一个经典示例。

在 JDBC 中,驱动程序是通过 java.sql.Driver 接口实现的,不同的数据库供应商提供不同的实现。而 DriverManager 类负责管理数据库驱动的加载和连接。在使用时通过 DriverManager 的静态工厂方法 DriverManager.getConnection 根据传入的 URL、用户名和密码,选择合适的驱动程序来建立连接,驱动的具体实现类会在运行时动态加载的。

public class JdbcExample {public static void main(String[] args) {try {// 动态加载 MySQL 驱动程序Class.forName("com.mysql.cj.jdbc.Driver");// 使用 DriverManager 获取数据库连接String url = "jdbc:mysql://localhost:3306/mydatabase";String username = "root";String password = "password";Connection connection = DriverManager.getConnection(url, username, password);System.out.println("Connection established successfully!");// 关闭连接connection.close();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (SQLException e) {e.printStackTrace();}}
}

注意:静态工厂方法的第五个优势,涉及到设计和开发规范,旨在提供一个统一的 API 设计。

两个缺点

缺点一:子类如果不含公有的或者受保护的构造器,就不能被子类化

如果一个类只通过静态工厂方法提供实例,那么通常该类的构造器会被设置为私有的,因此这也将使得这个类无法被子类化。

image-20240910223945256

缺点二:程序员很难发现它们

由于静态工厂方法是通过类名直接调用的静态方法,而不是通过创建类的新实例,因此它们可能在文档和 API 浏览工具中不如构造器那样明显。

如果静态工厂方法没有在类的文档中充分说明,或者缺乏清晰的示例和说明,程序员可能很难找到这些方法并理解它们的用途,特别是当这些静态工厂方法具有类似的名称或参数签名时,代码的阅读和维护可能变得更加复杂。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • java:mybatisplus查询功能演示,包括模糊查询
  • 降维打击 华为赢麻了
  • 15.2 JDBC数据库编程2
  • 【H2O2|全栈】关于HTML(2)HTML基础(一)
  • 线程(Thread)
  • 从“N 号房”看Deepfake乱象,如何证明“我”不是我?
  • C++之打造my vector篇
  • 【H2O2|全栈】关于HTML(1)认识HTML
  • Java通过jna调用c++动态库
  • 基于 AT 固件测试 ESP32 设备作为 WiFi AP 模式创建 TCP Server 开启 UART-to-WiFi 透传模式的指令序列
  • TCP通信实现
  • C++里定义和声明的区别
  • Java数组的定义及遍历
  • 常见分组加密算法的整体结构
  • 第六章 SqlSession 执行 Mapper 过程
  • Bootstrap JS插件Alert源码分析
  • electron原来这么简单----打包你的react、VUE桌面应用程序
  • ES6之路之模块详解
  • javascript从右向左截取指定位数字符的3种方法
  • Javascript弹出层-初探
  • Linux CTF 逆向入门
  • Odoo domain写法及运用
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • TypeScript实现数据结构(一)栈,队列,链表
  • ubuntu 下nginx安装 并支持https协议
  • yii2中session跨域名的问题
  • 从伪并行的 Python 多线程说起
  • 反思总结然后整装待发
  • 理解在java “”i=i++;”所发生的事情
  • 力扣(LeetCode)357
  • 什么是Javascript函数节流?
  • 小程序 setData 学问多
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • 组复制官方翻译九、Group Replication Technical Details
  • ​ 全球云科技基础设施:亚马逊云科技的海外服务器网络如何演进
  • # centos7下FFmpeg环境部署记录
  • #调用传感器数据_Flink使用函数之监控传感器温度上升提醒
  • #中的引用型是什么意识_Java中四种引用有什么区别以及应用场景
  • $.ajax中的eval及dataType
  • (1)STL算法之遍历容器
  • (14)目标检测_SSD训练代码基于pytorch搭建代码
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (附源码)springboot课程在线考试系统 毕业设计 655127
  • (机器学习-深度学习快速入门)第三章机器学习-第二节:机器学习模型之线性回归
  • (七)Knockout 创建自定义绑定
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (四)Tiki-taka算法(TTA)求解无人机三维路径规划研究(MATLAB)
  • (原)记一次CentOS7 磁盘空间大小异常的解决过程
  • *++p:p先自+,然后*p,最终为3 ++*p:先*p,即arr[0]=1,然后再++,最终为2 *p++:值为arr[0],即1,该语句执行完毕后,p指向arr[1]
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .describe() python_Python-Win32com-Excel
  • .net core 控制台应用程序读取配置文件app.config
  • .NET Framework、.NET Core 、 .NET 5、.NET 6和.NET 7 和.NET8 简介及区别
  • .NET Micro Framework初体验(二)
  • .NET/C# 避免调试器不小心提前计算本应延迟计算的值