静态工厂方法代替构造器
静态工厂方法的几大优势
静态工厂方法有名称,易于区分。
避免不必要的重复对象的创建工作
不必每次调用都创建一个新的对象,可以将构建好的实例缓存起来重复使用
- 可以返回原返回类型的任何子类型
返回对象有了更大的灵活性
返回的对象的类可以随着每次调用而发生变化,取决于静态工厂方法的参数值
方法返回对象所属的类,在编写包含静态工厂方法的类时可以不存在
静态工厂方法的几个缺点
- 类如果不含公有的或者受保护的构造器,就不能被子类化
- 不易发现
遇到多个参数时要使用构建器
当构造器扩展到大量的可选参数时,静态工厂和构造器就显得相当局限了。目前用的比较多的是重叠构造器
1 | NutritionFacts cocaCola = new NutritionFacts(240,8,100,0,35,27); |
缺点
- 可读性比较差
- 容易出错,如参数位置弄错位置
使用setter方案
1 | public class NutritionFacts { |
- 可读性增强
- 一致性的问题(不安全)
构建器
1 | public class NutritionFacts { |
- 直观、易于阅读(优点)
- 在注重性能的情况下,创建构建起是个问题(缺点)
- 最好一开始就用构建器(建议)
小结
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式是个不错的选择
私有构造器或者枚举类型强化Singleton属性
Singleton:仅仅被实例化一次的类
Singleton的两种实现
单例模式的实现(一)
1 | public class Elvis { |
特点
- 私有构造器只调用一次,用来实例化公有的静态final域(instance)
- 没有public或者protect的构造器,保证了instance的唯一性
- 但是可以通过反射的方式调用私有构造器(可以修改构造器在创建第二次实例时抛出异常)
单例模式的实现(二)
1 | public class Elvis { |
特点
- 添加了一个静态工厂方法,静态工厂方法的调用每次都返回同一个对象引用
- 公有的静态域是final的,该域总是包含相同的对象引用。公有域方法在性能上不会有任何优势,现在JVM实现能够将静态工厂方法的调用内联化。(内联:函数被调用的地方直接展开,编译器在调用时不会像一般函数那样,参数压栈,返回时参数出栈以及释放资源,直接提高程序的执行速度)
Singleton可序列化问题
Singleton仅仅在声明中加上implement Serializable是不够的,必须所有的实例都是瞬时的,并提供一个readResolve方法,否则每次反序列化都会创建一个新的实例
单例模式的实现(三)— 通过枚举类型实现
1 | public enum Elvis { |
特点
- 简洁
- 无偿提供了序列化机制,防止多次实例化
- 单元素的枚举类型是实现Singleton的最佳方法
使用依赖注入来引用资源
静态工具类引用底层资源1
2
3
4
5
6
7
8
9
10
11public class SpellChecker {
private static final Dictionary DICTIONARY = new Dictionary();
public static boolean isValid(String word){
System.out.println("do something");
return true;
}
public SpellChecker(){}
}
Singleton引用底层资源1
2
3
4
5
6
7
8
9
10
11public class SpellChecker {
private final Dictionary dictionary = new Dictionary();
private SpellChecker(){}
public boolean isValid(String word){
System.out.println("do something");
return true;
}
}
以上引入资源的两种形式都不太好。因为没法支持多种词典。所以静态资源类和Singleton类不适合作需要引用底层资源的类。
所以使用依赖注入来引入底层资源,当创建一个新的实例时,将该资源传到构造器中去。1
2
3
4
5
6
7
8
9
10
11
12
13public class SpellChecker {
private final Dictionary dictionary;
public SpellChecker(Dictionary dictionary){
this.dictionary = dictionary;
}
public boolean isValid(String word){
System.out.println("do something");
return true;
}
}
依赖注入的变体形式
将资源工厂传给构造器
小结
- 不要使用Singleton和静态工具类来实现依赖一个或者多个底层资源的类
- 尽量将资源或者创建资源的工厂传给构造器,(或者工厂方法、builder),这样会增加类的灵活性、重用性和可测试性
避免创建不必要的对象
最好重用对象而不是每次需要的时候创建一个相同功能的新对象,这样可以提高性能,缩短响应时间。
String创建的案例
1 | String s = new String("test"); |
第一个语句: 每次执行都会创建新的String实例,这些创建的对象都是不必要的。如果用在一个频繁调用的方法中,就回创建成千上万不必要的String实例。
第二个语句: 对于所有在同一台虚拟机运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
- java会在方法区运行时常量池保存”test”,当下次调用String = “test”,java会直接返回这个对象的引用,而不会重新创建对象,由此节省了内存开销,可以放心在循环中使用
- String s = new String(“test”)实际创建了两个对象,一个对象在堆中,一个保存在常量池
优先使用静态工厂方法而不是构造器来创建对象
- 构造器每次调用都会创建新对象,而静态工厂方法不会这样,静态工厂方法可以重用对象,也可以加缓存
- 如果反复需要一些创建成本比较高的对象,建议缓存下来重用。
不要创建多个适配器
如果对象是可以变化的,也可以实现重用
适配器:把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口,适配器除了后备对象,没有其他任何信息。
- Map接口的keyset()返回Map对象的Set视图,包含Map的所有键
- 每次调用都返回Map对象锁对应的Set实例,即便Map内容会有所变化,也能反映到Set实例中。
所以没有必要创建多个Set实例。
1 | public Set<K> keySet() { |
避免自动装箱造成重复对象的创建
自动装箱会导致多余对象的创建
1 | private static long sum(){ |
声明的变量是Long而不是long,所以每一次循环都会构造多余的Long实例, 所以要优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。
小结
- 通过维护对象池来避免创建对象也不是好事情,除非对象池中的对象是非常重要的对象。
- 维护对象池会把代码弄得很乱,同时增加内存占用,损害性能
消除过期的对象引用
虽然java帮助我们完成了大部分内存管理的工作,但是我们还不不能对内存管理置之不理。
- Stack引发的内存溢出的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACOTY = 16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACOTY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size ==0){
throw new RuntimeException("no element");
}
return elements[--size];
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements,2*size+1);
}
}
}
- 栈先增长,后收缩,对于弹出来的对象,栈依旧维护者这些对象的引用,所以这些对象不会被当做垃圾回收
- 所以一旦对象引用已经过期,只需清空这些引用就可以了
1
2
3
4
5
6
7
8public Object pop(){
if(size ==0){
throw new RuntimeException("no element");
}
Object result = elements[--size];
elements[size] = null;
return result;
}
注意点: 清空对象引用是一种例外,而不是一种规范行为,对于栈这种自己管理内存的情况,程序员就应该警惕内存泄露问题。
- 缓存引起的内存泄露
缓存容易被遗忘,一旦时间长了,日积月累的缓存容易出现内存溢出的情况。
- 监听器和回调造成的内存溢出