美文网首页
Effective Java: 考虑用静态工厂方法替代构造器

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

作者: mlya | 来源:发表于2020-05-08 11:19 被阅读0次

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

    这里更准确的说, 是替代 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 的应用模式.

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/tylfhftx.html