Effective Java: 考虑用静态工厂方法替代构造器

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

这里更准确的说, 是替代 public 的构造器. 这里的静态工厂方法指的是类中的一个静态方法, 返回该类的一个实例 (instance). 例如 Java 的 Boolean 包装类就提供了如下的静态工厂方法:

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

书中为我们概括了, 使用静态工厂方法有如下优点:

  1. 静态工厂方法与构造器 (constructors) 相比, 是可以具有名称的.
  2. 与构造起相比, 静态工厂方法并不要求每次调用的时候都重新创建一个新的对象, 这样可以避免创建很多不必要的对象.
  3. 第三点优势是, 静态工厂方法不仅可以创建当前类的对象, 而且可以返回返回类型的任何子类对象.
  4. 第四点优势是, 静态工厂方法返回的类型可以根据输入参数的不同而不同.
  5. 第五点优势在于, 静态工厂方法返回的对象, 在编写静态方法时, 其对应的类可以不存在.

静态工厂方法与构造器 (constructors) 相比, 是可以具有名称的

我们在创建类的时候, 有时候需要不止一种方式来产生对象, 一种方式是对构造器的重载, 但是对构造器方法的重载, 只能通过不同的参数来实现. 有时候, 我们使用同样的参数, 也想要使用不同的方式来构造对象, 这对于使用构造器来说是很难实现的.

使用静态工厂方法, 我们可以使用不同的命名的方式, 来使用不同的方式来构建对象.

例如, Boolean 类, 创建 Boolean 对象的方法有如下几种:

  • Boolean(boolean)
  • Boolean(String)
  • valueOf(boolean)
  • valueOf(String)

前两个是 Boolean 的公有构造器 (public constructor), 都接收一个参数, 第一个接收 boolean 类型, 第二个接收 String 类型. 都可以分别将对应的值转换为 Boolean 对象.

但是这里使用构造器意义不明确, 这两个方法其实对应着下面的两个 valueOf() 方法. 这两种用法其实是一致的, 但是, valueOf 的语意更加明确一些, valueOf 从语意上来说, 就是一种类型转换, 代表着将 boolean 类型或者 String 类型转换成 Boolean 的对象.

下面是一个项目中实际使用, 更加具有实际意义的例子. 在构建网络接口, 确定网络接口的返回值的时候, 我们通常需要进行一定的封装. 例如, 如果将所有的返回类型都封装在一个叫做 ResponseModel<M> 的泛型类中, 加入包含以下基本信息.

public class ResponseModel<M> {
    // 返回代码
    private int code;
    // 描述信息
    private String message;
    // 创建时间
    private LocalDateTime time = LocalDateTime.now();
    // 具体的内容
    private M result;
}

如果我们写一个服务程序, 那么这个类将是我们与客户端进行沟通的一个非常常用的类, 我们经常需要创建不同的 ResponseModel 来返回给客户端. 因此我们最好提供不同的方法来能够快速的使用不同的方法来创建不同的 ResponseModel. 例如, 请求成功的 Response (200), 包含不同类型的错误信息的 Response 等等. 如果使用构造器来实现, 是很难实现的, 我们需要非常谨慎的构建不同的重载来实现, 并且在调用时也是非常混乱的, 因为在这种情况下每一个构造方法的参数的不同并不能提供非常明确的语意以表示我们要创建的对象, 这可能会导致很大程度上的混乱, 使用上也非常不便.

如果使用静态工厂方法, 我们就可以通过给不同的方法进行命名, 来提供非常明确的语意信息来快速的构建所需的对象. 例如:

public static <M> ResponseModel<M> buildOk() {
    return new ResponseModel<M>();
}

public static <M> ResponseModel<M> buildOk(M result) {
    return new ResponseModel<M>(result);
}

public static <M> ResponseModel<M> buildParameterError() {
    return new ResponseModel<M>(ERROR_PARAMETERS, "Parameters Error.");
}

上面就分别列举了几种不同的静态工厂方法, 通过方法的名称就可以非常明确的知道我们所构建的对象的含义, 真正意义上的提供了快捷方法.

静态工厂方法并不要求每次调用的时候都重新创建一个新的对象, 这样可以避免创建很多不必要的对象.

这种机制对于很多值类来说, 是很常用的, 一个非常典型的例子就是 Java 中的那些装箱类.

在 Java 中共有 8 种 primitive 类型, 这八种基本数据类型对应 8 种自动装箱类:

  • char -> Character
  • boolean -> Boolean
  • byte -> Byte
  • short -> Short
  • int -> Integer
  • long -> Long
  • float -> Float
  • double -> Double

Boolean 类

Boolean 类是一个比较简单的类, Boolean 的可行值实际上只有两个, True 和 False. 因此, 理论上, 在运行过程中, Boolean 类最多只需要创建两个对象即可. 在 Boolean 类内部也是这样实现的, Boolean 内部包含两个静态成员:

/**
 * The {@code Boolean} object corresponding to the primitive
 * value {@code true}.
 */
public static final Boolean TRUE = new Boolean(true);

/**
 * The {@code Boolean} object corresponding to the primitive
 * value {@code false}.
 */
public static final Boolean FALSE = new Boolean(false);

在我们使用 Boolean 对象时, 应该始终使用的是这两个对象, 避免创建额外的变量, 这样能够方便我们使用.

Boolean 的 public 构造方法在新版本的 JDK 中已经被标记为 @Deprecated.

@Deprecated(since="9")
public Boolean(boolean value) {
    this.value = value;
}

@Deprecated(since="9")
public Boolean(String s) {
    this(parseBoolean(s));
}

在使用 Boolean 时, 我们应该使用其静态工厂方法 valueOf() 来创建 Boolean 对象, 或者直接使用静态成员 TRUEFALSE.

关于自动装箱和自动拆箱, 我查到有资料说是会自动调用对应的 valueOf() 方法.

只要不在外部调用 Boolean 的构造方法 (我们也不应该调用), 程序在运行过程中就只存在两个静态对象.

Integer 类

Integer 相对较复杂一些, 但是该类在设计时, 同样拥有静态工厂方法, 来代替构造器. Integer 的构造器同样也被标记为 @Deprecated, 我们同样不应该使用.

@Deprecated(since="9")
public Integer(int value) {
    this.value = value;
}

@Deprecated(since="9")
public Integer(String s) throws NumberFormatException {
    this.value = parseInt(s, 10);
}

同样的, 用于替代上面两个构造方法的静态工厂方法是 valueOf(String s, int radix), valueOf(String s) 以及 valueOf(int i). 前两个构造函数是从 String 转换成 Integer 对象, radix 表示进制.

Integer 与 Boolean 相比的额外的机制是缓存机制, Boolean 对象只有两个取值, 因此直接使用两个静态成员变量即可.

Integer 使用了额外的缓存机制, Integer 中有一个静态成员类 IntegerCache, 这是一个单例类, 使用静态代码块进行了初始化.

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer[] cache;
    static Integer[] archivedCache;

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                h = Math.max(parseInt(integerCacheHighPropValue), 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        // Load IntegerCache.archivedCache from archive, if possible
        VM.initializeFromArchive(IntegerCache.class);
        int size = (high - low) + 1;

        // Use the archived cache if it exists and is large enough
        if (archivedCache == null || size > archivedCache.length) {
            Integer[] c = new Integer[size];
            int j = low;
            for(int i = 0; i < c.length; i++) {
                c[i] = new Integer(j++);
            }
            archivedCache = c;
        }
        cache = archivedCache;
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}
  1. IntegerCache 的缓存范围默认是 -128 ~ 127.
  2. 在实现过程中, 缓存的下界只允许默认值, 而上界允许通过设置虚拟机参数的方式进行修改.
    1. String integerCacheHighPropValue = VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    2. 可以通过虚拟机参数 -XX:AutoBoxCacheMax=<size> 来设置缓存的上界. 在 JVM 初始化时, 会将该值缓存到 jdk.internal.misc.VM 中的 java.lang.Integer.IntegerCache.high 属性中.
    3. 关于为什么只允许修改上界而不允许修改下界, 我查到 Why does the JVM allow to set the “high” value for the IntegerCache, but not the “low”? 这个问题. 他表示这是个问题, 但是没有需求去修改下界.
    4. RFP 官方的说法.
    5. 这个参数的调整只在 Integer 中存在, 在 Long 中并没有任何可调整的缓存参数, 都是设置的固定值.
  3. 在实现过程中, 还有一点需要注意的是, 缓存使用了 CDS 机制 (Class Data Sharing).

由于有上述缓存机制, 我们进行如下测试:

@SuppressWarnings({"NumberEquality"})
public static void testInteger() {
    System.out.println("\n=====Some test for Integer=====\n");
    Integer a = 1000, b = 1000;
    System.out.println("a = " + a + ", b = " + b);
    // Warning: Only for test, don't use "==" to compare two boxed object
    System.out.println("a == b: " + (a == b));

    Integer c = 100, d = 100;
    System.out.println("c = " + c + ", d = " + d);
    // Warning: Only for test, don't use "==" to compare two boxed object
    System.out.println("a == b: " + (c == d));
}

对于上述代码的如下输出结果就比较容易理解了:

=====Some test for Integer=====

a = 1000, b = 1000
a == b: false
c = 100, d = 100
a == b: true

Char, Byte, Long 和 Short

Char, Byte, Long 和 Short 的实现机制和 Integer 几乎一致, 提供了一致的静态工厂方法, 同时也使用了缓存机制, 这里就不再赘述了.

Double, Float

Double 和 Float 也提供了静态工厂方法 valueOf(), 但是并没有提供缓存机制, 因为小数并不适合进行缓存.

静态工厂方法不仅可以创建当前类的对象, 而且可以返回返回类型的任何子类对象.

这一特性的一个应用是在不暴露子类的具体实现的情况下, 返回一个子类对象. 例如 Java Collections Framework. 在一个叫做 java.util.Collections 的伴生类中, 实现了不可修改集合 (UnmodifiableSet), 同步集合 (SynchronizedSet), 空集合 (EmptySet) 等等这些工具集合.

这些集合的实现, 都是非公有的, 如果想要获取这些类的对象, 就可以调用 Collections 中对应的静态工厂方法, 并使用接口去引用这些对象.

静态工厂方法返回的类型可以根据输入参数的不同而不同.

EnumSet 是一个抽象类, 其没有提供公有构造方法, 其提供了一系列的静态工厂方法来创建 EnumSet, 包括 noneOf(), allOf(), of(), range(). 这一系列静态工厂方法最终调用的都是 noneOf() 方法.

noneOf() 方法传入的参数是一个枚举类的类型信息, 其源码实现如下.

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    // 获取所有枚举的数组
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
        throw new ClassCastException(elementType + " not an enum");

    // 根据枚举元素的个数, 确定具体的 EnumSet 实现方式
    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else
        return new JumboEnumSet<>(elementType, universe);
}

可以看出, 其最终返回的是一个 RegularEnumSet 对象或者 JumboEnumSet 对象. 这两个类都是 EnumSet 的具体实现. 是根据枚举元素的具体个数, 从而确定 EnumSet 的具体实现.

RegularEnumSet 内部使用单个 long 类型进行支持:

/**
 * Bit vector representation of this set.  The 2^k bit indicates the
 * presence of universe[k] in this set.
 */
private long elements = 0L;

当元素个数小于等于 64 个时, 使用 RegularEnumSet 就足够了, 因为一个 long 类型的数据时 64 位.

当元素个数大于 64 时, 使用 JumboEnumSet 实现, 其内部使用一个 long 数组进行存储.

/**
 * Bit vector representation of this set.  The ith bit of the jth
 * element of this array represents the  presence of universe[64*j +i]
 * in this set.
 */
private long elements[];

静态工厂方法返回的对象, 在编写静态方法时, 其对应的类可以不存在.

其典型应用时 JDBC 的应用模式.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335