最近开始补点基础知识,买了本Effective Java,希望能慢慢啃完这本书。下面是书中的一些知识及我个人的一些看法。
对于类而言,为了让客户端获取它自身的一个实例,最常用的方法就是提供一个自身公有的构造器,还有一种方法,类可以提供一个公有的静态工厂方法,它只是一个返回类的实例的静态方法。下面是一个Boolean(基本类型boolean的包装类)的简单示例,这个方法将boolean基本类型值转换成了一个Boolean的对象引用:
1 | public static Boolean valueOf(boolean b) { |
看它的源码,发现里面定义了一个不可变的常量,也是实例化了一个对象。
1 | /** |
那么通过静态工厂方法相比于通过构造器有什么好处呢?
一、静态工厂方法与构造器不同的第一大优势在于,他们有名称。
如果构造器的参数本身没有确切地描述被返回的对象,那么具有适当名称的静态工厂方法会更易于阅读。例如构造器 new BigInteger(3, 10, new Random()) 返回的可能为质数(又称素数,质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数)。如果用名为 BigInteger.probablePrime(3, new Random()) 的静态方法来表示显然更清楚。
一个类只能有一个带有指定签名的构造器。我们通常知道如何避免开这一个限制:可以在构造器的参数类型,参数长度,或者是交换位置来重载。但这不是一个好主意,用户永远也记不住该用哪个构造器,如果没有对应的参考文档,往往会调用错误的构造器。
由于静态工厂方法有名称,更加灵活。当一个类需要多个带有相同签名的构造器时,就可以用它来代替构造器。
二、静态工厂方法不必每次调用它们的时候都创建一个新对象。
这使得不可变类可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。比如上面提到的 Boolean valueOf(boolean b) 这个方法,它从来不会重新创建一个对象。这种方法类似于Flyweight模式(享元模式)。如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能。
三、静态工厂方法可以返回原返回类型的任何子类型的对象。
这样我们在选择返回对象的类时就有了更大的灵活性。这种灵活性的应用是,API可以返回对象,同时又不会使对象的类变成公有的。以这种方法隐藏实现类会使API变得非常简洁。这项技术适用于基于接口的框架,因为在这种框架中,接口为静态工厂方法提供了自然返回类型。接口不能有静态方法,因此接口Type的静态工厂方法被放在一个名为Types的不可实例化的类中。
下面举个例子说明下
首先声明一个接口:
1 | public interface Ball { |
接着创建 BasketBall 类和 FootBall 类。
1 | public class BasketBall implements Ball { |
再创建一个工厂类,利用反射机制实现。
1 | public class BallFactory { |
结果如下:
1 | Play basketball |
BallFactory就利用了基类,返回了该类型的子类型。上述的类实体其实对客户端是不可见的。
静态工厂方法的缺点
一、类如果不含有共有的或者受保护的构造器,就不能被子类化。
对于公有的静态工厂所返回的非公有类,也同样如此。例如,要想将 Collections Framework 中的任何方便的实现类子类化,这是不可能的。但是同样也许会因祸得福,因为它鼓励我们使用复合(组合),而不是继承。
例如下面的父类,它只能通过静态工厂方法来完成实例化对象,因为它有一个私有的构造方法,不允许被外部实例化,同时该类也不能被继承。
1 | public class Parent { |
当我们试图去继承该类的时候,编译器就会提示报错:
但是我们可以通过复合(组合)的方式来完成实例化:
1 | public class Child { |
二、它们与其他的静态方法实际上没有任何区别。
它们没有像构造器那样在API文档中明确标识出来,因此对于提供了静态工厂方法而不是构造器的类来说,要想查明如何实例化一个类,这是非常困难的。你通过在类或者接口注释中关注静态工厂,并遵守标准的命名习惯,可以弥补这一劣势。
总之,我挺认同的书中的说法,平时项目中也比较喜欢用静态方法,不需要重新实例化对象,一是没必要浪费资源,二是看起来更简洁有力(其实就是懒)。至于书中所说的缺点,我感觉跟优点比起来就是小巫见大巫了。如果有静态工厂方法实际经验的,认为其有不合理之处,十分欢迎您提出宝贵的意见。