如何更好的创建Java对象

静态工厂

除了使用构造函数创建对象外,还可以使用静态工厂来创建对象,JDK中大量使用了这种技巧,例如:

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

静态工厂的优势

使用静态工程有几大优势,具体如下:

静态工厂有名称

当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造函数,并且慎重地选择名称以便突出他们之前的区别。

相比之下,构造函数只能有一个名称,如果要接受不同的参数,需要进行重载,面对这样的API,用户很难记住还用哪个构造函数去创建一个对象,也经常调用错误的构造器,如果在遇到没有参考文档的情况,往往不知从何入手。

不必每次都创建一个新的对象

静态方法可以重复调用,返回同一个对象,这种类被称为实例受控的类(instance-controlled)。通过这种技术衍生出来的一个设计模式就是单例模式(Singleton)。

关于单例模式的实现可以参考:https://my.oschina.net/u/2450666/blog/688416

可以不返回对象本身的类型

这句话比较绕,但解释起来很简单,意思是,静态工厂可以返回要返回的对象的父类或接口。

也就是说,可以要求客户端调用者使用接口来引用(接收)静态工厂返回的类型,而不是通过它的实现类来引用被返回的对象,这是一个好习惯,也就是我们常说的,面向接口编程。

虽然构造函数也可以做到这一点,但是,静态工厂可以根据接收的参数返回不同的类型,这点是构造函数做不到的,比如:

public static ColorInterface createInstance(int type) {
    if (type == 0) {
        return new Red();
    } else {
        return new Green();
    }
}

而且,如果有新的类型需要返回,也可以添加到静态工厂中。

创建对象的语法更简洁

在调用泛型对象时,调用构造函数通常需要编写两次泛型类型:

Map<String, List<String>> m = new HashMap<String, List<String>>();

这种方式在JDK1.7中得到类优化,加入了类型推导,在1.7中不需要再编写两次泛型类型:

// jdk1.7+
Map<String, User> m = new HashMap<>();

也可以使用静态工厂让其创建变得简单,并且与JDK版本无关:

Map<String, List<String>> m = Factory.newInstance();

静态工厂的不足

与其他静态方法没有任何区别

静态方法没有像构造函数那样在API中明确标识,因此,如果要用静态工厂实例化一个类,需要特意去了解API文档,但是使用一些命名规范,可以弥补这个劣势,这些规范如下:

valueOf:该方法返回的实例与它的参数具有相同的值,目的是做类型转换;

of:是valueOf的一种更简洁的替代;

getInstance:返回单例对象;

newInstance/createInstance:创建一个实例,相当于new一个对象;

构建器

静态工厂与构造函数有一个共同的局限性:创建对象时,不能很好地扩展大量的可选参数。

举例来说,在创建一个有20个域(字段)的对象,其中有3个域是创建时必须要初始化的,另外17个是选填初始化的,那么就要进行大量的构造函数重载,提供一个3个参数(3个必填域)的构造函数,虽然重载一个4个参数(3个必填域,1个可选域),随后提供一个5个参数的、6个参数的……

重载构造函数虽然可行,但是类的代码很难写,并且难以阅读与维护。

由此需求,Builder设计模式就诞生了。它不直接生成想要的对象,而是让API使用者利用所有的必填参数来创建一个建造者对象。然后利用建造者对象的setter方法去初始化选填参数。最后调用建造者的build方法来生成对象。

实战经验

很多Web工程都创建了一个返回值对象,用于向前端返回信息,其结构如下:

public class ResponseBean() {
    //  状态码,例如:0代表成功,1代表异常
    private int code;
    //  要返回的信息
    private String message;
    //  要返回的数据
    private Object data;
}

上面的类中,在创建ResponseBean对象时,只有code是必填的,message和data都是选填的,这种情况就可以使用Builder模式进行对象的创建(例子中只有3个字段,考虑字段更多的情况)。

分享一个实现,具体如下(可以将WebAppResult设计成泛型类):

package cn.yesway.pms.entity;

import cn.yesway.pms.enums.ResultStatus;

/**
 * Web应用的响应数据实体对象。<br>
 * 包含了与前端约定的属性。
 */
public class WebAppResult {

	// 响应状态
	// 不是http的响应状态,是业务系统的响应状态
	// 业务通过返回Success
	// 业务不通过(例如,登录失败等)返回Fail
	private final ResultStatus status;
	// 发送给前端的信息
	private final String msg;
	// 发送给前端的json数据
	private final Object data;

	private WebAppResult(Builder builder) {
		this.status = builder.status;
		this.msg = builder.msg;
		this.data = builder.data;
	}

	/**
	 * WebAppResult的构造器。
	 */
	public static class Builder {

		private final ResultStatus status;
		private String msg;
		private Object data;

		public Builder(ResultStatus status) {
			this.status = status;
		}

		public Builder msg(String msg) {
			this.msg = msg;
			return this;
		}

		public Builder data(Object data) {
			this.data = data;
			return this;
		}

		public WebAppResult build() {
			return new WebAppResult(this);
		}
	}

	/**
	 * @return 获得响应状态
	 */
	public ResultStatus getStatus() {
		return status;
	}

	/**
	 * @return 获得响应消息
	 */
	public String getMsg() {
		return msg;
	}

	/**
	 * @return 获得响应数据
	 */
	public Object getData() {
		return data;
	}

}

创建WebAppResult实体如下:

// 没有data
return new WebAppResult.Builder(ResultStatus.SUCCESS).msg("操作成功").build();

这些编写,可以让代码很容易的编写,而且易于阅读。

条件检测

将参数从builder拷贝到对象后,对象域可以对这些参数进行检查,如果失败可以抛出IllegalStateException异常。在setter时也可以加入条件约束,失败抛出IllegalArgumentException。在set阶段就会检查条件,而不是等到调用build方法时。

存在的不足

为了创建对象,必须创建Builder。如果十分注重性能,可能就会造成问题。

一般在需要用很多参数去构造一个对象时,比如4个参数或更多,使用Builder更为合适。

简而言之,如果类的构造函数或者静态方法中具有多个参数,设计这种类时,Builder模式就是一种不错的选择。特别是大多数参数都是可选的时,相比构造函数和静态工厂,Builder模式更易于阅读和编写。

创建不可实例化的类

有时候可能需要编写只包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象语言中滥用这样的类。尽管如此,他们也确实有他们特有的用处。例如java.lang.Math和java.util.Arrays。

这样的工具类不希望被实例化,实例对它没有意义。然后在默认情况下,编译器会提供一个公有的、无参数的构造函数。我们可以将类的构造函数显示实现为private的,来防止类的实例化:

public class Util {
    private Util() {
        throw new AssertionError();
    }
}

AssertionError不是必须的,它是防止在Util类的内部,在任何情况下都不会实例化该类。

避免创建不必要的对象

一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的对象。重用方式既快速,又流行。如果对象是不可变的(immutable),它就始终可以被重用。

对用同时提供类静态工厂和构造函数的不可变类,通常可以使用静态工厂而不是构造函数,以避免创建不必要的对象,例如:

// 推荐
Boolean.valueOf(String);
// 不推荐
Boolean(String)

有些时候,对象并不一定是不可变对象,这时候可以考虑适配器模式(adapter),有时也叫视图模式(view)。

它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例,关于适配器模式的信息可以参考:https://my.oschina.net/u/2450666/blog/725357

避免创建不必要的,不等于尽量避免创建对象,由于小对象的构造器只做了少量工作,它的创建和回收是特别廉价的,特别在现代的JVM上更是如此。通过创建对象提升程序的清晰性、简洁性和功能性是件好事。关于对象的分配和回收,可以参考:https://my.oschina.net/u/2450666/blog/1573087

过期的对象引用

过期引用很容易造成内存泄漏,在支持垃圾回收的语言中,内存泄漏是很隐蔽的,这类问题的修复方法很简单,一旦对象引用已经过期,只需要清空这些引用即可。

清空对象引用应该是一种例外,而不是一种规范行为。

一般而言有三个常见问题会造成内存泄漏:

第一个常见问题,只要类的自己管理内存的,就要警惕内存泄漏,解决方法是一旦对象被释放,就清空这个对象内包含的任何引用;

第二个常见问题,来自于缓存的内存泄漏,解决方法是定期清除过期的缓存;

第三个常见问题,来自于监听器和回调器,解决方法是确保手动取消注册,并在监听器中将注册的对象保存在弱引用(week reference)中;

避免使用finalizer

终结方法一般情况下是不必要的。Java中可以采用try-finally来完成类似的工作。

终结方法不能确保被及时的调用,而且在不同的JVM实现中,调用终结方法的时间点大不相同,而且终结方法的线程优先级不高。Java语言规范不仅不保证总结方法会被及时的调用,而且根本就不保证他们会被执行。

结论为:不应该依赖终结方法来更新重要的状态。

避免使用终结方法,只需要提供一个显示的终止方法(自定义),并要求客户端在每个实例不再有用时调用这个方法,例如InputStream、OutputStream、Connection等都提供类close()方法。他们通常是配合try-finally结构结合起来使用的。

终结方法的用途有两种:

第一种,当客户端忘记调用上面提到的显示终止方法时,充当“安全网”(safety net),使用这种方法,强烈建议在终结方法中,发现资源未终止,必须在日志中记录一条警告。

第二种,在使用native有关的操作时,JVM不知道需要回收这些native object,在native object不拥有关键资源的前提下,在终结方法中回收它。

注意,如果子列覆盖类父类的终结方法,需要手动调用父类的终结方法,否则父类的终结方法永远不会执行。

再次强调,除了以上两种情况,否则请不要使用终结方法。

猜你喜欢

转载自my.oschina.net/u/2450666/blog/1630129
今日推荐