总括
1.使用静态工厂方法代替构造方法
2.当构造函数有很多参数时考虑使用构建器
3.用私有构造器或枚举类型实现单例模式
4.在想让一个工具类不被实例化的时候使用私有构造函数
5.优先考虑使用依赖注入来引用资源
6.避免创建不必要的对象
7.消除不必要的对象引用
8.避免使用finalize和clear方法
9.try-with-resources优先于try-finally
1.使用静态工厂方法代替构造方法
如将boolean转成Boolean对象的方法:
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
静态工厂方法的优点:
(1)这个方法有名称,更易用,代码更易读
(2)与构造方法不同,它不会在每次调用的时候都创建一个对象,也就意味着它能够为重复的调用返回相同的对象
(3)它可以返回原返回类型的任意子类对象
(4)静态方法返回对象的类型会随着参数的不同而不同
(5)方法返回对象所属的类,在编写该包含静态工厂方法的类时可以不存在
一个类最好只有一个带有指定签名的构造函数,这样使得在使用的时候不会出现错误和混淆。我们也看到在Java swing中大量使用了静态工厂方法。
缺点:
(1)如该类没有公有构造函数,则它不能被继承
(2)javadoc不会像对待构造函数那样明确标出静态工厂方法,使之不易被发现
2.当构造函数有很多参数时考虑使用构建器
通常在实例化对象的时候我们有两种方式来满足个性化的实例化:使用重叠构造器和JavaBeans模式。前者在不同的构造器之间使用this相互引用,但当参数数量较多时使得代码较难编写且难以阅读;后者通过无参构造先创建对象,再通过setter进行初始化,这使得创建对象变得容易,但JavaBeans可能处于不一致状态,影响到类的不可变性。
于是就有了第三种方法:构建器(Builder)。它的做法是这样:考虑到构造函数中有必要的参数和可选的参数,它让客户端先利用必要参数调用构造方法,之后在builder对象上调用类似setter的方法来初始化可选参数。通常把Builder类作为该类的静态成员类。
例:
public class NutritionFacts{
//必要参数
private final int servings;
//可选参数
private final int calories;
private final int fat;
private final int sodium;
public static class Builder{
//必要参数
private final int servings;
//可选参数
private int calories = 0;
private int fat = 0;
private int sodium = 0;
public Builder(int servings){ this.servings = servings}
public Builder calories(int val){
calories = val;
return this;
}
public Builder fat(int val){
fat = val;
return this;
}
public Builder sodium(int val){
sodium = val;
return this;
}
public NutritionFacts(Builder builder){
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
}
}
}
则客户端使用构建器创建对象的代码为:
NutritionFacts Cola = new NutritionFacts.Builder(240).calories(100).sodium(30).build();
这样既可以实现按需求进行初始化,又使得代码变得易于阅读。
3.用私有构造器或枚举类型实现单例模式
方法一:
public class Single{
private static final Single INSTANCE = new Single();
private Single(){}
public static Single getInstance(){ return INSTANCE; }
public void whateverMethod(){//...}
}
方法二:使用枚举,适于需要序列化的时候。
public enum Singleton {
INSTANCE;
public void whateverMethod() { }
}
方法三:较常用的方法
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
4.在想让一个工具类不被实例化的时候使用私有构造函数
我们有时候会有一些提供静态方法的工具类来进行面向过程编程,因为是静态方法,他们是属于类的,虽然可以被继承,但是不存在多态,有时候我们希望这样的类不被实例化,于是可以在该类中定义私有的构造方法,因为若不这样做,编译器会自动生成无参构造,私有构造方法是的不能在类的外部进行实例化。
例:
public class UtilityClass{
//压制使用默认构造函数
private UtilityClass(){
throw new AssertionError(); //用于防止对象的空初始化,防止在类内调用该构造函数
}
}
5.优先考虑使用依赖注入来引用资源
当创建一个实例时,就将该资源传入构造方法中。例:
public class SpellChecker{
private final Lexicon dict;
public SpellChecker(Lexicon dict){
this.dict = Objects.requireNonNull(dict);
}
//...
}
此外,静态工具类和单例模式不适合用于需要引用资源的类。
当依赖较多时,管理各种依赖变得十分困难,这通常由Spring这样的依赖注入框架来解决。
6.避免创建不必要的对象
当创建对象的代价很大时(比如大对象),考虑对象的重复使用。
比如使用正则表达式匹配的String.matches方法时,由于每次调用该方法都会创建一个Pattern实例,所以我们可以将正则表达式编译缓存起来重复使用,如判断是否是罗马数字:
public class RomanNumerals{
private static final Pattern ROMAN = Pattern.compile(
"此处是正则表达式,省略"
);
static boolean isRomanNumerals(String s){
return ROMAN.matcher(s).matches();
}
}
此外在无必要时尽可能使用基本数据类型而非其包裹类型,这样可减少开销。
维护自己的对象池通常不是好的做法,除非对象非常之大,如数据库连接池,因为数据库连接的开销很大。轻量的对象就不必考虑复用了,因为JVM会帮我们进行优化。
7.消除不必要的对象引用
JVM将程序员从内存管理的痛苦中解放出来,但这不意味着我们不需要做内存管理了,看一下实现栈这一数据结构中弹栈的代码:
@SuppressWarnings("unchecked")
public T pop(){
if(!isEmpty()){
return (T)elements[top--];
}else{
System.out.println("Stack is empty");
return null;
}
}
当栈先增长,再缩小,则那些弹出栈的对象不会被垃圾回收。即使栈的程序不再引用这些对象,它们也不会被回收,因为栈内维护着这些对象的过期引用,虽然它们逻辑上不存在了,但是实际上并没有被回收,造成内存泄漏。
避免内存泄漏的做法是对这些不使用的对象进行解除引用,如下:
@SuppressWarnings("unchecked")
public T pop(){
if(!isEmpty()){
T result = (T)elements[--top];
elements[top] = null;
return result;
}else{
System.out.println("Stack is empty");
return null;
}
}
清空对象引用应该是一种例外,而不是一种规范,当数组元素变成了非活动的一部分(逻辑上不存在),需要手动进行清空。
8.避免使用finalize和clear方法
上述方法的缺点:不能保证被调用;存在性能损失和安全问题。
9.try-with-resources优先于try-finally
例:
try(BufferedReader br = new BufferedReader(new FileReader(path))){
return br.readLine();
}catch(IOException e){
//...
}
这用于避免多重try-finally嵌套造成的调试困难。
参考文献:《Effective Java》