读书笔记|如何更好的创建和销毁对象

静态工厂方法代替构造器

静态工厂方法的几大优势

  1. 静态工厂方法有名称,易于区分。

  2. 避免不必要的重复对象的创建工作

不必每次调用都创建一个新的对象,可以将构建好的实例缓存起来重复使用

  1. 可以返回原返回类型的任何子类型

返回对象有了更大的灵活性

  1. 返回的对象的类可以随着每次调用而发生变化,取决于静态工厂方法的参数值

  2. 方法返回对象所属的类,在编写包含静态工厂方法的类时可以不存在

静态工厂方法的几个缺点

  • 类如果不含公有的或者受保护的构造器,就不能被子类化
  • 不易发现

遇到多个参数时要使用构建器

当构造器扩展到大量的可选参数时,静态工厂和构造器就显得相当局限了。目前用的比较多的是重叠构造器

1
NutritionFacts cocaCola = new NutritionFacts(240,8,100,0,35,27);
缺点
  • 可读性比较差
  • 容易出错,如参数位置弄错位置

使用setter方案

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class NutritionFacts {
private int servingSize = -1;
private int servings = -1;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;

public NutritionFacts() {
}

public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}

public void setServings(int servings) {
this.servings = servings;
}

public void setCalories(int calories) {
this.calories = calories;
}

public void setFat(int fat) {
this.fat = fat;
}

public void setSodium(int sodium) {
this.sodium = sodium;
}

public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}

NutritionFacts nutritionFacts = new NutritionFacts();
nutritionFacts.setServingSize(240);
nutritionFacts.setServings(100);
nutritionFacts.setSodium(35);
nutritionFacts.setCarbohydrate(27);
nutritionFacts.setCalories(8);
  • 可读性增强
  • 一致性的问题(不安全)

构建器

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class NutritionFacts {
private int servingSize = -1;
private int servings = -1;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;

private NutritionFacts(Builder builder){
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}

public static class Builder{
private final int servingSize;
private final int servings;

private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;

public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}

public NutritionFacts build(){
return new NutritionFacts(this);
}

public Builder calories(int val){
this.calories = val;
return this;
}

public Builder fat(int val){
this.fat = val;
return this;
}

public Builder sodium(int val){
this.sodium = val;
return this;
}

public Builder carbohydrate(int val){
this.carbohydrate = val;
return this;
}
}

public static void main(String[] args) {
NutritionFacts nutritionFacts = new NutritionFacts.Builder(240,8).calories(100)
.sodium(35).carbohydrate(27).build();
}
}
  • 直观、易于阅读(优点)
  • 在注重性能的情况下,创建构建起是个问题(缺点)
  • 最好一开始就用构建器(建议)

小结

如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式是个不错的选择

私有构造器或者枚举类型强化Singleton属性

Singleton:仅仅被实例化一次的类

Singleton的两种实现

单例模式的实现(一)
1
2
3
4
5
6
7
8
9
public class Elvis {
public static final Elvis instance = new Elvis();

private Elvis(){}

public void doSomething(){
System.out.println("do something");
}
}

特点

  • 私有构造器只调用一次,用来实例化公有的静态final域(instance)
  • 没有public或者protect的构造器,保证了instance的唯一性
  • 但是可以通过反射的方式调用私有构造器(可以修改构造器在创建第二次实例时抛出异常)
单例模式的实现(二)
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Elvis {
public static final Elvis instance = new Elvis();

private Elvis(){}

public static Elvis getInstance(){
return instance;
}

public void doSomething(){
System.out.println("do something");
}
}

特点

  • 添加了一个静态工厂方法,静态工厂方法的调用每次都返回同一个对象引用
  • 公有的静态域是final的,该域总是包含相同的对象引用。公有域方法在性能上不会有任何优势,现在JVM实现能够将静态工厂方法的调用内联化。(内联:函数被调用的地方直接展开,编译器在调用时不会像一般函数那样,参数压栈,返回时参数出栈以及释放资源,直接提高程序的执行速度)
Singleton可序列化问题

Singleton仅仅在声明中加上implement Serializable是不够的,必须所有的实例都是瞬时的,并提供一个readResolve方法,否则每次反序列化都会创建一个新的实例

单例模式的实现(三)— 通过枚举类型实现

1
2
3
4
5
6
7
public enum Elvis {
INSTANCE;

public void doSomething(){
System.out.println("do something");
}
}

特点

  • 简洁
  • 无偿提供了序列化机制,防止多次实例化
  • 单元素的枚举类型是实现Singleton的最佳方法

使用依赖注入来引用资源

静态工具类引用底层资源

1
2
3
4
5
6
7
8
9
10
11
public 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
11
public 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
13
public 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
2
3
String s = new String("test");

String s = "test";

第一个语句: 每次执行都会创建新的String实例,这些创建的对象都是不必要的。如果用在一个频繁调用的方法中,就回创建成千上万不必要的String实例。

第二个语句: 对于所有在同一台虚拟机运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。

  • java会在方法区运行时常量池保存”test”,当下次调用String = “test”,java会直接返回这个对象的引用,而不会重新创建对象,由此节省了内存开销,可以放心在循环中使用
  • String s = new String(“test”)实际创建了两个对象,一个对象在堆中,一个保存在常量池

优先使用静态工厂方法而不是构造器来创建对象

  • 构造器每次调用都会创建新对象,而静态工厂方法不会这样,静态工厂方法可以重用对象,也可以加缓存
  • 如果反复需要一些创建成本比较高的对象,建议缓存下来重用。

不要创建多个适配器

如果对象是可以变化的,也可以实现重用

适配器:把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口,适配器除了后备对象,没有其他任何信息。

  • Map接口的keyset()返回Map对象的Set视图,包含Map的所有键
  • 每次调用都返回Map对象锁对应的Set实例,即便Map内容会有所变化,也能反映到Set实例中。

所以没有必要创建多个Set实例。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();

public boolean hasNext() {
return i.hasNext();
}

public K next() {
return i.next().getKey();
}

public void remove() {
i.remove();
}
};
}

public int size() {
return AbstractMap.this.size();
}

public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}

public void clear() {
AbstractMap.this.clear();
}

public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
keySet = ks;
}
return ks;
}

避免自动装箱造成重复对象的创建

自动装箱会导致多余对象的创建

1
2
3
4
5
6
7
private static long sum(){
Long sum = 0L;
for(long i = 0;i<=Integer.MAX_VALUE;i++){
sum += i;
}
return sum;
}

声明的变量是Long而不是long,所以每一次循环都会构造多余的Long实例, 所以要优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。

小结

  • 通过维护对象池来避免创建对象也不是好事情,除非对象池中的对象是非常重要的对象。
  • 维护对象池会把代码弄得很乱,同时增加内存占用,损害性能

消除过期的对象引用

虽然java帮助我们完成了大部分内存管理的工作,但是我们还不不能对内存管理置之不理。

  1. 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
    29
    public 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
    8
    public Object pop(){
    if(size ==0){
    throw new RuntimeException("no element");
    }
    Object result = elements[--size];
    elements[size] = null;
    return result;
    }

注意点: 清空对象引用是一种例外,而不是一种规范行为,对于栈这种自己管理内存的情况,程序员就应该警惕内存泄露问题。

  1. 缓存引起的内存泄露

缓存容易被遗忘,一旦时间长了,日积月累的缓存容易出现内存溢出的情况。

  1. 监听器和回调造成的内存溢出

避免使用终结方法

try-with-resource优先于try-finally