本文主题是创建和销毁对象,关注一下几个问题:
- 何时以及如何创建对象
- 何时以及如何避免创建对象
- 如何去报它们能够适时销毁
- 如何管理对象销毁之前必须进行的各种清理动作
1.考虑使用静态工厂方法代替构造器(静态工厂模式)
创建类实例的方式有两种:
- 公有的构造器
- 公有的静态工厂方法
静态工厂方法
-
优势
- 静态工厂方法与构造器不同的第一大优势在于,它们有名称,如果构造器的参数本身没有确切的描述返回的对象,那么适当名称的静态工厂会更加合适
- 不必每次调用它们的时候都创建一个新对象,使得不可变对象可以使用预先构建好的实例,利用缓存实例进行复用,为重复的调用返回相同的对象,如果创建对象的代价很高,这个技术可以极大提升性能
- 可以返回类型的任何子类型的对象,在选择返回对象时有更大的灵活性。
- 可以返回非公有对象,同时又不会使对象的类变成公有的,隐藏实现类
- 公有的静态工厂方法所返回对象的类不仅可以是非公有的,而且该类可以对着每次调用而发生变化,取决于静态工厂方法的参数值(工厂方法模式)
- 创建参数化类型(泛型)实例时,使代码变得更加简洁
-
缺点
- 类如果不含有公有或者受保护构造器,就不能被子类化
- 与其他的静态方法实际上没有任何区别
-
静态工厂方法命名规范
- valueOf
- 该方法返回的实例与它的参数具有相同的值,这样的静态工厂方法实际上是类型转换方法
- of
- getInstance
- 返回的实例通过方法的参数来描述,如果没有参数,则返回唯一的单例
- newInstance
- 确保返回的实例都与其他实例不同
- getType
- newType
- valueOf
2.遇到多个构造器参数时要考虑用构建器(Builder创建者模式)
静态工厂和构造器有个共同的局限性,不能很好地扩展到大量可选参数。
处理有大量可选参数的构造器的方式:
- 重叠构造器
- JavaBeans 模式
- Builder 模式
重叠构造器
提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推,最后一个构造器包含所有可选参数。
public NutritionFact(int servingSize, int servings){}
public NutritionFact(int servingSize, int servings, int calories){}
public NutritionFact(int servingSize, int servings, int calories, int fat){}
public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium){}
...
缺点:重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写和难以阅读
JavaBeans 模式
另一种替代方法,JavaBeans 模式,调用一个无参构造器来创建对象,然后调用setter方法设置每个必要参数,以及每个相关的可选参数。
NutritionFact cocoCola = new NutritionFact();
cocoCola.setServingSize(240);
cocoCola.setServings(8);
cocoCola.setCalories(100);
cocoCola.setSodium(35);
cocoCola.setCarbohydrate(27);
缺点:
- 构造过程被分到几个调用中,构造过程 JavaBean 可能处于不一致的状态。类无法仅仅通过校验构造器参数的有效性来保证一致性
- JavaBean 模式阻止了把类做成不可变的可能,需要确保它的线程安全
Builder 模式
不直接生成想要的对象,客户端利用多有必要的参数调用构造器(或静态工厂)得到一个builder对象,然后客户端再builder 对象上调用类似setter方法,来设置每个相关的可选参数,最后客户端调用无参的build方法来生成不可变的对象,这个builder是类的静态成员类。
public class NutritionFacts {
private final int calories = 0;
private final int fat = 0;
private final int sodium = 0;
// 静态内部类 Builder 对象
public static class Builder {
private int calories = 0;
private int fat = 0;
private int sodium = 0;
// setter方法返回当前builder对象,方便链式调用
public Builder setCalories(int val) {
calories = val;
}
public Builder setFat(int val) {
fat = val;
}
public Builder setSodium(int val) {
sodium = val;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
// 传入 Builder 对象的构造方法
public NutritionFacts(Builder builder) {
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
}
}
缺点:需要额外开销
总结
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder 模式就是不错的选择。
3.用私有构造器或者枚举类型强化Singleton属性(单例模式)
Singleton 指仅仅被实例化一次的类。
实现Singleton的方式有很多种
方式一
把构造器保持为私有的,并导出公有的静态成员,并且静态成员是个final的
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {...};
}
问题:无法抵御通过反射调用私有构造器的攻击。
方案:可以修改构造器,让它在要求创建第二个实例的时候抛出异常。
方式二
方式二中,公有成员不再是属性,而是一个静态方法getInstance
public class Elvis {
pvivate static final Elvis INSTANCE = new Elvis();
private Elvis() {...};
public static Elvis getInstance() {return INSTANCE;}
}
问题:如果此类实现了序列化,序列化之后的结果都会创建一个新的实例。
方案:重写readResolve方法
private Object readResolve() {
return INSTANCE;
}
方式三
编写一个包含单个元素的枚举类型。
public enum Elvis {
INSTANCE;
}
与公有方法相近,但更加简洁,无偿的提供了序列化机制,并且防止序列到导致多次实例化,并且防止反射的攻击。
总结
单元素的枚举类型已经成为实现Singleton的最佳方法。
4.通过私有构造器强化不可实例化的能力
工具类不希望被实例化,实例对它没有任何意义。
在缺少显示构造器时,编译器会自动提供一个公有的,无参的缺省构造器。
可通过创建私有构造器,并构造器中抛出异常,来避免实例化此类。
5.避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。如果对象是不可变的,那么它就应该始终被重用。
举例一
String s = new String("mystring") // 每次都会创建一个新的String实例
String s = "mystring" // 推荐,保证在同一台虚拟机中运行的代码,只要包含相同的字符串字面常量,就会被重用
举例二:不可变类
对于不可变类,优先使用静态工厂方法,每次调用可以重用,避免创建不必要的对象。
举例三
通过静态初始化器避免在每次调用方法时都会生成一些不必要的对象。
class Person {
static {
// 初始化整个类需要用到的不可变可重用对象
}
public boolean isBaby() {
// 这里使用到一些不可变的对象,无需每次都创建,把创建操作放到静态初始化器中,这里直接使用即可
}
}
缺点:如果方法没有被调用,那么初始化工作就没有必要,可以通过延迟初始化,即把初始化工作放到方法初次调用时。
举例四
自动装箱会创建出多余的对象。
sum 声明为 Long 类型,导致循环内部构造大量大于 Long 实例。
Long sum = 0;
for (long i = 0;i < Interger.MAX_VALUE; i ++) {
sum += i;
}
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
举例五
通过维护自己的对象池来避免创建对象并不会一种好的做法,除非池中的对象是非常重量级的,现代JVM实现具有高度优化的垃圾回收器,其性能很容易就超过轻量级对象池的性能。
6.消除过期的对象引用
- 只要类自己管理内存,程序就应该警惕内存泄漏问题
- 内存泄漏另一个场景来源是缓存
- 另一个场景是监听器和其他回调,如果注册了回调,却没有显式地取消注册,那么会产生内存泄漏,确保回调立即被当做垃圾回收的最佳方法是只保持它们的弱引用。
7.避免使用终结方法
- 介绍
- 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的
- 使用终结方法导致行为不稳定,降低性能,以及可移植性问题
- C++的析构函数可以被用来回收其他的非内存资源,Java 中,一般用try-finally块来完成类似工作
- 终结方法的缺点在于不能保证被及时执行,JVM会延迟执行终结方法,所以不要用来关闭已经打开的文件,程序不能依赖终结方法被执行的时间点
- 不应该依赖终结方法来更新重要的持久状态
- System.gc 和 System.runFinalization 增加了终结方法执行的机会,但不能保证终结方法一定会被执行
- System.runFinalizersOnExit 可以保证终结方法被执行,当然此方法已被废弃
- 如果被捕获的异常在终结方法中被抛出,那么这种异常会被忽略
- 使用终结方法有非常严重的性能损失,即使什么也不做
- 终止资源(文件或线程资源)
- 显示提供一个终止方法,在实例不再需要时,调用此方法
- 通常与try-finally结构结合,在finally中显式调用终止方法
网友评论