《Effective Java》——创建和销毁对象

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?

为什么会建议使用静态工厂方法?通过额外的静态工厂替代直接调用构造器来创建对象?这样能有什么好处?
优点:

  1. 静态工厂方法不同于构造器,他具有自己独一无二的名字
    这一点在对于拥有多个构造函数的时候,能够极大的提升代码的可读性。试想一下,你可以通过createByName(String name), createBySize(Integer size)等极具含义的方法创建对象,并且你能通过名字区分每个构造对象之间的差异。而通过new的方式,你根本不能从参数列表里知道每个参数是什么含义。
  2. 通过静态工厂方法,你可以不用每次都新建一个对象
    你甚至可以在静态工厂方法里实现你的单例逻辑,这样每次getInstance()获取的是同一对象。不必如同new的方法,每次都会创建一个新的对象
  3. 静态工厂方法可以返回任意的子类型,可以带来极大的灵活性
    java.util.Collections的实现,里边所有的容器实现均为private,每一种具体的容器都能通过一个static方法获取,根据不同的静态工厂返回不同的具体容器实现。假如使用new,只能返回同一种类型,也就失去了这种灵活性。
  4. 静态工厂方法还能仅仅返回一种类型,而不用立即返回对象的具体实现
    静态工厂方法如同普通的方法,他的返回值可以是一个接口,一个类型,而不一定立即需要实例化的对象,这一点也是new做不到的。具体案例实现参考JDBC。
limitation

当然,静态工厂方法也有他自身的限制。其中最主要的是:

  1. 一般使用静态工厂方法,会将构造器设为private(不允许外部使用new构造,不然就没意义了)。但是一旦所有的构造器设为private,就意味着这个类不能被继承,因为子类无法调用父类的构造器进行初始化。所以使用静态工厂方法的时候,考虑下是否该类会被其他类继承。
  2. 对于程序员来讲,难以从文档上区分出哪个是初始化方法,不如构造方法直观(不过这是小问题,可以遵从一些公用规范的名字,例如: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?

针对于上面的问题,我们怎么去解决?

  1. Java Bean的方式

    MyClass m = new MyClass();
    m.setField1(1);
    m.setFiled2(2);
    m.setFiled3(3);
    ...
    

    通过新建一个不带参数的默认构造器创建一个对象,然后再后续依次将值set进去,典型的Java Bean的方式。这样虽然能解决代码可读性的问题,但同时也会引入其他问题。第一,将对象的创建和初始化分开,可能会导致可能field值set不完整的情况,无法保证对象的所有属性都成功初始化。第二,不能创建不可变对象,Java Bean模式的前提在于,对象创建后是可变的,可以重新修改他的值。对于不可变对象,这种方式就束手无策了。

  2. 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优点:

  1. 可以清晰入参含义,减少构造函数数量。同时大大提升代码可读性
  2. 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化过期引用

优点:
  1. 防止过期对象无法回收,造成内存泄露
  2. 防止过期对象被错误引用,而无法及时发现。把过期引用及时null化,程序会及时抛出NullPointerException,能够防止很多暗坑。

但是也不要过度的去用null手动注销对象,大部分对象还是通过作用域的结束而自动回收,根据实际情况具体分析。

适用建议

那什么时候可能需要注意手动释放过期引用的问题呢?观察上述stack案例

  1. 有自己管理的内存空间,上述案例的array数组是由stack自己维护,所以GC没办法知晓这部分内存的过期与否。通俗来讲,如果一个class拥有自己的内存,就需要警醒是否需要手动释放
  2. 另一个常见场景,cache,也可能会造成内存泄露。需要多注意,对象过期后,是否需要手动去释放。
  3. listener和callback场景,也极有可能造成内存泄露。当你注册了一个回调函数,却忘记注销,会造成对象不断累加,最终导致内存泄露。

Item 8:Avoid finalizers and cleaners(避免使用finalize()和cleaner机制)

在Object里有一个finalize()方法,GC的时候会去调用该方法进行一些操作。但是并不建议重载去使用该方法,并且在Java9中已经将该方法deprecated,用cleaners去替代,虽然减少了部分风险,但依然不可控,仍不建议使用。

Why?
  1. 该方法并不可控,无法预知在何时会被调用。如果在finalize()里去做一下资源关闭,那得等到GC的时候才会调用,而GC的时间并不可控,因此可能会导致大量的资源占用,得不到及时释放。(释放资源请使用try-with-resource)
  2. 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。能大大提升代码的可读性,使代码更短更可读

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值