第一条: 考虑用 “静态工厂方法” 代替 “构造器”
此处的 “静态工厂方法” 不直接对应设计模式中的工厂方法。
“静态工厂方法” 相对于 “构造器”有以下4点优势:
第一大优势:有名称。
名字可用于描述被返回的对象,易于阅读和字面理解。类似 bigApple()、smallApple()可以很形象表述要返回对象的特征。
第二大优势:不必每次调用都创建一个新对象。
比如 “单例模式”,就可以通过 “静态工厂方法” 实现。
“单例模式” 返回的对象,在进行 “相等” 判断时,可以用 “==” 操作符代替 equals(Object) 方法
第三大优势:可以返回原返回类型的任何子类型的对象。
这种方式可以 “隐藏实现类”,实现类可以是非公有的,代码可以是不可见的(可以以jar包的形式被import)。这里的“实现类”指的是静态工厂方法返回的对象所属的类。
这个优势使得 coder 在选择返回对象时有了更大的灵活性,很多服务框架就是基于这个优势产生的。
关于“服务框架”的详情,之前没有接触过,理解的不彻底。
第四大优势:在创建参数化类型实例的时候,代码可以变得更简洁。
这里的 “参数化类型”,我个人理解应该是 “泛型” 的使用。
举个例子:
一般情况下:
Map<String, List<String>> m = new HashMap<String, List<String>>();
但是HashMap中提供了以下静态工厂:
public static <K, V> HashMap<K, V> newInstance(){
return new HashMap<K, V>();
}
这样,可以使用下面这句简洁的代码代替上面方所的声明:
Map<String, List<String>> m = HashMap.newInstance();
“静态工厂方法” 的两个缺点:
第一个缺点:如果类不包含public或者protected的构造器(constructor),就不能被子类化。
关于Java中类的构造器有以下规则:
当类不包含显式的构造器时,编译器会生成缺省的构造器。
所有构造器都必须显式或者隐式地调用超类(superclass)构造器。
子类的构造器中,如果没有使用super(有参或无参),系统默认调用父类的无参构造函数,否则编译通不过。
因此:父类必须有构造器,而且必须是能被子类引用的构造器(public或者protected,不能全是private;如果全是private,则不能子类化)
第二个缺点:“静态工厂方法” 与其他静态方法没有本质区别。
这样导致一个后果:无法向构造器那样在 API 文档中被明确标识处理,因此,对于提供了“静态工厂方法” 而未提供 “构造器” 的类来说,coder 想要查明如何实例化一个类,有些困难,因为无法从 API 文档中获取相关信息。
这个缺点,对应Java 5中的Javadoc工具。不确定最新版本的JDK中是否有变化。
第二条: 遇到多个构造器参数时考虑用构建器(Builder)
通常,一个类在构造器 Constructor 中待初始化的成员变量可以分为必要(必须要的)参数和可选参数;当一个类有多个初始化可选参数时,如果按常规方法,将可选参数全部在一个函数中实现初始化,会导致一定的混乱,此时,静态工厂方法和构造器便遇到了瓶颈。
常见有以下三种解决方案。
第一种:重叠构造器
public class NutritionFacts{
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public NutritionFacts(int servingSize, int servings){
this(seringSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories){
this(seringSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat){
this(seringSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium){
this(seringSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbonhydrate){
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbonhydrate = carbonhydrate;
}
}
重叠构造器的缺点:
当客户端使用参数众多的构造器时,代码难写,切难以阅读。
第二种:JavaBeans模式
什么是JavaBeans模式?
该模式的实现主要包含两个步骤:
第一步:调用一个无参构造器创建对象。
第二步:调用setter方法设置每个必要的参数,以及每个相关可选的参数。
代码实例:
public class NutritionFacts{
private final int servingSize = -1;
private final int servings = -1;
private final int calories = 0;
private final int fat = 0;
private final int sodium = 0;
private final int carbohydrate = 0;
public NutritionFacts(){}
public void setServingSize(int val) {servingSize = val};
public void setServings(int val) {servings = val};
public void setCalories(int val) {calories = val};
public void setFat(int val) {fat = val};
public void setSodium(int val) {sodium = val};
public void setCarbohydrate(int val) {carbohydrate = val};
}
JavaBeans模式的缺点:
因为构造过程被分到了几个调用中,在构建过程中JavaBean可以处于不一致的状态。
这一描述中 “不一致状态” 本人不太理解,通过网上其他人的解释,大概的意思就是说,使用同一个构造器(该模式下的无参构造器)获取的两个对象,有可能包含的属性(未调对应setter函数即不包含该属性)是不一致的,因为各个对象实际的属性取决于对应的setter函数的调用。
另外,JavaBeans模式阻止了把类做成不可变的可能,需要coder复出额外精力保证 “线程安全。”
第三种:构建器(Builder)模式
该方法既能保证像重叠构造器模式那样的安全性,也能保证JavaBeans模式那样好的可读性。
这种模式不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或静态工厂),得到一个Builder对象;然后客户端在Builder对象上调用类似setter的方法,来设置每个相关的可选参数。最后,客户端调用Builder对象的无参build方法来生成不可变的对象。这个Builder是待构造的类的静态内部类。
代码实例:
public class NutritionFacts{
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbonhydrate;
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 carbonhydrate = 0;
public Builder(int servingSize, int servings){
this.servingSize = servingSize;
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 Builder carbonhydrate(int val)
{ carbonhydrate = val; return this;}
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder){
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbonhydrate = builder.carbonhydrate;
}
}
优点:
易于阅读;
使用灵活,单个Builder对象可构建多个对象。Builder对象的参数可以在正式调用build()函数创建对象之前进行调整。也可以随着不同的对象而改变。
缺点:
为创建对象,必须先创建他的构建器,在十分注重性能的情况下,有可能有影响。
选择构建器(Builder)模式的场景:
优越Builder模式比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用。
另外,构建器的使用最好在最初定义类的时候就加入,不然多种模式的使用,会显得复杂、重复、不协调,这样就违背了使用Builder模式的初衷。
第三讲:用私有构造器或者枚举类型强化Singleton属性
说实话,工作4年多来,这一讲谈到的很多知识点,我都没用到过,甚至之前都不知道,也不是太理解。
该讲提到了三种Singleton的实现方法:
第一种方法:公有的静态final域
//Singleton with public final field
public class Elvis{
public static final Elvis INSTANCE = new Elvis();
private Elvis(){...}
public void leaveTheBuilding(){...}
}
提醒:享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。
若需要抵御这种攻击,可以修改构造器,在它被要求创建第二个实例的时候抛出异常。
第二种方法:静态工厂方法
//Singleton with static factory
public class Elvis{
private static final Elvis INSTANCE = new Elvis();
private Elvis(){...}
public static Elvis getInstance(){ return INSTANCE; }
public void leaveTheBuilding(){...}
}
上述提醒依然适用。
优势:提供了灵活性:在不改变 API 的前提下,可以变更该类的 Singleton属性。
如果想让一个Singleton类变成可序列化,需要做好以下三个步骤:
第一步:“implements Serializable”
第二步:声明所有的实例域都是瞬时的(transient)的
第三步:提供一个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 imperonator
return INSTANCE;
}
这么做的原因是:每次反序列化一个序列化实例时,都会创建一个新的实例,从而破坏 Singleton 属性,比如上面的 Elvis 类;为防止该情况发生,需要在 Elvis 类中添加 readResolve 方法。
第三种方法:单元素枚举类型
//Enum singleton - the preferred approach
public enum Elvis{
INSTANCE;
public void leaveTheBuilding(){...}
}
该方法功能上同第一个方法接近,但更加简洁,无偿提供了序列化机制,绝对防止多次实例化,同事可以有效应对反射攻击。
枚举的使用,基本语法尚不清楚,因此这一方法我个人的理解还很不到位。
第4条:通过私有构造器强化不可实例化的能力
Java中,在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器。
对于用户而言,这个构造器和其他构造器没有任何区别。
因此,要实现类的不可实例化,必须自定义显式的构造器,否则,编译器会自动提供public的构造器;而且,所有显式构造器必须是私有的,才能保证无法被用户调用,从而实现不可实例化。
具体实现参考以下形式:
//Noninstantiable utility class
public class UtilityClass{
//Suppress default constructor for noninstantiability
private UtilityClass(){
throw new AssertionError();
}
... // Remainder omitted
}
由于上述显式的构造器是私有的,所以不可以在该类外部访问它。
AssertionError不是必需的,但是它可以避免不小心在类内部调用构造器。它保证任何情况下都不会被实例化。
上述方法存在一个副作用:
该类无法子类化
由于所有的构造器都必须显示或者隐式调用超类(superclass)构造器,在这种情况下,上述做法导致:子类没有可访问的超类构造器。
第5讲:避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。
这个“一般来说”,非常适用于,某个会被频繁重复调用的代码块。
例:
String s = new String("string ette"); //DON'T DO THIS!
改进后的版本如下:
String s = "string ette";
这个规则依据的理论是:性能改进领域经常用到的 “空间换时间”。
另外,关于 “基本类型” 和 “装箱基本类型” 的使用,要遵守:优先使用 “基本类型” 而不是 “装箱基本类型”。
所谓 “装箱基本类型” 是对应 “基本类型” 的封装类
装箱:讲 “基本类型” 转换成 “包装类” 的过程
拆箱:将 “包装类” 转换成 “基本类型” 的过程
关于 “装箱基本类型” 和 “基本类型” 的关系理解,如下:
Java八种基本数据类型的大小,以及封装类,自动装箱/拆箱的用法?
原始类型-大小-包装类型
(1) char-2B-Character
booelan-1B-Boolean
(2) byte-1B-Byte
short-2B-Short
int-4B-Integer
long-8B-Long
(3) float-4B-Float
double-8B-Double
代码示例:
1 public class Solution {
2
3 public static void main(String[] args) {
4
5 Integer a = new Integer(3);
6
7 Integer b = 3; // 将3自动装箱成Integer类型,new一个Integer对象
8 Integer c = 3; // 如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象
9
10 int d = 3;
11
12 Integer e = 200;
13 Integer f = 200;
14
15 System.out.println(a == b); // a和b是不同对象的引用,返回false
16 System.out.println(b == c); // 3在-128到127之间,故c装箱时不再new对象,b和c指向同一个对象,返回true
17 System.out.println(b == d); // 自动拆箱,基本类型的比较,返回true
18 System.out.println(e == f); // 200不在-128到127之间,故e和f分别指向不同的对象,返回false
19
20 }
21 }
在遵守避免创建不必要对象的时候,要避免陷入以下认识:
“创建对象的代价非常昂贵,我们应该要尽可能的避免创建对象”。
实际上,由于小对象的构造器只做了很少量的显式工作,所以,小对象的创建和回收是非常廉价的。
要避免对本条规则认识的片面性。
在实际编码过程中,对于一些重要的、涉及系统安全性的对象数据,在操作时要适时采取“保护性拷贝”,实现保证系统“安全性”的目标;反之,如果一昧的强调“避免创建对象”,为了保证“安全性”等,有可能极大增加代码复杂度。
总之,实际编码时,要准备评估 “重用对象要复出的代价” 和 “创建重复对象要复出的代价”,做出最优取舍。