Effective Java(第三版) 学习笔记 - 第二章 创建和销毁对象 Rule1~4
目录
Rule3 用私有构造器或者枚举类型强化Singleton属性
Rule1 用静态工厂方法替代构造器
静态工厂方法、实例工厂方法、构造器
/**
* HelloWord
* @author xuweijsnj
*/
public class HelloWord {
/**
* 构造器
*/
public HelloWord() {
}
public static void main(String[] args) {
// 构造器创建
HelloWord a1 = new HelloWord();
// 静态工厂创建
HelloWord a2 = StaticFactory.getInstance();
// 实例工厂创建
HelloWord a3 = new ExampleFactory().createHelloWorld();
}
}
/**
* 静态工厂方法
* @author xuweijsnj
*/
public class StaticFactory {
public static HelloWord getInstance() {
return new HelloWord();
}
}
/**
* 实例工厂方法
* @author xuweijsnj
*/
public class ExampleFactory {
public HelloWord createHelloWorld() {
return new HelloWord();
}
}
静态工厂方法和实例工厂方法对比
实例工厂需要先初始化实例工厂对象,然后再创建HelloWord对象
静态工厂方法和构造器相比优势
- 第一大优势:静态工厂方法可以有不同的带有各自含义的方法名。构造器必须与类名完全相同,而类名往往都是名词形式
例:
BigInteger(String)
BigInteger(int, Random)
BigInteger(int, int, Random)
如果只看这三个方法名,而不查看注释或者源码,使用者根本无法分清三者有何不同,各自入参应该传递什么内容。
BigInteger(int, int, Random)返回的BigInteger可能是素数。
相比之下probablePrime(int, Random)通过方法名来表示有可能是素数更加清晰。
- 第二大优势:静态工厂方法不必再每次调用他们的时候都创建一个新对象
这使得不可变类预先构建好实例,缓存起来重复利用,从而避免创建不必要的重复对象。
这种方法类似于设计模式的享元模式。
如果程序经常请求创建相同的对象,并且创建对象的代价很高,这样做可以极大的提升性能。
例:Boolean.class中
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
- 第三大优势:静态工厂方法可以返回原返回类型的任何子类型对象
构造函数只能创建原返回类型,而java8之后接口可以有默认的静态方法。而不需要像以往一样,接口Type的静态工厂方法会被放在一个名为Types的不可实例化的伴身类。
- 第四大优势:静态工厂方法可以根据参数值,返回不同的对象的类
例:EnumSet.class中,noneOf(Class<E>)方法,universe.length<=64,返回RegularEnumSet,其他则返回JumboEnumSet。
RegularEnumSet和JumboEnumSet都是EnumSet的子类,呼应第三大优势。
- 第五大优势:静态工厂方法返回的对象所属的类,在编写包含静态工厂方法时可以不存在
这种灵活性,构成了服务提供者框架基础。
例:JDBC API相关
Connection就是服务接口的一部分。DriverManager.registerDriver(Driver)是提供者注册API。DriverManager.getConnection(String, Properties, Class<?>)是服务访问API。Driver是服务提供者接口。
※服务提供者框架是指一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。
静态工厂方法和构造器相比劣势
- 如果类没有公有或者受保护的构造器(即、显示的实现了私有构造函数),就不能被子类化。
※但是这也有好处,鼓励程序员使用复合而不是继承
- 程序员如果不看源码,很难发现哪些方法是静态工厂实现的
作者提出了一种弥补该劣势的可能:遵守标准的命名规约
// from
Date d = Date.from(instant);
// of
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
// valueOf
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
// instance或getInstance
StackWalker luke = StackWalker.getInstance(options);
// create或者newInstance
Object newArray = Array.newInstance(classObject, arrayLen);
// getType
FileStore fs = Files.getFileStore(path);
// newType
BufferedReader br = Files.newBufferedReader(path);
// type
List<Complaint> litany = Collections.list(legacyLitany);
Rule2 遇到多个构造器参数时优先考虑使用构建器
思考一个问题
Q:做项目时,如果遇到一个POJO,具有多个属性,同时其中有几个是业务必须项,那么我们一般如何实现该POJO的对象创建?
我先来说说我的第一反应:
如果是接口入参,直接定义POJO的属性,使用Lombok注解,自动编译时生成get/set方法。针对必须项属性加上@NotEmpty注解。
如果是程序中间过程的POJO,还是定义属性使用Lombok注解,只不过赋值往往都是手动显示set,必须项属性一般良心一点的会注释上标明。但是往往团队开发中甚至连含义的注释都没有或者根本对应不上,至于如何判断是否是必须项属性就看各自对业务的理解了。
针对这个问题,书中给出了几种方式:
1、利用构造函数生成对象
// Telescoping constructor pattern - does not scale well! (Pages 10-11)
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola =
new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}
提供一个只包含必须属性的最短构造器,然后针对非必须属性,依次不断的生成新的构造函数依次累加。这样做缺陷在于,对象生成的构造器实在是太多了,非必须属性越多,构造器将越多。而且如果我如果只想赋值servingSize、servings、sodium这三个属性的话,没有合适的构造器。我一定得选择NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium),然后calories和fat给个初始值0,如果属性不是int,而是String的话,给个null。
说实话,让我这样实现我也不愿意,因为实在觉得太SB了。
2、利用JavaBean实现
// JavaBeans Pattern - allows inconsistency, mandates mutability (pages 11-12)
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1; // Required; no default value
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// Setters
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; }
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
这个方式往往就是我们常用的实现写法。但是书中提到了,有个遗憾就是JavaBeans模式自身有个严重的缺点:因为生成想要的对象时,构造过程被分到了几个调用中,就导致了在构造过程中JavaBean可能处于不一致的状态。简单理解来说就是对象是先创建出来通过不断赋值完成最终想要的结果,而不是一步到位直接生成想要的对象。这样就有可能有个隐患就是当你的属性才赋值到一半,已经有其他程序在使用这个对象了。虽然我个人感觉在项目一般开发中不会出现这样的实际情况,但是也必须承认这样实现,生成对象的代码行数看着不太简洁,属性越多赋值的代码行数就会越多。
看了构造器实现之后,针对利用JavaBean实现可能有个更优雅的写法,就是构造器不再是无参构造,而是提供一个所有必须项构造,但是其他非必须属性还是得自我赋值。最终还是没法解决构造一致性问题。
3、建造者模式
// Builder Pattern (Page 13)
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 static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
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 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 carbohydrate(int val)
{ carbohydrate = 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;
carbohydrate = builder.carbohydrate;
}
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
}
}
简单理解来说,就是不直接提供NutritionFacts的JavaBean,而是利用一个内部静态类,将属性的赋值部分独立出来,使得NutritionFacts对象的创建过程变得不可拆分,同时对象不可变。builder的赋值方法永远返回builder本身,这样可以形成一个流式的API调用链。
Builder模式也适用于类层次结构。
// Builder pattern for class hierarchies (Page 14)
// Note that the underlying "simulated self-type" idiom allows for arbitrary fluid hierarchies, not just builders
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
// HAM, MUSHROOM, ONION, PEPPER, SAUSAGE 火腿,蘑菇,洋葱,胡椒,香肠
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// Subclasses must override this method to return "this"
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // See Item 50
}
}
// Subclass with hierarchical builder (Page 15)
// 纽约披萨,可以指定尺寸
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override public NyPizza build() {
return new NyPizza(this);
}
@Override protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
@Override public String toString() {
return "New York Pizza with " + toppings;
}
}
// Subclass with hierarchical builder (Page 15)
// 卡尔佐内披萨,可以指定馅料是内置还是外置
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // Default
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
@Override public String toString() {
return String.format("Calzone with %s and sauce on the %s",
toppings, sauceInside ? "inside" : "outside");
}
}
// Using the hierarchical builder (Page 16)
public class PizzaTest {
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
System.out.println(pizza);
System.out.println(calzone);
}
}
父类中addTopping调用self,而父类中self是个abstract,这样就可以让NyPizza、Calzone子类实现返回各自类型的Builder。build方法可以构建各自子类类型:子类方法声明返回超级类中声明的返回类型的子类型,这被称为协变返回类型。它允许客户端无须转换类型就能使用这些构建器。
Builder模式十分灵活,相比构造器,它可以有多个可变参数。相比JavaBean模式,他在创建对象时,代码更精简(一行代码就可以搞定属性的赋值和最终对象的创建),而且没有不一致状态这个问题。
但是Builder模式也有自身的不足。为了创建对象,必须先创建它的构造器,虽然大部分情况下这部分的开销不那么明显,但是在某些十分追求性能的情况下,可能就是问题所在了。
同时,对象类自身的代码量比JavaBean模式更多,可能开发人员在追求项目进度的情况下,往往都不会考虑这么实现。而且因为对象属性都是在创建时就赋值的,如果想要再次针对某些属性二次赋值,可能就不是那么方便了。
但是反过来思考,如果一个POJO,不需要二次赋值,而且在项目中很多地方都需要使用到该对象的创建。那么项目代码中按照JavaBean实现,就会在很多地方存在大量的赋值语句,属性越多,赋值语句代码块就越长,看着实在是有点糟心。这种情况下,构建器模式声明对象的简洁就突出起来了。
Rule3 用私有构造器或者枚举类型强化Singleton属性
做开发就避免不了设计模式,接触设计模式往往都是从最简单的单例模式开始学习。
第一种 私有化构造函数,提供公有实例化静态成员
// Singleton with public final field (Page 17)
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
这个时候,有人就提出来了,不应该对外公开类的属性,让其他类可以直接接触当前类属性,而是对它进行封装,提供get方法。这个时候就出现了第二阶段。
第二种 私有化构造函数,提供静态工厂方法进行实例化
// Singleton with static factory (Page 17)
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
这个时候又有人提出来,一上来就已经实例化了Elvis,如果没有被用到,感觉有点资源浪费。这个时候就可以使用延迟加载的思想,如下实现。
第三种 利用静态工厂方法进行延迟加载
public class Elvis {
private static Elvis instance = null;
private Elvis() { }
public static Elvis getInstance() {
if (instance == null) {
instance = new Elvis();
}
return instance;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
这个就是最终最优实现方法么,有没有可能在多线程的场景下,两个线程同时进入到了if判空的代码块之中,这样就会导致被实例化两次?有这种可能性的,所以我们还有双重判空单例模式。
第四种 双重判空单例模式
public class Elvis {
private volatile static Elvis instance = null;
private Elvis() { }
public static Elvis getInstance() {
if (instance == null) {
synchronized (Elvis.class) {
if (instance == null) {
instance = new Elvis();
}
}
}
return instance;
}
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
为什么synchronized加锁之后,还需要在判空一次?假设有A、B 2个线程同时进入到了第一个if判空之中,这时A线程获得锁、B等待,当A完成任务之后,B线程进入加锁代码,但是这个时候由于A已经完成了对象实例化,B线程没有必要再次实例化对象,所以我们进行空判断,B线程可以直接结束。
顺便简单提一下,volatile关键字是做什么的。1、volatile保证可见性,不保证原子性。2、volatile禁止指令重排序。
目前为止,我所知道的实现单例模式就这么多了,但是书中还提到了另外一种实现方式:声明一个包含单个元素的枚举类型。
第五种 单个元素的枚举实现单例模式
// Enum singleton - the preferred approach (Page 18)
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
可以避免多线程带来的问题,因为是枚举类,天然的具备反序列化重新创建对象。但是缺陷也很明显,放弃了类的一些特性,同时也无法延迟加载,最主要的是这样实现的人太少了。我实话实说,我是在看这本书时第一次看见这种写法。
至于书本中描述的除了枚举实现单例的其他方式中,序列化、反序列化带来的问题,和通过反射攻击带来的可以多次实例化对象的问题。我个人感觉考虑的有些过多,一般的项目实现中基本不太需要考虑这方面因素(当然也有可能主要是我等级太低了)。
Rule4 通过私有构造器强化不可实例化能力
有些时候我们往往遇到一种情况,当前类是静态类只提供静态方法,简而言之就是工具类。我们不希望工具类被实例化,因为这样做没有意义。但是如果不显示的声明构造器,编译器会自动提供一个公有的无参构造函数。所以我们往往会显示的声明一个私有的构造器,防止工具类可以被实例化。
例:java.lang.Math、java.util.Arrays、java.util.Collections
本文技术菜鸟个人学习使用,如有不正欢迎指出修正。xuweijsnj