选择语言
durumis AI 总结的文章
- 靜態工廠方法是一種替換建構函式來建立類別實例的方法,具有指定實例名稱、快取建立成本高的物件以提高效能,以及根據需要返回不同子類別的物件等優點。
- 特別是,在 Java 8 之前,無法在介面中宣告靜態方法,因此需要建立伴隨類別來定義靜態工廠方法。然而,從 Java 8 開始,可以在介面中直接新增靜態方法,因此不再需要單獨定義伴隨類別。
- 使用靜態工廠方法可以促進組合而非繼承,並且為了建立不可變型別,必須遵守此限制,這反而可能成為優點。但是,與建構函式不同,靜態工廠方法在 API 說明中並不顯眼,因此開發人員需要撰寫良好的 API 文件,並遵循廣為人知的慣例命名方法,以減輕此問題。
概述
获得类的实例的传统方法是使用公共构造函数。
public class Member {
private String name;
private int age;
private String hobby;
private MemberStatus memberStatus;
public Member(String name, int age, String hobby, MemberStatus memberStatus) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.memberStatus = memberStatus;
}
}
public enum MemberStatus {
ADVANCED,
INTERMEDIATE,
BASIC;
通常,仅使用公共构造函数就足够了,但除了构造函数外,还提供静态工厂方法 (static factory method) 在某些情况下,可以让用户更容易地根据需要创建实例。
静态工厂方法的典型示例是 Boolean 的 valueOf() 方法。
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
上述方法接收基本类型 boolean 值,并将其转换为 Boolean 对象后返回。
静态工厂方法的优点
可以有名称
传递给构造函数的参数以及构造函数本身无法完全描述返回对象的特性。例如,在上面的 Member 类中,仅通过主构造函数 (name, age, hobby, memberStatus) 很难确定 Member 的特性。
此外,一个签名只能创建一个构造函数,而静态工厂方法可以有名称,因此一个签名可以创建多个静态工厂方法来返回实例。
public class Member {
private String name;
private int age;
private String hobby;
private MemberStatus memberStatus;
public Member(String name, int age, String hobby, MemberStatus memberStatus) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.memberStatus = memberStatus;
}
public static Member basicMember(String name, int age, String hobby) {
return new Member(name, age, hobby, MemberStatus.BASIC);
}
public static Member intermediateMember(String name, int age, String hobby) {
return new Member(name, age, hobby, MemberStatus.INTERMEDIATE);
}
public static Member advancedMember(String name, int age, String hobby) {
return new Member(name, age, hobby, MemberStatus.ADVANCED);
}
与使用构造函数区分 MemberStatus 相比,创建具有相同签名的多个静态工厂方法,可以让用户在无需混淆的情况下创建具有特定技能的 Member 实例。
在 JDK 定义的库中,我们可以看到 BigInteger 的静态工厂方法 probablePrime()。
public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
return (bitLength < SMALL_PRIME_THRESHOLD ?
smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
比较 BigInteger 的普通构造函数和静态工厂方法 probablePrime(),后者更能描述返回的 BigInteger 的特性,即返回值为素数。
调用时不必每次都创建新实例
public static Boolean valueOf(boolean b) {
return (b ? Boolean.TRUE : Boolean.FALSE);
我们可以看到,Boolean 的 valueOf() 方法会预先缓存实例,然后将其返回。这种特性在需要频繁请求创建成本较高的对象的场景中可以显著提高性能,享元模式也属于类似的技术。
使用静态工厂方法以相同的方式返回相同对象的类被称为实例控制类。控制实例可以创建单例类或不可实例化类。此外,在不可变值类中,可以确保只有一个实例。
实例控制是享元模式的基础,枚举类型保证只有一个实例被创建。
示例
在 Minecraft 中,需要种植树木。如果每个树木对象都需要重新创建,则可能会导致内存溢出。
因此,可以将红色树木和绿色树木对象存储起来,然后只更改位置并返回。当然,颜色除了这两种颜色之外还可以更多,因此将树木存储在 Map 等数据结构中以供有效访问是有效的。
public class Tree {
// 树木具有以下三个信息。
private String color;
private int x;
private int y;
// 只通过颜色创建构造函数。
public Tree(String color) {
this.color = color;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
// 种植树木时
public void install(){
System.out.println("x:"+x+" y:"+y+" 位置上有 "+color+" 色树木!");
}
}
public class TreeFactory {
// 使用 HashMap 数据结构来管理创建的树木。
public static final Map treeMap = new HashMap<>();
public static Tree getTree(String treeColor){
// 在 Map 中查找输入的颜色是否有树木,如果有,则提供该对象。
Tree tree = (Tree)treeMap.get(treeColor);
// 如果 Map 中还没有相同颜色的树木,则创建一个新对象并提供。
if(tree == null){
tree = new Tree(treeColor);
treeMap.put(treeColor, tree);
System.out.println("创建新的对象");
}
return tree;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入想要的颜色 :)");
for(int i=0;i<10;i++){
// 输入树木颜色
String input = scanner.nextLine();
// 从工厂获取一颗树
Tree tree = (Tree)TreeFactory.getTree(input);
// 设置树木的 x、y 位置
tree.setX((int) (Math.random()*100));
tree.setY((int) (Math.random()*100));
// 种植树木
tree.install();
}
}
与单例模式的区别
单例模式只能创建一个树类。因此,如果使用单例模式,则需要更改创建的唯一对象的颜色。也就是说,单例模式只允许存在一个对象,而不考虑类型。
使用案例
Java 的 String 常量池使用享元模式。
可以返回返回类型子类型的对象
如果您使用过 Arrays 实用程序类的 asList() 方法,那么您就会理解这个优点。
public static List asList(T... a) [
return new ArrayList<>(a);
asList() 方法使用 ArrayList 将值包装并返回,用户无需了解此实现细节。也就是说,可以灵活地选择返回对象的类,开发人员无需公开实现,因此可以保持 API 的简洁性。
关于 Java 接口的静态方法
在 Java 8 之前,接口无法声明静态方法,因此如果需要返回名为“Type”的接口的静态方法,则需要创建一个不可实例化的伴随类“Types”来定义该方法。
最典型的例子是 JCF 提供的 45 个实用程序实现,其中大多数实现都是通过静态工厂方法从单个伴随类 java.util.Collections 中获取的。尤其是这些实现中,有些实现不是公共的,只能通过静态工厂方法创建实例(当然无法继承这些实现)。
此外,由于没有公开这 45 个实现,因此 API 的规模也大大缩小。
// 接口和伴随类的示例
但是,从 Java 8 开始,可以在接口中直接添加静态方法,因此不再需要单独定义伴随类。
可以根据输入参数返回不同类的对象
不仅仅是返回子类型,还可以根据参数的值返回不同的子类型。例如,如果想根据分数返回不同的 MemberStatus,就可以像下面这样创建静态工厂方法,并在其中设置比较逻辑。
public enum MemberStatus {
ADVANCED(80, 100),
INTERMEDIATE(50, 79),
BASIC(0, 49);
private final int minScore;
private final int maxScore;
MemberStatus(int minScore, int maxScore) {
this.minScore = minScore;
this.maxScore = maxScore;
}
public static MemberStatus of(int score) {
return Arrays.stream(values())
.filter(decideMemberStatus(score))
.findAny()
.orElseThrow(() -> new NoSuchElementException("没有找到相应的 MemberStatus 对象。"));
}
private static Predicate decideMemberStatus(int score) {
return element -> element.minScore <= score && element.maxScore >= score;
}
}
@DisplayName("MemberStatus 测试")
class MemberStatusTest {
@ParameterizedTest
@CsvSource(value = {"0:BASIC", "30:BASIC", "50:INTERMEDIATE", "70:INTERMEDIATE", "80:ADVANCED", "100:ADVANCED"}, delimiter = ':')
@DisplayName("根据分数返回不同的 MemberStatus。")
void of(int input, MemberStatus expected) {
assertThat(MemberStatus.of(input)).isEqualTo(expected);
}
编写静态工厂方法时,不必存在要返回的对象的类
上面的句子中,对象的类指的是我们编写的类文件。
另外,Class> 代表类加载器在加载类时分配到堆区域的 Class 对象。此 Class 对象包含我们编写的类的各种元数据。
package algorithm.dataStructure;
public abstract class StaticFactoryMethodType {
public abstract void getName();
public static StaticFactoryMethodType getNewInstance() {
StaticFactoryMethodType temp = null;
try {
Class> childClass = Class.forName("algorithm.dataStructure.StaticFactoryMethodTypeChild"); // 反射
temp = (StaticFactoryMethodType) childClass.newInstance(); // 反射
} catch (ClassNotFoundException e) {
System.out.println("类不存在。");
} catch (InstantiationException e) {
System.out.println("无法加载到内存中。");
} catch (IllegalAccessException e) {
System.out.println("类文件访问错误。");
}
return temp;
}
从上面的代码中,我们可以看到,通过接口实现的位置来创建 Class 对象,并使用反射技术来初始化实际的实现。此时,编写静态工厂方法的时刻StaticFactoryMethodTypeChild 类不必存在。
如果在使用静态工厂方法时,algorithm.dataStructure.StaticFactoryMethodTypeChild 路径中不存在实现,则会发生错误,但在编写静态工厂方法时不会出现问题,因此具有灵活性。
public interface Test {
int sum(int a, int b);
// Test 是一个接口,即使没有实现,在编写静态工厂方法时也不会出现问题。
static Test create() {
return null;
}
}
public class Main {
public static void main(String[] args) {
Test test = Test.create();
System.out.println(test.sum(1, 2)); // 发生 NPE
}
即使不使用反射,也可以获得相同的灵活性。在 Test 的静态工厂方法 create() 中,即使没有实现,在编写时也不会出现问题。当然,在实际使用时会发生 NPE,因此需要稍后返回实现。
这种灵活性是创建服务提供者框架的基础,JDBC 就是一个典型的例子。JDBC 服务提供者框架的提供者是服务的实现,框架负责将这些实现提供给客户端,将客户端与实现分离(DIP)。
- 服务提供者框架的组件
- 服务接口
- 定义实现的行为
- JDBC 的 Connection
- 提供者注册 API
- 提供者注册实现
- JDBC 的 DriverManager.registerDriver()
- 服务访问 API
- 当客户端获取服务的实例时使用,如果未指定条件,则返回默认实现或支持的实现,轮流返回。
- 对应于静态工厂方法
- JDBC 的 DriverManager.getConnection()
- (可选)服务提供者接口
- 如果没有,则在创建每个实现的实例时需要使用反射。
- JDBC 的 Driver
- 服务接口
服务提供者框架模式有多种变体,例如桥接模式、依赖注入框架等。
典型 JDBC 示例
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection connection = null;
connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORA92", "root", "root");
通常,JDBC 的编写方式如上所示。通过 Class.forName() 注册 Driver 实现之一 OracleDriver,然后通过 DriverManager.getConnection() 获取 OracleDriver 的 Connection 实现之一。
这里,Connection 是服务接口,DriverManager.getConnection() 是服务访问 API,Driver 是服务提供者接口。但是,提供者注册 API DriverManager.registerDriver() 没有使用。尽管如此,我们仍然可以使用 Class.forName() 注册 Driver 实现 OracleDriver。这是如何实现的?
Class.forName() 的工作原理
该方法将物理类文件名称作为参数传入,请求 JVM 加载该类。然后,类加载器将类的元数据存储到方法区域,并分配 Class 对象到堆区域。另外,当类加载完成后,静态字段和静态块将被初始化,此时将使用提供者注册 API。
public class OracleDriver implements Driver {
static {
defaultDriver = null;
Timestamp timestamp = Timestamp.valueOf("2000-01-01 00:00:00.0");
try {
if (defaultDriver == null) {
defaultDriver = new OracleDriver();
DriverManager.registerDriver(defaultDriver); // 注册 OracleDriver
}
} catch (RuntimeException runtimeexception) {
} catch (SQLException sqlexception) {
}
}
...
实际上,在 OracleDriver 中,我们可以看到在静态块中使用 DriverManager.registerDriver() 注册 Driver 实现 OracleDriver。
DriverManager 类分析
public class DriverManager {
private DriverManager() {
}
private static final Map drivers = new ConcurrentHashMap();
public static final String DEFAULT_DRIVER_NAME = "default";
public static void registerDefaultPrivider(Driver d) {
System.out.println("注册 Driver");
registerDriver(DEFAULT_DRIVER_NAME, d);
}
public static void registerDriver(String name, Driver d) {
drivers.put(name, d);
}
public static Connection getConnection() {
return getConnection(DEFAULT_DRIVER_NAME);
}
public static Connection getConnection(String name) {
Driver d = drivers.get(name);
if (d == null) throw new IllegalArgumentException();
return d.getConnection();
}
DriverManager 类实际上要复杂得多,但仅从核心方面来看,它与上面类似。如上所述,通过在 OracleDriver 的静态块中调用 registerDriver() 注册 OracleDriver,用户可以调用 getConnection() 获取 Connection 的实现。
仔细观察用户访问 API getConnetion(),我们可以看到它是从 Driver 接口中获取 Connection 的。如果没有服务提供者接口 Driver,则需要使用 Class.forName() 等反射来返回想要的 Connection 实现。此时,Connection 实现不必在编写静态工厂方法时存在。
相反,我们使用 Driver 接口,动态地注册 Driver 的实现,然后可以轻松地获取与该 Driver 对应的 Connection 实现。
另外,我对 DriverManager 的 getConnection() 方法的实际 JDK 代码进行了分析,如果您不太感兴趣,可以跳过。
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
首先调用公共静态方法 getConnection(),并将 url、Properties 和 CallerClass 作为参数传递给私有静态方法 getConnection()。此时,Reflection.getCallerClass() 负责获取调用该公共静态方法 getConnection() 的类的 Class 对象。如果 Car 类调用 getConnection(),则通过 Reflection.getCallerClass() 可以获得 Class 对象。
private static Connection getConnection(String url, java.util.Properties info, Class> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
}
if (reason != null) {
throw reason;
}
throw new SQLException("No suitable driver found for "+ url, "08001");
callerCL 是类加载器对象,由调用者或当前线程的类加载器创建。然后,从当前应用程序中注册的 Driver 列表 registeredDrivers 中逐个提取 aDriver。如果该 Driver 由 isDriverAllowed() 判定为 true,则使用该 Driver 获取 Connection 对象并返回。isDriverAllowed() 负责检查调用者中是否存在 aDriver。
JDBC 框架的优点
JDBC 框架的关键在于 Driver、Connection 接口及其实际实现的类完全分离提供。通过使用接口创建框架,然后根据框架创建各自的实现类,这具有非常灵活的优点。
因此,即使出现了其他 DBMS,该供应商也可以通过实现 Driver 和 Connection 接口来提供这些接口,从而使使用 Java 的开发人员能够使用与其他 DBMS 驱动程序相同的 API。
静态工厂方法的缺点
继承时需要公共或受保护的构造函数,如果只提供静态工厂方法,则无法创建子类
但是,这种限制更倾向于引导组合而不是继承,并且为了创建不可变类型,需要遵守这种限制,因此它反而可以成为优点。
静态工厂方法难以找到
由于它不像构造函数那样明确地显示在 API 说明中,因此开发人员需要编写良好的 API 文档,并使用广泛认可的约定为方法命名来缓解此问题。
静态工厂方法的命名方式
- from
- 接收一个参数并返回该类型的实例
- Date date = Date.from(instant);
- of
- 接收多个参数并返回适合类型的实例
- Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueOf
- from 和 of 的更详细版本
- BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
- instance 或 getInstance
- 返回指定的实例,但不保证是相同的实例。
- StackWalker luke = StackWalker.getInstance(options);
- create 或 newInstance
- 与 instance 或 getInstance 相同,但保证每次都创建并返回新实例。
- Object newArray = Array.newInstance(classObject, arraylen);
- getType
- 与 getInstance 相同,但在定义工厂方法的类不是要创建的类时使用。
- FileStore fs = Files.getFileStore(path);
- newType
- 与 newInstance 相同,但在定义工厂方法的类不是要创建的类时使用。
- BufferedReader br = Files.newBufferedReader(path);
- type
- getType 和 newType 的简化版本
- List<Complaint> litany = Collections.list(legacyLitany);
总结
静态工厂方法和公共构造函数各有用途,请根据需要使用。
来源
- Effective Java
- https://catsbi.oopy.io/d7f3a636-b613-453b-91c7-655d71fda2b1
- https://velog.io/@hoit_98/디자인-패턴-Flyweight-패턴
- https://velog.io/@shinmj1207/Effective-Java-객체-생성과-파괴1
- https://a1010100z.tistory.com/entry/아이템-1-생성자-대신-정적-팩터리-메서드를-고려하라
- https://plposer.tistory.com/61
- https://honbabzone.com/java/effective-java-static-factory-method/#장점-5--정적-팩터리-메서드를-작성하는-시점에서-반환할-객체의-클래스가-존재하지-않아도-된다
- https://ktaes.tistory.com/2