欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

effective java 3th item1:考虑静态工厂方法代替构造器

程序员文章站 2022-07-02 13:04:28
传统的方式获取一个类的实例,是通过提供一个 构造器。这里有技巧,每一个程序员应该记住。一个类可以对外提供一个 的 静态工厂方法 ,该方法只是一个朴素的静态方法,不需要有太多复杂的逻辑,只需要返回该类的实例。 这里通过 (是原始类型 的包装类)举一个简单的例子: 这个方法,将一个 原始类型的值转换为 ......

传统的方式获取一个类的实例,是通过提供一个 public 构造器。这里有技巧,每一个程序员应该记住。一个类可以对外提供一个 public静态工厂方法 ,该方法只是一个朴素的静态方法,不需要有太多复杂的逻辑,只需要返回该类的实例。


这里通过 boolean (是原始类型 boolean 的包装类)举一个简单的例子:

    public static boolean valueof(boolean b) {
        return b ? boolean.true : boolean.false;
    }

这个方法,将一个 boolean 原始类型的值转换为 boolean 对象引用。


值得注意的是,本条目中说的一个 静态工厂方法 不同于 设计模式 的工厂模式,同样的,本条目中描述的静态工厂方法在设计模式找不到对应的模式。


一个类可以对外提供静态工厂方法,来取代 public 的构造器,或者与 public 构造器并存,对外提供两种方式获取实例。用静态工厂方法取代 public 构造器,既有优势也有缺点。

优势体现在下面几点:

  1. 静态工厂方法与构造器比起来,它可以随意命名,而非固定的与类的名字保持一致。

    如果一个构造器的参数本身,不能对将要返回的对象具有准确的描述。此时使用一个具有准确描述名字的静态工厂方法是一个不错的选择。它可以通过名字对将要返回的对象,进行准确的描述。使得使用的人可以见名知意。

    举个例子,biginteger(int, int, random) 构造器,返回的值可能是素数,因此,这里其实可以有更好的表达,通过使用一个静态工厂方法 biginteger.probableprime(int, int, random) 该方法于 1.4 被加入。

    一个类只能有一个指定方法签名的构造器。通常我们都知道如何绕过这限制,通过交换参数列表的顺序,得到不同的方法签名。但是这是一个很糟糕的主意。这给使用 api 的开发人员造成负担,他们将很难记住哪一个方法签名对应哪一个对象的返回,最后往往都是错误的调用。阅读代码的人同样也蒙圈,如果没有相应的文档告诉他们,不同的方法签名对应的构造器返回的对象是什么。

    静态工厂方法不受上述限制,不需要去通过交换参数顺序来彼此区分,因为它们可以拥有自己的名字。因此,当一个类的多个构造器,方法签名差不多,仅仅参数顺序不一样的时候,考虑使用静态工厂方法,仔细的为静态工厂方法取名字,以区分它们之间的不同。

  2. 静态工厂方法与构造器比起来,不必每次调用都创建新的对象

    这允许不可变的类使用预创建的实例,或者在创建实例的时候,将实例缓存起来,重复的使用该实例,避免创建不重要的重复对象。boolean.valueof(boolean) 方法使用该技巧,它永远都不会创建对象,返回的都是预创建好的对象。这个技巧有点类似于设计模式中的享元模式 。它能大幅度的提高性能,特别是在特定场景下:一些对象创建的时候,需要花费很大性能,并且这些对象经常被使用。

    该特性允许类在任何时候,对其产生多少实例具有精确的控制。用这种技巧的类,被称为实例受控的类。这里有几个使用实例受控类的理由。实例受控允许一个类保证它是一个单例或者不可实例化的类。同样的,实例受控,也可以保证不可变类不会存在两个相等的实例。

  3. 静态工厂方法与构造器比起来,可以返回该类的任意子类型的对象

    具有足够的灵活性,在获取对象的时候。可以返回协变类型,在方法中使用该类的子类构造器创建对象,然后返回,同时对外不需要暴露这些子类对象,适合于面向接口编程。在 1.8 之前,接口中不能有静态方法,针对情况的惯例做法是,针对名为 type 类型的接口,它的静态方法被放在一个不可实例化的类 types 中,典型的例子是 java.util.collections 类,它通过静态工厂方法,可以构建返回各式各样的集合:同步集合、不可修改的集合等等,但是返回的时候都是返回接口类型,具体实现类型不对外公开。

    // collections 中非公开类,同步map
       private static class synchronizedmap<k,v>
            implements map<k,v>, serializable {
            private static final long serialversionuid = 1978198479659022715l;
    
            private final map<k,v> m;     // backing map
            final object      mutex;        // object on which to synchronize
    
            synchronizedmap(map<k,v> m) {
                this.m = objects.requirenonnull(m);
                mutex = this;
            }
    
            synchronizedmap(map<k,v> m, object mutex) {
                this.m = m;
                this.mutex = mutex;
            }
    
        // collections 的静态工厂方法,返回接口接口map,但是内部是返回同步map类型。
       public static <k,v> map<k,v> synchronizedmap(map<k,v> m) {
            return new synchronizedmap<>(m);
        }

    起到简化 api 的作用,这种简化不仅仅是 api 体积上的减少,减少对外暴露的类的数量,也给程序员直观上的简洁,他们只需要记住接口类型即可,无需记住具体的实现类型。这样也督促程序员面向接口编程,这是一种好的习惯。

    1.8 以后,接口可以写静态方法。因此,不再需要按照以前的习惯,为接口,写一个对应的类,直接在接口中写静态工厂方法。但是关于返回的实现类型,依然应该继续隐藏,使用非公开类实现。

  4. 静态工厂方法与构造器比起来,可以随着传入参数的不同,返回不同的对象。

    可以返回任何子类型,和第三条一样,但是可以继续添加控制,根据传入参数的不同,返回不同的对象。

    一个例子,enumset ,是一个不可实例的类,只有一个静态工厂方法,没有构造器。但是在 openjdk 的实现中,具体的返回类型,是根据实际枚举的个数决定的,如果小于等于 64,则返回 regularenumset 类型,否则返回 jumboenumset 类型。这两个类型对于使用者来说,都是不可见的。如果 regularenumset 类型,在将来不再为小的枚举类型提供优势,即便在未来的发行版删除 regularenumset 也不会有什么影响。同样的,未来也可以继续添加第三个、第四个版本,对使用者也是无感的。使用者不需要关心具体返回的是什么对象,他们只知道,返回的对象都是 enumset 类型。

    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");
    
            // 如果小于等于 64 ,则返回 regularenumset
            if (universe.length <= 64)
                return new regularenumset<>(elementtype, universe);
            else
                return new jumboenumset<>(elementtype, universe);
        }
    
        // regularenumset 类是 enumset的子类
        class regularenumset<e extends enum<e>> extends enumset<e> {
    
            ...
        }
    
    
  5. 静态工厂方法与构造器比起来,在编写静态工厂方法的时候,具体的类型可以不存在。

    这句话还是面向接口的优势,意思就是,我们在编写方法的时候,可以没有任何实现类,在使用的使用,先注册实现类,然后再返回实现类,这使得扩展变得很容易。我们只是在维护一个框架,一个接口,具体的实现,我们不给出,谁都可以实现,然后注册使用。这也是 服务者框架 的含义。jdbc 就是这么一个思想的服务者框架。

缺点:

  1. 静态工厂方法与构造器比起来,没有 public 或者 protected 修饰的构造器,无法实现继承

    例如,上面提到的 collections ,就无法被继承,我们就不能继承其中的任何一个便利的实现。这或许,也是一种对使用组合而非继承的鼓励。

  2. 静态工厂方法与构造器比起来,它们不容易被程序员所知晓

    在文档中,它们不像构造器那么显眼,在上面单独的列出来,基于这个原因,要想知道如何实例化一个类,使用静态工厂方法比使用构造器相比,前者是比较困难的,因为文档中,静态工厂方法和其他静态方法没啥区别,没有做特殊处理。java 文档工具或许在未来会注意到这个问题,对静态工厂方法多给予一些关注。

    同时,我们可以在文档中对静态工厂方法的名字做一些特殊处理,遵守常见的命名规范,来减少这个问题,比如像下面提到的几个规范:

    1. from类型转换方法,根据一个单一传入的参数,返回一个对应的类型,比如:date d = date.from(instant);
    2. of聚合方法,接受多个参数,返回一个合并它们的实例。比如:set<rank> facecards = enumset.of(jack, queen, king);
    3. valueof比 from 和 of 更加详细 。比如:biginteger prime = biginteger.valueof(integer.max_value);
    4. instance or getinstance获取实例方法,根据传入的参数,返回实例,但是不保证返回的实例完全一样,根据传入的参数不同而不同。比如:stackwalker luke = stackwalker.getinstance(options);
    5. create or newinstance获取新的实例,每次都返回新创建的实例。比如:object newarray = array.newinstance(classobject, arraylen);
    6. gettype ,和 getinstance 类似,用于返回的类型,不是静态工厂方法所在的类,而是其他类型。比如:filestore fs = files.getfilestore(path);
    7. newtypenewinstance 类型,同样用于返回的类型,不是静态工厂方法所在的类,而是其他类型。比如:bufferedreader br = files.newbufferedreader(path);
    8. typegettype and newtype 的简化版。比如:list<complaint> litany = collections.list(legacylitany);

总结下,静态工厂方法和 public 构造器都有自己的优点,了解它们各自的优点是有帮助的。大部分情况下,静态工厂方法更占优势,所以,我们应该避免第一反应就使用构造器,而是先考虑下静态工厂方法。



  1. 这里的静态工厂方法,和设计模式中的静态工厂模式,很相似,但是设计模式中的静态工厂模式,它是对外隐藏对象的实现细节,通过一个工厂,根据不同的输入,产生不同的输出。本条目中的工厂,只会产生特定的输出,即自己的实例。二者还是不同的。

  2. 方法签名,指的是方法名字,以及方法参数列表,包括方法参数的顺序。

  3. 类似于单例模式的饿汉式

  4. 类似于单例模式的懒汉式

  5. 享元模式,23种设计模式中的一种, 它针对每一种内部状态仅提供一个对象,设置不同的外部状态,产生多种不同的形态,但是其内部状态对象只有一个,达到对象复用的目的。

  6. 这里的 tyle 类型,不是泛型的 type ,只是一种代指,跟 xxx 一个意思,表示 1.7 以前,对于面向接口编程的时候,想要返回协变类型,常规的做法,是写一个不可实现类 ,类的名字,就是接口的名字,多加一个 s