深入理解《Effective Java》 之 条目1:用静态工厂方法替代构造器
前言:《Effective Java》在Java界被称为三本必读书目之一,对深入理解Java开发,写出更优雅的代码,大有裨益。然而,外国人的语言表达方式,本就别扭,再加上书中概念多,例子少且深,初学者理解起来就非常吃力。为了更好更深入理解这本书的开发思想和方法,本人打算做个系列文章,分享下学习心得,用代码示例去解读深奥的理论,供各位小伙伴们作个学习参考。需要说明,如果未加说明,本系列文章引用块部分均出自原文。
条目1:用静态工厂方法替代构造器
一、什么是静态工厂方法?
静态工厂方法是一种常用于替代构造器的模式,它允许我们通过一个类的静态方法来创建并返回类的实例,而不是直接使用new关键字。
一般情况下,我们都是通过关键字new创建类的实例。这样,每new一次都会创建一个新的对象,方便、灵活。比如,声明一个字符串。
String say=new String("hello world!");
但是创建字符串,还可以用String.ofValue()方法进行,这个方法就是静态工厂方法。
String say=String.ofValue("hello world!"); //这个方法有很多重载方法
我们在创建Calendar实例时,也可以使用静态工厂方法。
Calendar cld=Calendar.getInstance();
二、静态工厂方法的优点
2.1 静态工厂方法有名称,容易使用且代码可读性更好【关键字:易记易用】
如果构造器的参数本身不能清晰地描述所返回对象的信息,相比之下,如果使用具有优雅名称的静态工厂方法,那用起来就会更容易,程序的可读性也会更好。例如:构造器BigInteger(int,int,Random)返回一个可能素数(probable prime)的BigInteger,但是如果用一个名为BigInteger.probalbePrime的静态方法来表示,效果会更好。
BigInteger prime1=BigInteger(4,4,new Random());
BigInteger prime2=BigInteger.probablePrime(4, new Random());//显然这个更直观
一个类只能有一个带有特定签名的构造器。
在Java中,构造器即构造函数是方法的一种,也遵循重载的原则。方法的重载是由签名决定的,同一签名只能定义一个方法,签名不同方法不同。签名=方法名+参数(Type-Position-Count)。即使名称相同,但是参数类型不同,或不同类型参数顺序不同,或参数个数不同,就是一个不同的签名。比如:User(String name,int age)与User(int age,String name)就是两个不同签名的构造器。
但是,不要认为User(String name,String sex)和User(String sex,String name)是两个不同签名,签名与参数名称无关,这是一个方法名、参数类型、参数类型顺序、参数个数均相同的签名,是同一签名,当然,这总编译也通不过。
那么,这里的“特定”如何理解呢?首先,构造器的签名应不能重复;其次,不应声明功能相同、无实意义的构造器。
有的程序员会通过改变参数列表中的参数类型的顺序,来提供两个不同的构造器,从而绕开这一限制。此举不可取。
有的程序员在定义Measurement类是这样做的:
public class Measurement {
private double value;
private String unit;
// 构造器1: 值, 单位
public Measurement(double value, String unit) {
this.value = value;
this.unit = unit;
}
// 构造器2: 单位, 值
public Measurement(String unit, double value) {
this.value = value;
this.unit = unit;
}
}
这两个构造器都能编译通过,问题是当调用时,特别是参数是变量时,容易混淆。
double val = 10.5;
String u = "meter";
// 这两个调用都有效,但哪个是正确的?
Measurement m3 = new Measurement(val, u);
Measurement m4 = new Measurement(u, val);
不可取的原因:
- 虽然编译通过,但两个构造器功能完全相同
- API不清晰,调用者不知道应该用哪个构造器
- 违反了"一个类只有一个特定签名构造器"的设计原则
如果类需要多个带有相同签名的构造器,就可以用精心选择了不同签名的多个静态工厂方法来代替,从而让区别看更加明显。
上述定义Measurement类可以这样修改:
public class Measurement {
private double value;
private String unit;
// 私有构造器,防止外部直接创建
private Measurement(double value, String unit) {
this.value = value;
this.unit = unit;
}
// 静态工厂方法1: 值, 单位
public static Measurement withValueAndUnit(double value, String unit) {
return new Measurement(value, unit);
}
// 静态工厂方法2: 单位, 值
public static Measurement withUnitAndValue(String unit, double value) {
return new Measurement(value, unit);
}
}
这样做的优点:API含义清晰,具有更好的可读性,且还可以添加更多静态工厂方法而不破环现有的API。
2.2 与构造器不同,不用在每次被调用时候创建一个新的对象【关键字:实例复用】
要理解这个优点,就必须搞清楚实例受控和享元模式。
首先,什么是实例受控?
如果静态方法工厂不用返回一个新的创建新对象,那么对象从哪里来?
- 使用预先构建好的实例
- 再构建实例时将其缓存下来并反复分发
这种对于重复多次的调用,静态工厂方法都可以返回同一个对象的类,被称为实例受控的类。实例受控(instance-controlled)是静态工厂方法核心优点。所谓实例受控,就是一种软件设计模式,指一个类通过私有化构造函数和提供静态工厂方法等方式,主动控制其实例的创建、数量、生命周期和可访问性**,而不是允许外部代码随意创建新实例。
适用于频繁创建对象,特别是当创建对象的开销非常大的情况。
实例受控主要实现方式;
(1)不可实例化类
主要指无状态的工具类,如Math、Collections等。我们看下Math构造器定义:
private Math() {}
Math类没有提供任何外部实例化方法,所以,根本无法在程序中进行创建。
(2)单例模式
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {} // 私有构造函数
public static Singleton getInstance() {
return INSTANCE; // 始终返回同一个实例
}
}
(3)实例缓存池
public class ConnectionPool {
private static final Map<String, Connection> CACHE = new HashMap<>();
private ConnectionPool() {}
public static Connection getConnection(String url) {
return CACHE.computeIfAbsent(url, k -> createConnection(k));
}
}
(4)不可变值类
public class Color {
private static final Map<Integer, Color> CACHE = new HashMap<>();
private final int rgb;
private Color(int rgb) { this.rgb = rgb; }
public static Color of(int rgb) {
return CACHE.computeIfAbsent(rgb, k -> new Color(rgb));
}
}
(5)枚举(天然实例控制)
public enum Day {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");
private final String chineseName;
private Day(String chineseName) {
this.chineseName = chineseName;
}
public String getChineseName() {
return chineseName;
}
}
其次,什么是享元模式?
可以实现一个不可变的值类,保证不存在两个相同实例。所谓相同,就是当且仅当a==b时,
a.equals(b)才为true。这是享元模式的基础。
第一、二句的意思:
翻译成人话,就是我们创建的一个不可变类,不允许存在两个不同实例但内容相同的情况,如果内容相同,那就是同一个实例。 可以从三个层次理解这句话:
一是,在Java中,== 运算符比较的是两个对象的引用是否指向同一个对象,即是否在内存中是同一个实例。而equals()方法默认比较的也是引用,但可以被重写以比较对象的内容。
二是,如果我们不重写equals方法,那么默认的equals方法就是使用== 比较,这样每个新创建的实例都是唯一的,不会出现两个不同实例但equals为true的情况。但是,如果我们重写了equals方法以比较内容,那么就有可能存在两个不同的实例但内容相同,此时equals返回true,但== 返回false。
三是,为了保证不存在两个相同实例(即内容相同的实例),我们需要确保对于内容相同的对象,始终返回同一个实例。
例: 创建一个不可变类MyColor:
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public final class MyColor implements Comparable<MyColor>{
// 不可变字段
private final int red;
private final int green;
private final int blue;
// 缓存
private static final Map<CacheKey,MyColor> CACHE=new ConcurrentHashMap<>();
// 私有构造函数,确保只能通过工厂方法创建
private MyColor(int red,int green,int blue) {
this.red=red;
this.green=green;
this.blue=blue;
}
// 工厂方法获取实例
public static MyColor of(int red,int green,int blue) {
CacheKey key=new CacheKey(red,green,blue);
return CACHE.computeIfAbsent(key, k-> new MyColor(red,green,blue));
}
// 重写equals和hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MyColor)) return false;
MyColor that = (MyColor) o;
return red == that.red && green == that.green && blue == that.blue;
}
@Override
public int hashCode() {
int result = red;
result = 31 * result + green;
result = 31 * result + blue;
return result;
}
// 实现Comparable接口
@Override
public int compareTo(MyColor that) {
int cmp=Integer.compare(this.red, that.red);
if(cmp!=0) return cmp;
cmp=Integer.compare(this.green, that.green);
if(cmp!=0) return cmp;
return Integer.compare(this.blue, that.blue);
}
// 缓存键类
private static final class CacheKey{
private final int red;
private final int green;
private final int blue;
private final int hashCode;
CacheKey(int red,int green,int blue){
this.red=red;
this.green=green;
this.blue=blue;
this.hashCode=Objects.hash(red,green,blue);
}
@Override
public boolean equals(Object o) {
if(this==o) return true;
if(o==null) return false;
if(o instanceof CacheKey) {
CacheKey other=(CacheKey)o;
return red==other.red&&green==other.green&&blue==other.green;
}
return false;
}
@Override
public int hashCode() {
return hashCode;
}
}
// 测试
public static void main(String[] args) {
// 情况1:创建相同实例(从缓存返回)
MyColor red1=MyColor.of(255, 0, 0);
MyColor red2=MyColor.of(255, 0, 0);
System.out.println("red1 == red2: " + (red1 == red2)); // true
System.out.println("red1.equals(red2): " + red1.equals(red2)); // true
// 情况2:创建不同实例(内容不同)
MyColor green = MyColor.of(0, 255, 0);
System.out.println("red1 == green: " + (red1 == green)); // false
System.out.println("red1.equals(green): " + red1.equals(green)); // false
}
}
MyColor通过实例复用,保证了内容相同时,返回的实例是同一个内存地址引用;内容不同时,返回的就是两个完全不同的实例。
第三句话的意思:
通过静态工厂方法,实现实例共享,是享元模式的基本条件。所谓享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享对象来有效支持大量细粒度对象的复用,从而减少内存消耗。核心思想是将对象状态分为内部状态(可共享)和外部状态(由客户端传入)。
例: 绘制20个圆形但只创建4个对象
// 享元接口
interface Shape {
void draw(int x, int y, String backgroundColor);
}
// 具体享元类 - 圆形
class Circle implements Shape {
private final String color; // 内部状态(可共享)
private int radius = 10; // 内部状态
public Circle(String color) {
this.color = color;
}
@Override
public void draw(int x, int y, String backgroundColor) {
// 外部状态由参数传入
System.out.printf("绘制%s圆形: 位置(%d,%d) 背景色%s 半径%d\n",
color, x, y, backgroundColor, radius);
}
}
// 享元工厂
class ShapeFactory {
private static final Map<String, Shape> circleMap = new HashMap<>();
public static Shape getCircle(String color) {
// 如果颜色存在则返回现有对象,否则创建新对象
return circleMap.computeIfAbsent(color, Circle::new);
}
public static int getObjectCount() {
return circleMap.size();
}
}
// 使用示例
public class FlyweightPatternDemo {
private static final String[] colors = {"红", "绿", "蓝", "黄"};
public static void main(String[] args) {
// 绘制20个圆形,但只创建4个对象
for (int i = 0; i < 20; i++) {
String color = colors[(int)(Math.random() * colors.length)];
Shape circle = ShapeFactory.getCircle(color);
circle.draw(
(int)(Math.random() * 100), // 外部状态x
(int)(Math.random() * 100), // 外部状态y
"白色" // 外部状态背景色
);
}
System.out.println("实际创建对象数量: " + ShapeFactory.getObjectCount());
}
}
2.3 与构造器不同,可以返回所声明的返回类型的任何子类型的对象【关键字:多态封装】
这样在选择返回对象的类时,就有很大的灵活性。
用接口为静态工厂方法提供统一的返回类型,隐藏类的具体实现。比如,我们在设计缓存管理时,就可以根据系统条件返回不同的具体实现类。
class CacheManager {
// 根据系统条件返回不同的缓存实现
public static Cache getInstance() {
if (System.getProperty("cache.type").equals("redis")) {
return new RedisCache(); // Redis实现
} else if (memoryIsSufficient()) {
return new MemoryCache(); // 内存缓存
} else {
return new DiskCache(); // 磁盘缓存
}
}
}
这种灵活性有一种应用是,不用将类设计为公有的,就可以让API返回这个类的对象。
class Service{
// 静态工厂可以返回一个包级私有的实现类
public static Service getInstance(){
return new ServiceHelper(); // ServiceHelper 只能包内可见的,非公开
}
private static class ServiceHelper extends Service{
// 这个类对用户是隐藏的
}
}
在Java8之前,接口中不能包含静态方法。按照惯例,接口Type的静态工厂方法会被放到一个名为Types的、不可以实例化的伴生类中。
这里的伴生类可以理解为特定的工具类。按照惯例,接口Type一般会有个工具类Types为其服务,而静态工厂方法就放到这个工具类中。
你可以将Type与Types理解为汽车与汽车工厂的关系,汽车工厂有轿车、SUV等生产线,但这些生产线都是汽车工厂的供应商,工厂会只要求这些供应商建造生产各种汽车的产线,而不会关心这些产线具体如何建造。
例如,Java集合框架(Java Collections Framework)为其中的接口提供了45个工具类实现,几乎所有这些实现都是通过一个不可实例化类(java.util.Collections)中的静态工厂方法导出的,并且所有返回对象的类都是非公有的。为什么不将这45类工具类都涉及为公有的?
因为设计公有的,集合类框架就会 大 出很多。这个“大”的真正含义:
- 认知负担大:用户需要学习45个类而不是45个方法
- API表面积大:每个公有类都是一个API入口点
- 维护成本大:每个公有类都需要保持向后兼容
- 依赖关系大:用户代码会直接依赖具体实现类
- 演化难度大:难以修改实现而不破坏用户代码
2.4 在方法每次调用时,所有返回对象的类可以随输入参数的不同而改变【关键字:版本演进】
这里有两次含义:一是所声明的返回类型的任何子类型都是被允许的,这在上一优点介绍了;二是在Java不同版本中,所返回对象的类型也可以不同,新旧版本使用不同实现类,但你却无感。
// 第一版
public static MyService getInstance() {
return new MyServiceImplV1();
}
// 第二版:无缝升级,使用者无感知
public static MyService getInstance() {
return new MyServiceImplV2(); // 更好的实现
}
2.5. 在编写包含该方法的类时,所有返回对象的类并不一定要存在【关键字:服务接口】
Java开发中的服务提供者框架(Service Provider Framework),通常也称为服务提供者接口(Service Provider Interface, SPI)机制,是Java提供的一种服务发现机制。它的核心思想是定义标准接口,允许第三方提供者提供具体实现,从而实现服务接口与具体实现的解耦。这样,系统可以在不修改核心代码的情况下,通过配置动态加载和替换功能模块,体现了“对扩展开放,对修改封闭”的开闭原则。
一个典型的服务提供者框架包含以下四个关键部分:
- 服务接口(Service Interface):定义服务的抽象契约,供客户端调用。
- 服务提供者接口(Service Provider Interface, SPI):服务提供者需要实现的接口,用于创建服务实例。(此组件有时是可选的)
- 提供者注册API(Provider Registration API):用于系统注册服务提供者,使得客户端可以访问其实现的服务。
- 服务访问API(Service Access API):客户端用来获取具体服务实例的API。
SPI与API的本质区别:前者是别人实现你的约定,后者是你调别人的功能。SPI在Java生态中被广泛应用,是许多框架实现可扩展性的基础,比如:JDBC驱动加载、日志门面等。
// JDBC DriverManager 就是通过静态工厂返回不同的驱动实现
Connection conn = DriverManager.getConnection(url, user, password);
// 实际返回的是 MySQL、Oracle 或 PostgreSQL 的具体实现
// 但对调用者来说,都是同一个 Connection 接口
三、缺点
3.1 如何没有公有的或受保护的构造器,就无法为这样的类创建子类
因为不向外部提供构造函数,你是无法new一个对象的,当然就使用继承,因为子类构建必须在父类构建完毕后进行,而父类却都没提供构造器。这样类是不可实例化的类,工具类就是一个典型例子,也因此采用组合设计方式,才有了基础。
3.2 程序员很难找到它们
官方都静态工厂方法未作统一规定,只是开发中遵循通的命名惯例,这使得其不如构造器那么好识别 ,目前,你要做的就是需要不断积累这方面的开发经验。
更多推荐


所有评论(0)