为什么android API 中有很多对象的创建都是使用new关键字?
比起工厂方法、builder模式,java 中不提倡直接使用构造方法创建对象(new),为什么android API 中还是有很多对象的创建都使用构造方法 ?
这只是个草稿
首先,谢邀。
其次,是怎么找到我知乎账号的,我隐藏的这么深(脸红了)
最后,加入了自己的总结概括,让然也可以当成读书笔记来看。
我会很认真,很认真地回答问题哒,毕竟这是第一次回答专业相关的提问 : )
最近在温习《Effective Java》这本书,真的是每一次都有新的收获和认识。从第二章《创建和销毁对象》开始,就涉及了“静态工厂方法”,“构造器创建对象”等概念,篇幅不长,但实用性极强,且概括性极强,可谓句句精辟。
那么回到问题本身,其实在Java中,并不是不提倡直接使用构造函数来创建对象,而是在某些情况下,很难区分究竟调用哪个构造函数来初始化对象,或者说当函数签名类似时,一不小心就使用了错误的构造函数,从而埋下难以发现的隐患,最后付出程序崩溃的代价,等等一系列“眼一花,手一滑”所导致的后果,或多或少给人们带来“使用new关键字直接创建对象不靠谱”的错觉,其实这种结论有些片面了,为什么呢?因为所有的用例都有一个场景约束,一旦脱离适用场景,强制使用总是很牵强的。OK,让我们来再来细致的了解一下,或者说回顾一下。
考虑使用静态工厂方法代理构造函数
假设你已经知道了这里的“静态工厂”与设计模式中的“工厂模式”是两码事。
静态工厂方法可以有突出的名称
我们不能通过给类的构造函数定义特殊的名称来创建具备指定初始化功能的对象,也就是说我们必须通过参数列表来找到合适的构造函数,即便文档健全但仍很烦人,而且一旦使用了错误的构造函数,假如编译期不报错,一旦运行时奔溃,那就说明我们已经离错误发生的地方很远了,而且错误的对象已经被创建了,不过谢天谢地,它崩溃了,如果不崩溃,我们将更难找到问题所在。所以,这个时候我们就需要使用“静态工厂方法”了,因为有突出的名称,因此它很直观,易读,能够帮助我们避免这种低级错误的发生。当然,它的适用场景是存在多个构造函数,如果你只有一个构造函数,且希望被继承,则完全可以使用new来创建对象。
静态工厂方法可以使用对象池,避免对象的重复创建
反正这也应该是细节隐藏的,因此我们可以在“静态工厂方法”的背景下,在类的内部维护一个对象缓存池。这使得不可变类可以使用预先构件好的实例,或者将构建好的实例缓存起来,重复利用,从而避免创建不必要的对象。
可以像Boolean.valueOf(boolean)
那样,使用预先创建好的实例。
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
它从不创建新的对象,而且Boolean
自身的不变性,因此能够很好的使用预先创建好的实例。
或者像Parcel.obtain()
那样,在类的内部维护一个数组结构的缓存池:
private static final int POOL_SIZE = 6;
private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
/**
* Retrieve a new Parcel object from the pool.
*/
public static Parcel obtain() {
final Parcel[] pool = sOwnedPool;
synchronized (pool) {
Parcel p;
for (int i=0; i<POOL_SIZE; i++) {
p = pool[i];
if (p != null) {
pool[i] = null;
if (DEBUG_RECYCLE) {
p.mStack = new RuntimeException();
}
return p;
}
}
}
return new Parcel(0);
}
也可以像Message.obtain()
那样,使用一个链表结构的缓存池:
private static Message sPool;
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
需要注意的是,为这些对象添加一个正确的回收逻辑。
在这些场景下,我们能够轻松的控制究竟使用缓存实例,还是创建新的对象,或者设计成单例,它完全是可控的,属于“实例受控类”的范畴。相反地,如果你在设计类的时候考虑到,既不需要缓存,也不可能成为单例,那么你同样可以,以直接new的方式来创建对象。
使用静态工厂方法可以返回“原返回”类型的任何子类型
这样,我们在选择返回对象的类时就有了更大的灵活性。
这种灵活性的一种场景是,API可以返回对象,同时又不会使对象的所对应的类变成共有的。以这种方式隐藏实现类会使API变得非常简洁。如Collections.unmodifiableList(list)
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
static class UnmodifiableRandomAccessList<E> extends UnmodifiableList<E>
implements RandomAccess{
UnmodifiableRandomAccessList(List<? extends E> list) {
super(list);
}
...
}
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list;
}
...
}
就像描述中的一样,由于访问域的限制,我们“永远”无法在Collections
类的外部直接初始化UnmodifiableRandomAccessList
或UnmodifiableList
实例。
不过这也有个限制,我们只能通过接口”List”来引用被返回的对象,而不是通过它的实现类来引用,值得一提的是,通过接口或者抽象来引用被返回的对象,理应成为一种良好的习惯。
静态工厂方法在创建参数化类型实例的时候,它们使代码变得更加简洁。
在调用参数化构造器时,即使类型参数很明显,也必须指明。这通常需要连续两次提供类型参数
Map<String, List<String>> map = new HashMap<String, List<String>>();
/*使用静态工厂方法,编译器会通过“类型推导”,找到正确的类型参数*/
Map<String, List<String>> map1 = newInstance();
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
不过现在编译器或者说IDE已经足够智能,上面第一个例子完全允许写成:
Map<String, List<String>> map = new HashMap<>();
不必连续两次提供类型参数。
上面提到的大都是使用“静态工厂方法”相较于其他(创建对象方式)的优势,那么我们再来看看它有什么限制。
静态工厂方法,类如果不含共有的或者受保护的构造器,就不能子类化
因为子类需要在构造函数中隐式调用父类的无参构造函数或者显式调用有参构造函数,这和把类修饰成final
所表达的效果一致。而一旦类中存在公有构造函数,也就是说客户端可直接通过构造函数创建对象,也就弱化了静态工厂方法约束性。
静态工厂方法,它和其他静态方法实际上没有任何区别
一旦考虑使用“静态工厂方法”,就必须考虑简单,直观,完善的命名,这的确是个头疼的事 : (
遇到多个构造器参数时考虑使用构建器
其实,静态工厂方法和构造函数都有局限性:“他们都不能很好的扩展到大量的可选参数”。
在《Effective Java》举了这样一个经典的例子:
考虑用一个类表示包装食品外面显示的营养成分标签。这些标签中有几个域是必需的:每份含量,每罐的含量以及每份的卡路里,还有超过20个可选域:总脂肪量、饱和脂肪量、转化脂肪、胆固醇,钠等等。
如果这种情况下依然坚持使用构造函数或者静态工厂方法,那么要编写很多重叠构造函数,而且对于那么多的可选域而言,这些重叠函数简直就是噩梦!
避免代码难写,难看,难以阅读,有两种办法可以解决。
JavaBeans模式
使用JavaBeans模式,把必需域作为构造函数的参数,可选域则通过setter
方法注入。
我们都知道JavaBeans模式自身存在着严重的缺陷。因为构造过程可能被分到几个调用中,在构造过程中JavaBean可能处于不一致状态。类无法通过检验构造参数的有效性来保证一致性。而试图使用处于不一致状态的对象,将会导致失败,这种失败与包含错误代码大相径庭,因此调试起来十分困难。与此相关的另一点不足在于,JavaBeans模式阻止了了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。
Builder模式
幸运地是,Builder模式既能保证像重叠模式那样的安全性,也能保证JavaBeans模式那么好的可读性。而且也能够对参数进行及时的校验,一旦传入无效参数或者违反约束条件就应该立即抛出IllegalStateException
异常,而不是等着build
的调用,从而创建错误的对象。
那么我们真的需要把创建对象的方式更改为Builder吗?
答案是,否定的。
我们可以在可选域多样化的条件下,考虑使用这种模式,而且我们应该注意:不要过度设计API。
其实看完这些总结和经验,我想你心里一定有明确的答案了,那就让我们再来一句总结:
如果你的类足够简单,那么完全可以使用new来直接创建!切记过犹不及的API设计