EffectiveJava经验总结:第二章–创建和销毁对象
一、使用静态工厂代替构造方法
1、静态工厂方法可以减少创建对象的次数
减少对象创建次数,对于一些长时间使用的对象 例如数据库连接对象,不需要频繁的创建和销毁 整个生命周期一个就够了,将这种类的创建放在静态代码块里面,只创建一个,mysql驱动类创建方式就是这样类似单例模式享元模式
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
2、静态工厂方法有名字,能明确的知道创建出这个对象是什么样子的,不需要根据参数类型去判断(这个比较好理解)
3、静态工厂方法可以返回类的子类型
public class Animal {
public static Animal getInstance() {
// 这里可以返回父类Animal,也可以返回Tiger或者Horse,所以比构造器创建对象要更加灵活一些。
return new Animal();
}
}
class Tiger extends Animal {
}
class Horse extends Animal {
}
4、静态工厂方法可以根据方法的参数值返回不同的对象
可以在创建对象的工厂方法里面做判断,根据传进来的方法参数不同创建不同的对象,但是不需要传参数
public class Animal {
// 吃荤的类型
private static final int TYPE_EAT_MEAT = 0;
// 吃素的类型
private static final int TYPE_EAT_GRASS = 1;
/**
* 根据传入参数的不同返回不同类型的
*/
public static Animal getInstance(int type) {
// 这里可以返回父类Animal,也可以返回Tiger或者Lion,所以比构造器创建对象要更加灵活一些。
if (TYPE_EAT_MEAT == type) {
return new Tiger();
}
if (TYPE_EAT_GRASS == type) {
return new Horse();
}
return new Animal();
}
}
5、静态工厂方法返回的对象类:在编写该静态方法时候可以不存在
比如下面这个例子,在这里虽然调用了静态方法创建对象,但实际上没被调用之前,没有对象被创建了,
// 根据不同的class字节码创建不同的对象,这儿为了方便并没有对传入的字节码类型做校验。
public static <T> T getAnimal(Class c) throws Exception {
T o = (T) c.newInstance();
if (Animal.class.isInstance(o)) {
return o;
}
return null;
}
6、提供静态方法的类必须提供公共的或者受保护的构造器
要创建一个对象或者这个对象的子类,都需要有构造方法
7、静态工场方法容易和其他的静态方法混淆 要注意
二、当构造方法参数过多时使用builder模式
1、重叠构造器的弊端(深有体会,容易把参数放错位置,每次使用比较麻烦)
2、使用javabean的方式创建一个无参构造,然后再对其中参数进行赋值:(这种情况有个弊端就是线程安全,在不同的时刻不同的线程来获取对象可能得到的参数值不同,需要额外花精力来解决线程的数据同步)
3、Builder方式:使用建造者设计方式:
与构造器相比,Builder模式灵活,可以利用单个builder构建多个对象。这里的是线程安全的,且可读性也像javaBean那样setter方法那样。
builder的内部实现实际上就是使用一个静态内部类,看set了什么参数,创建什么样子的对象。
public class Card {
private int id;
private String name;
private boolean sex;
Card(int id, String name, boolean sex) {
this.id = id;
this.name = name;
this.sex = sex;
}
public static Card.CardBuilder builder() {
return new Card.CardBuilder();
}
public static class CardBuilder {
private int id;
private String name;
private boolean sex;
CardBuilder() {
}
public Card.CardBuilder id(int id) {
this.id = id;
return this;
}
public Card.CardBuilder name(String name) {
this.name = name;
return this;
}
public Card.CardBuilder sex(boolean sex) {
this.sex = sex;
return this;
}
public Card build() {
return new Card(this.id, this.name, this.sex);
}
public String toString() {
return "Card.CardBuilder(id=" + this.id + ", name=" + this.name + ", sex=" + this.sex + ")";
}
}
}
三、用私有构造器或者枚举类型强化Singleton属性
主要是保证一个类只提供一个Singleton类型的对象,一般通过final创建一个实例,静态工厂方法可以灵活的返回这个实例(灵活体现在是否想反回这个单例还是一个新的对象),但是需要注意:序列化的时候可能回序列化出一个非单例的,仅仅使用implement方法不可行,还需要提供一个readResolve() 方法,确认实例化的是上面的枚举单例。
// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
四、通过私有构造器强化不可实例化的能力
意思是说我们在编写工具类的时候,比如一个关于时间的 TimeUtiles,里面都是相应的静态方法,可以让这个类有一个private的构造方法,但是这样子就不能有子类了, 子类的所有构造函数都必须显示或者隐式地调用父类的构造函数,在这种情形下,子类就没有可访问的父类构造器可用了。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass(( {
throw new AssertionError();
}
... // Remainder omitted
}
五、固定资源首选依赖注入
如果我要实现一个类,这个类有依赖其他资源类,但是这个资源类的类型和个数不相同,这种情况下, 用一个方法去添加创建这些资源类,同样会有线程安全问题,这时候可以用依赖注入的方式解决
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
如上图所示:在构造方法里面传递一个资源类,就可以改变资源类的类型,在扩展一下,如果传进来的是一个资源类的工厂方法,就可以线程安全的创建使用不同资源类的对象。
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
六、避免创建不必要的对象
在使用一些方法时 我们会无意之间创建很多不必要的类拖垮了性能,
比如使用String str = new (“abc”);这种会循环的创建很多字符串对象,使用 String a = "abc"使用的就一直是一个对象了,另外在使用正则表达式的match方法时:match方法会创建一个partten对象,partten需要就爱哪个正则表达式便以为有限状态机,为了提高性能将正则表达式显式编译为Pattern实例(不可变)作为类初始化的一部分,对其进行缓存,并在每次调用使用正则验证方法时重用相同的实例:
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
这样子可以大大的加快性能。
另一个例子是自动装箱:如下图
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
图中使用Long创建了一个对象 在后进行相加,这种情况导致每次做加法都创建了一个Long的新对象,指向宿命,使用long的话会好很多。
七、清除过期对象的引用
看下这个例子:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
在一个栈的pop方法里,仅仅是将元素返回,会不停的创建对象,前一个引用一直存在,导致栈溢出等问题,只需要返回前一个,将当前置为null即可解决
Stack类容易被内存影响的原因是:stack类自己管理内存它相当于一个小的栈,自己维护了一下elements的数组,只有在这里面的部分是有用的,而其他的元素不清除,JVM是不清楚的。
只要是自己管理内存的类,都需要注意下,
另一个常见的内存泄露是缓存:使用WeakHashMap过期清除,使用弱引用(一旦发现弱引用对象无论当前空间是否充足都会回收)能淘汰掉一些时间较久的缓存对象。
八、避免使用终结方法和清空方法
语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证它们会被执行。不要被System.gc和System.runFinalization这两个方法所诱惑,他们确实增加了终结方法和清理方法被执行的机会,但是他们不保证终结方法或清理方法一定会被执行。唯一声称保证这两个方法一定会被执行的方法是System.runFinalizersOnExit,以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命的缺陷,已经被废弃了[ThreadStop]。
因为终结器也会抑制有效的垃圾收集。某些类(比如文件或线程)封装了需要终止的资源,对于这些类的对象,你应该用什么方法来替代终结方法和清理方法呢?让他们实现AutoCloseable接口即可,
这里经验教训是很明确的(The lesson is clear):在处理必须关闭的资源时,相比于try-finally,始终优先使用try-with-resources。 生成的代码更短更清晰,它生成的异常更有用。 try-with-resources语句可以在使用必须关闭的资源的同同时轻松编写正确的代码,使用try-finally几乎是不可能的。