对象创建和构造
- Item1: Consider static factory methods instead of constructors (考虑使用静态工厂方法替代构造器)
- 静态工厂方法示例
- Why?
- limitation
- Item2: Consider a builder when faced with many constructor parameters(拥有很多构造参数时,考虑使用builder进行对象创建)
- Problem:
- How?
- builder模式示例
- Why?
- Item3: Enforce the singleton property with a private constructor or an enum type(单例实现,请把构造器private化或者从枚举获取)
- 示例
- Item4:Enforce noninstantiability with a private constructor(用private构造器达到类不能实例化的目的)
- 场景
- 示例
- Item5:Prefer dependency injection to hardwiring resource(使用依赖注入其他对象的引用而不是直接写死)
- Item6:Avoid creating unnecessary object(避免创建不必要的对象)
- 示例
- Item7: Eliminate obsolete object references (及时释放过期的对象引用,让GC把过期对象回收)
- 场景:
- 优点:
- 适用建议
- Item 8:Avoid finalizers and cleaners(避免使用finalize()和cleaner机制)
- Why?
- Item9:Prefer try-with-resource to try-finally(优先使用try-with-resource去替代try-finally)
- 场景
- Why?
- 好处
Item1: Consider static factory methods instead of constructors (考虑使用静态工厂方法替代构造器)
一般情况下,我们都是使用MyClass object = new MyClass()
,通过new调用构造器的方式来创建对象。这样的方式可以说是拈手而来,轻车熟路。但是我们不妨换个方式,试试使用静态工厂的方式(此工厂方法非设计模式里的工厂方法)来替代构造器,提升代码可读性。
静态工厂方法示例
现考虑如下代码:
class MyClass {
public static MyClass getInstance() {
return new MyClass();
}
}
// client代码:
MyClass object = MyClass.getInstance();
可能通过上述示例你并不觉得使用静态工厂的方式有什么优势,甚至还觉得冗余了代码。那再看下Boolean的实现源码
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
\\client代码
Boolean b = Boolean.valueOf(true);
Why?
为什么会建议使用静态工厂方法?通过额外的静态工厂替代直接调用构造器来创建对象?这样能有什么好处?
优点:
- 静态工厂方法不同于构造器,他具有自己独一无二的名字
这一点在对于拥有多个构造函数的时候,能够极大的提升代码的可读性。试想一下,你可以通过createByName(String name), createBySize(Integer size)
等极具含义的方法创建对象,并且你能通过名字区分每个构造对象之间的差异。而通过new的方式,你根本不能从参数列表里知道每个参数是什么含义。 - 通过静态工厂方法,你可以不用每次都新建一个对象
你甚至可以在静态工厂方法里实现你的单例逻辑,这样每次getInstance()
获取的是同一对象。不必如同new的方法,每次都会创建一个新的对象 - 静态工厂方法可以返回任意的子类型,可以带来极大的灵活性
如java.util.Collections
的实现,里边所有的容器实现均为private,每一种具体的容器都能通过一个static方法获取,根据不同的静态工厂返回不同的具体容器实现。假如使用new,只能返回同一种类型,也就失去了这种灵活性。 - 静态工厂方法还能仅仅返回一种类型,而不用立即返回对象的具体实现
静态工厂方法如同普通的方法,他的返回值可以是一个接口,一个类型,而不一定立即需要实例化的对象,这一点也是new做不到的。具体案例实现参考JDBC。
limitation
当然,静态工厂方法也有他自身的限制。其中最主要的是:
- 一般使用静态工厂方法,会将构造器设为private(不允许外部使用new构造,不然就没意义了)。但是一旦所有的构造器设为private,就意味着这个类不能被继承,因为子类无法调用父类的构造器进行初始化。所以使用静态工厂方法的时候,考虑下是否该类会被其他类继承。
- 对于程序员来讲,难以从文档上区分出哪个是初始化方法,不如构造方法直观(不过这是小问题,可以遵从一些公用规范的名字,例如:form,valueOf,create,getInstance等)
Item2: Consider a builder when faced with many constructor parameters(拥有很多构造参数时,考虑使用builder进行对象创建)
Problem:
经常我们会面临这样一种场景,new MyClass(int a, int b, int c, int d ...)
,如此调用一个构造函数,需要传入很多个参数。更过分的是,假如其中有部分参数,存在默认值,还要根据默认值写多个构造器,这样的代码让人抓狂。第一,当拥有很多构造入参时,使用new创建对象,代码可读性变得很差,别人根本不能知道每个参数的含义是什么,而且很容易因为顺序搞错出现谜之bug。第二,假如多个field拥有默认值,因为java不允许默认参数,所以我们得写多个构造函数,类的实现也会变得很臃肿。
How?
针对于上面的问题,我们怎么去解决?
- Java Bean的方式
MyClass m = new MyClass();
m.setField1(1);
m.setFiled2(2);
m.setFiled3(3);
...
通过新建一个不带参数的默认构造器创建一个对象,然后再后续依次将值set进去,典型的Java Bean的方式。这样虽然能解决代码可读性的问题,但同时也会引入其他问题。第一,将对象的创建和初始化分开,可能会导致可能field值set不完整的情况,无法保证对象的所有属性都成功初始化。第二,不能创建不可变对象,Java Bean模式的前提在于,对象创建后是可变的,可以重新修改他的值。对于不可变对象,这种方式就束手无策了。
- Builder模式
如许多优秀源码实现一样,构建一个Builder来负责对象的创建。通过New MyClass.Builder().filed(a).build()
的方式,传入构造参数并创建对象。
builder模式示例
// Builder Pattern
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;
}
}
使用:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
Why?
为什么要使用builder模式来创建对象?真的能解决多参数的问题吗?
builder优点:
- 可以清晰入参含义,减少构造函数数量。同时大大提升代码可读性
- builder模式可以继承给子类,将Builder定义为abstract,交由子类去实现。
Item3: Enforce the singleton property with a private constructor or an enum type(单例实现,请把构造器private化或者从枚举获取)
我们常常需要将一个对象单例化,来节约资源。如果把单例交给client去实现,会比较难保证,使用double check实现是一种方式,我们也可以在类中实现单例。
示例
public class MyClass {
// 可以通过public,直接MyClass.INSTANCE获取单例
// 但是个人推荐,还是使用下方的静态工厂方式获取
// 这样即使你后期修改这个单例策略,也可以不用变动接口
// public static final MyClass INSTANCE = new MyClass();
private static final MyClass INSTANCE = new MyClass();
private MyClass() {...}
public static MyClass getInstance () { return INSTANCE;}
}
实现单例,可以通过结合静态工厂的方法获取单例对象,同时把构造器私有(保证不能从外部新建对象)。当然结合静态工厂也有弊端,最大限制如Item1所讲,不能进行继承。
Item4:Enforce noninstantiability with a private constructor(用private构造器达到类不能实例化的目的)
场景
有些类,可能都是一些static的field或者方法,并没有示例化的意义。这种情况下,请一定记得构造一个private的构造函数,强迫让该类不能被实例化,例如java.util.Collections,工具类
等。
示例
public class MyUtil {
private MyUtile() {
// 抛异常是为了,防止可能被其他人通过反射无意修改权限后,被调用到
throw new AssertionError();
}
}
Item5:Prefer dependency injection to hardwiring resource(使用依赖注入其他对象的引用而不是直接写死)
这一条不用刻意强调,主流代码基本都是这样写。这里只是记录下,该条说的依赖注入是怎么回事。把需要的引用,在构造器里传入,不是在field上写死。
// 把引用写死,这样不具有灵活性,无法动态传递需要的引用
public class MyClass {
private static final Resource resource = new Resource();
public MyClass () {...}
}
// 依赖注入,可以动态传递不同的Resource类型
public class MyClass {
private Resource resource;
public MyClass ( Resource resource ) {
this.resource = resource;
...
}
}
如果依赖特别多,可能会有成千上万个对象需要注入,这就需要引入Spring, Dagger这些框架去解决了。
Item6:Avoid creating unnecessary object(避免创建不必要的对象)
这条不是要过于限制对象的创建,毕竟面向对象的特性+垃圾回收器,让Java本身就一定程度上鼓励多创建对象,并且效率一般情况下并不是瓶颈。这里的重点,在于怎么去区分不必要,和避免一些暗坑。
注意如下代码
示例
示例一
public boolean isURL (String s) {
// 一个字符串是否匹配邮箱格式
return s.matchs("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$");
}
// 分析matchs源码会发现,整个调用链如下
// matchs --> Pattern.matches --> Pattern.compile(regex) --> new Pattern()
// 可以发现每次都会创建一个Pattern对象
// 而Pattern对象的创建非常耗费资源,需要去适配机器,编译表达式
// 如果在很多次调用的情况下,这里将存在性能瓶颈,因为不断的创建Pattern对象
// 所以考虑复用Pattern对象,大大提升效率
Pattern pattern = Pattern.compile("^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$");
public boolean isURL(String s) {
return pattern.matcher(s).matches();
}
示例二
public Long sum () {
Long sum = 0L;
for (long i = 0; i <= INTERGER.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
// 这段代码也有很大的性能问题,这由于Java的自动装箱和拆箱。
// 我们定义的sum是Long类型,每次+= 操作的时候,会把sum的值拆箱取出成long。
// 和i做完累加后,得到一个long型的结果值,
// 但是sum是Long,Java会自动新建一个Long对象,再把结果的long值塞回去。
// 这样每一次循环,都会进行一次装箱拆箱,都会新建一个Long对象
// 所以这段代码会导致验证的性能问题,
// 我们需要做的修改仅仅是把sum的定义修改成long,就能带来很大的性能提升
long sum = 0L;
所以,在我们有必要重用一个对象的时候,尽量重用已有对象,这样能带来一定的性能提升。
Item7: Eliminate obsolete object references (及时释放过期的对象引用,让GC把过期对象回收)
场景:
下边代码可能会造成内存泄露,看下能不能找出来问题在哪
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
上边代码哪里会造成内存泄露?试想一下,当栈增长又收缩,pop出去的对象,理应是不会再需要了,但是我们的element数组里实际上依然保存有该对象的引用,造成GC并不会去回收该对象。如果栈突然增长到很大,又收缩,那就会有很多的对象无法被回收,可能引起OutOfMemoryError。所以,我们需要及时清理过期的对象引用,确保GC能够回收掉这部分数据
上面stack实现的优化改进:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object reuslt = elements[--size];
element[size] = null; // 及时清除不需要的对象引用
return result;
}
及时null化过期引用
优点:
- 防止过期对象无法回收,造成内存泄露
- 防止过期对象被错误引用,而无法及时发现。把过期引用及时null化,程序会及时抛出NullPointerException,能够防止很多暗坑。
但是也不要过度的去用null手动注销对象,大部分对象还是通过作用域的结束而自动回收,根据实际情况具体分析。
适用建议
那什么时候可能需要注意手动释放过期引用的问题呢?观察上述stack案例
- 有自己管理的内存空间,上述案例的array数组是由stack自己维护,所以GC没办法知晓这部分内存的过期与否。通俗来讲,如果一个class拥有自己的内存,就需要警醒是否需要手动释放
- 另一个常见场景,cache,也可能会造成内存泄露。需要多注意,对象过期后,是否需要手动去释放。
- listener和callback场景,也极有可能造成内存泄露。当你注册了一个回调函数,却忘记注销,会造成对象不断累加,最终导致内存泄露。
Item 8:Avoid finalizers and cleaners(避免使用finalize()和cleaner机制)
在Object里有一个finalize()方法,GC的时候会去调用该方法进行一些操作。但是并不建议重载去使用该方法,并且在Java9中已经将该方法deprecated,用cleaners去替代,虽然减少了部分风险,但依然不可控,仍不建议使用。
Why?
- 该方法并不可控,无法预知在何时会被调用。如果在finalize()里去做一下资源关闭,那得等到GC的时候才会调用,而GC的时间并不可控,因此可能会导致大量的资源占用,得不到及时释放。(释放资源请使用try-with-resource)
- finalize会额外调起一个finalizeThread去处理,导致GC缓慢,严重影响效率。而且在finalize方法里,如果不小心引入了死锁,死循环等,会造成对象无法得到释放,并且很难定位到问题。
Item9:Prefer try-with-resource to try-finally(优先使用try-with-resource去替代try-finally)
正如Item8所讲,我们不能用finalize去关闭资源,但是关闭数据库连接,关闭输入流等很多场景又都需要我们及时去关闭这些资源。这个时候最好的方式就是使用try-finally和try-with-resource,其中更推荐try-with-resource。为啥呢?
看下以下代码:
场景
// try-finally - No longer the best way to close resources! 一个资源看起来还行
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
// This may not look bad, but it gets worse when you add a second resource:
// try-finally is ugly when used with more than one resource! 两个叠加看起来就是一团糟了
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
Why?
看上面示例可以轻易看出,如果有两个资源以上,使用try-finally的代码看起来很冗长,假如再加上catch,那就更看着烦心了。所以解决办法,通过try-with-resource。看看代码
// try-with-resources - the the best way to close resources!
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}
// try-with-resources on multiple resources - short and sweet,多个资源
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
// 加入catch异常
// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
好处
推荐优先使用try-with-resource代替try-finally。能大大提升代码的可读性,使代码更短更可读