(一)前言
在Java应用程序的运行过程中,经历了不断的创建和销毁对象;何时创建对象,如何确保对象能够正常销毁,如何避免创建无用对象对应用程序的性能有着重要的影响;对象创建的滥用往往会造成资源的浪费和性能的降低,无用对象无法正常销毁甚至会造成内存泄露,而在对象销毁时没有做相关资源(网络,输入/输出流)的关闭会造成系统稀缺资源的浪费。
(二)正文
第1条:考虑静态方法代替构造器
一般而言,类要为客户端程序员提供创建对象的方法,最常用的方法是提供公有的构造器,还有一种是提供返回该类的对象的公共静态方法。这里是一个使用静态方法的实例
实例01
interface Animal{
void eat();
void cry();
}
public class Dog implements Animal{
private String name="no name";
//这个私有构造方法不能少
private Dog(){}
private Dog(String name){this.name = name;}
public eat(){}
public cry(){}
public static Dog newDog(){
return new Dog();
}
public static Dog newNameDog(String name){
return new Dog(name);
}
}
静态方法的优点
尽管使用公共构造器构建对象对于类的设计者来说是理所应当的,但是,大部分的类设计者会使用静态方法,这样做有几大优势。
- 静态方法的方法名能明确的描述要创建什么样的对象。
公共构造器的方法名与类同名,而通过参数并不能想客户端程序员传达必要的信息。例如,构造器Dog(String)
,通过这种方式创建对象,客户端程序员并不知道要创建什么样的Dog类,而newNameDog(String)
明确的说明是具名Dog对象。
更进一步的,当一个类的构造器过多时,会有参数相同而参数顺序不同的构造器,在这样的构造器中,客户端程序员很难区分彼此。而静态方法能通过命名,明确告知。 创建单体类(Singleton)
对于所有域相同的不可变类,创建多个是没有意义的,而且会造成性能的下降。我们可以通过静态方法,预先构建实例或者将构建好实例并缓存下来,例如Boolean.valueOf(boolean)
就不会重新创建对象。
像静态方法这种总是返回同一个对象的方法,能在某时刻严格控制对象的存在,这种类叫做实例受控类(instance-control),这常用于单体设计模式中。
注:单体类的一个优点是,比较两个对象时候相等时可以直接使用==
代替equals()
,从而提升性能。Ps: 静态方法并不是创建单体类的唯一方法,却是最好的一种方法,至于创建类的其他方法,以后有机会再探讨。
静态方法可以返回原类型的子类型(或者接口类的实现类),更加灵活
在实例01
中,我们可以把静态方法的返回值做下修改:
public static Animal newInstance(){
return Dog();
}
这种方法非常适用于基于接口的框架(具体的实现在后序探讨)。
例如,Java Collentions Framework
的集合接口有32个便利实现,分别提供了不可修改的集合,同步集合等。而相关的对象都在java.util.Collentions
中通过静态方法导出,而这些对象本身全部是非公有的。
静态方法返回的对象的类型,还能每次随着调用而发生变化,这可以通过静态方法的参数值实现,只要是已经声明的返回类型的子类型都是可以的。这样的好处是增加了扩展性,API的设计者可以在不改变原有框架的情况下添加新的私有扩展新功能或者提高性能。
静态方法可以通过其参数返回在编写静态方法时还不存在的类,这里有点抽象,现在举个例子,在实例01
中我们可以定义这样的静态方法
//为了简单化,这个实例省略了注册部分,而是直接把Animal传给了静态方法,实际情况并不是这样的
public static Animal newMyInstance(Animal a){
return a;
}
这里客户端程序员完全可以自己创建一个Cat类传进去。
这种静态方法构成了服务提供者框架的基础(Service Provider Frameword),
例如JDBC就是这种框架的产物,它的大概意思多个服务提供者实现一个服务(例如 mysql,oracle,sqlserver同时为java.sql.Driver
接口提供具体数据库驱动实现)并把客户端从多个实现中解耦出来。
服务提供者的四个组件是:服务接口(Connection
),提供者注册API(DiverManager.registerDriver
),服务访问API(DiverManager.getConnection
),服务提供者接口(java.sql.Driver
由mysql,oracle,sqlserver实现),如果没有服务提供者接口,实现就按照名称注册,并通过反射方式实例化。一般情况下服务访问API会让用户指定哪个服务提供者,如果不指定就是默认的。
原文(P7)中一个关于服务提供者框架的例子:
实例02
//服务提供者框架模式
//服务接口
public interface Service{}
//服务提供者接口
public interface Provider{
Service newService();
}
//服务的注册和访问
public class Services{
private Services();
//建立Service的集合
private static final Map<String,Provider> providers = new ConcurrentHashMap<String,Provider>();
public static final DEFUALT_PROVIDER_NAME = "<def>";
//注册API
public static void registerDefaultProider(Provider p){
registerProvider(DEFAULT_PROVIDER_NAME,P);
}
public static void registerProvider(String name,Provider p ){
provider.put(name,p);
}
//访问API
public static Service newInstance(){
return newInstance("DEFAULT_PROVIDER_NAME");
}
public static Service newInstance(String name){
Provider p = Provider.get(name);
if(p ==null)
throw new IllegalArgumentException("No provider registered with name:" +name);
return p.newService();
}
}
静态方法的缺点
1.由静态方法创建的类由于没有公共构造器,无法被继承
2.和其他静态方法无法区分
静态方法的一些惯用的名称:
- valueof()——类型转换方法
- of——等同于valueof(),常用于EnumSet中
- getInstance——返回的实例通过参数确定,既可以是singleton也可以是新对象
- newInstance——与getInstance一致,但每次都会返回新的对象。
- getType——与getInstance相似,创建非静态方法所在类时使用。
- newType——与newInstance相似,创建非静态方法所在类时使用。
第2条:构造器参数过多时可以考虑使用Builder
重叠构造模式
对于大量可选参数组成的构造器,程序员一般使用重叠构造器模式,在这种构造器中第一个是只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数,以此类推。最有一个构造器包含所有可选参数,如下例:
实例03
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(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;
}
}
当你要创建实例时,就用最短的构造器,但是若最后一个可选参数需要设置,中间的可选参数缺省时,你仍然要传递值。
例如:
NutrtionsFacts coca = new NutritionFacts(224,8,0,0,27);
随着可选参数的数量增加,这种方法很难适用。
JavaBeans模式
这种方法使用无参构造器创建对象,并用setter()方法,设置每个需要设置的参数,如下例:
实例04
public class NutritionFacts {
//所有参数
private int servingSize= -1;
private int servings = -1;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private 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不能创建不可变类,同时,这种方法也不是线程安全,如果多个调度同时起作用会造成数据的不一致性。
Builder模式
Builder既有重叠构造器的安全性又有JeanBeans的易用性。这种方法不直接生成想要的对象,而是通过客户端利用Builder构造器(带有必要参数),得到一个Builder对象,通过Builder的setter方法,先把参数传递给Builder对象,最后利用一个无参构造器生成NutritonFacts的不可变对象,如下例:
实例05
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 {
// 必要参数
private final int servingSize;
private final int servings;
// 可选参数
private int calories= 0;
private int fat= 0;
private int carbohydrate= 0;
private int sodium= 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 carbohydrate(int val)
{ carbohydrate = val;return this; }
public Builder sodium(int val)
{ sodium = 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;
}
}
客户端代码如下:
//注意setter方法返回Builder本身
NutritonFacts coca = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).builder();
Builder模式的优势在于可以有多个可变参数,另外builder可以创建多个对象,也可以自动填充某些域,例如自增序列号。
builder还可以传递给客户端的方,为客户端创造多个对象,就像抽象工厂,例如:
public interface Builder<T>{
public T builder();
}
可以让NutritionFacts.Builder类实现Builder接口,以供客户端使用。
注:builder的类型参数可以使用通配符来约束,例如:
Tree builderTree(Builder<? extends Node> nodeBuilder){...}
builder也弥补了Class.newInstance()作为抽象工厂实现在调用无参构造器的不足(以后再讨论)。
builder模式的优势
易于扩展,容易编写和阅读,创建的不可变类线程安全。
builder模式的不足
builder模式需要创建Builder对象,这增加了系统开销,
第3条:用私有构造器或者枚举类型强化Singleton属性
实现Singleton的常用方法
- 公有静态final成员域
public class Elvis{
public static final Elvis Instance = new Elvis();
private Elvis(){}
public void leaveTheBuilding(){}
}
- 静态方法
public class Elvis{
private static final Elvis Instance = new Elvis();
private Elvis(){}
public static Elvis getInstance(){
return Instance;
}
public void leaveTheBuilding(){}
}
两种方法的优劣
- 使用公有静态域的好处是更加清晰的表明总是包含相同对象的引用,更容易让用户辨认出singleton类,但是就性能方面公有静态域和静态方法几乎不会有差别(因为JVM能使静态方法的调用内联化)。
- 静态方法提供了灵活性,可以在不改变API的前提下,改变类的Singleton性,
- 静态方法的另一个优势是泛型(第27条)
- 关于Singleton序列化的问题
为了维护并保持Singleton,必须声明所有实例域都是不被序列化的(transient)并提供一个readResolve方法(第77条),否则每次序列化实例时都会创建新的实例。下面是在singleton中实现readResolve方法:
private Object readResolve(){
//返回真的Evis给垃圾回收器
return INSTANCE;
}
Ps:关于transient——Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想 用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。 transient是Java语言的关键字,用来表示一个域不是该对象序列化的一部分。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。
通过枚举实现singleton
public enum Elvis{
INSATNCE;
public void leaveTheBuilding(){}
这种方法更加简洁,同时提供了序列化机制,绝对防止多次实例化,已经被广泛的应用。
第4条:通过私有构造器强化不可实例化的能力
通常会有一些类质保含静态方法和静态域,例如java.util.Arrays把与数组相关的方法组织起来,java.util.Collections把特定接口的对象上的静态方法组织起来,java.lang.Math把算术运算的静态方法组织起来。这通常称为工具类。
显然,这些类是不希望被实例化的,所以API设计者应该设计一种途径禁止客户端程序员实例化他们。一种方法是为这些类加私有构造方法,使它们不会被客户端程序员实例化。
public class UtilityClass{
//私有构造器
private UtilityClass(){
//防止类内部不小心调用了它
throw new AessertionError();
}
}
第5条:避免创建不必要的对象
一般来说,最好能重用对象而不是每次需要的时候就创建一个形同功能的新对象。
不可变类的重用
这里有一个错误的例子:
while(true){
String str = new String("helloworld");
}
String是不可变类,而在每次循环时,都要创建一个相同的类,这是不必要的,比较好的方法是使用字符串赋值而不是使用构造器:
while(true){
String str = "hello world";
}
注:像String这样即提供静态方法又提供公共构造器的不可变类通常使用静态方法创建类,类似的还有Boolean
可变类的重用
可变类的重用,通常是那种创建之后就不会在改变状态的类(是一般不会不是不能哦!!),看下面这个例子:
实例06
public class Person {
private final Date birthDate;
public boolean isBabyBoomer() {
Calendar gmtCal =Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&birthDate.compareTo(boomEnd) < 0;
}
}
在这里每次调用isBabyBoomer()都会创建Calendar和两个Date,但是这是不必要的,每次创建的Date是相同的作用也是一样的。我们可以使用静态初始化器,使仅仅在类被加载时创建对象,而每次调用共享这种对象:
实例07
class Person {
private final Date birthDate;
//私有静态域
private static final Date BOOM_START;
private static final Date BOOM_END;
//静态初始化器
static {
Calendar gmtCal =Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
}
}
这种静态初始化器的缺点是,初始化是在类加载时进行的,如果isBabyBoomer()
永远不被调用哪?这样静态初始化器创建的对象反而占用了资源,造成了性能的下降,这种情况可以使用延迟初始化技术(第71条)。
注:延迟初始化是非常复杂的,这样做并不能是性能提升,建议不要这样做。
适配器(视图)的重用
适配器是这样一种对象,它把功能委托给一个后备对象,从而位后背对象提供一个可以代替的接口。
由于适配器除了后备对象外没有其他信息,所以针对某个特定对象的后备对象而言,不需要创建多个适配器。
例图Map接口的keySet()方法返回一个Map对象的Set视图,其中包含该Map中所有的键(key)。这里不需要每次调用keySet()创建一个Set对象,因为这些对象都是基于同一个Map实例的,因而是相同的。
自动装箱中多余对象的创建
自动装箱是由于基本类型和基本类型对应对象混用而引起的自动装箱和拆箱。我们看一段代码:
public long AllIntSum(){
//注意这里用的是Long对象
Long sum = 0L;
for(long i=0;i<Integer.Max_VALUE;i++)
sum += i;
return sum;
}
由于不小心把long写成了Long每次循环都会创建一个Long实例,改程long后性能得到提升。
注:要优先使用基本类型而不是基本类型对象。
Ps:我们提倡对象的复用并不代表创建对象的代价非常昂贵,相反小对象的创建是非常廉价的,通过附加对象而提升程序的清晰性,简洁性和功能性通常是个好事。相反通过自建对象池来避免创建对象并不是一种好的做法,除非对象资源是非常稀缺和昂贵的,例如数据库连接池。而JVM提供的高级优化的垃圾回收器,其性能很容易超过轻量级对象池的性能。
对象复用绕不开的话题——保护性拷贝
当要实现保护性拷贝时对象复用的代价远远大于因创建重复对象而付出的代价。而没有实施保护性拷贝时会造成潜在的错误和安全漏洞。
第6条:消除过期的对象引用
JVM的垃圾回收机制使程序员不用自己创建析构函数回收无用的对象,然而是不是Java中就不存在内存泄露了哪?是不是我们就不用考虑内存管理了哪?
我们来看个例子:
实例08
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];
}
//扩展数组保证能装下stack
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
这是一个stack的实现,这个stack是基于数组构建的,这里隐藏这一个问题,在pop()方法中elements[--size]
被认为是出栈,但是在数组中的对象引用却没有消除,造成了内存泄露,随着程序的运行这种程序的性能的降低会逐渐表现出来,在极端情况下会造成磁盘交换,甚至导致OutOfMemoryError。
这类问题的修复方法是将过期的对象引用置空。上面stack可以做如下的修复。
Object result = elements[--size];
element[size] = null;
清空对象引用并不是一种规范行为,最好的方法是把包含改引用的变量结束其生命周期,有效的方法是最紧凑范围内定义每一个变量(第45条)。
支持垃圾回收的语言中,内存泄露是很隐蔽的。如果一个对象被无意识保存下来,垃圾回收器不仅不会回收这个对象,而且不会处理这个对象所引用的其他对象,即使少量这样的对象被保存下来也会造成许许多多对象无法回收,对象性能造成了潜在的重大影响。
那么为什么Stack类造成了内存泄露哪,因为它在试图在逻辑上模拟自己管理内存。存储池包含了elements数组的元素。数组活动区域的内存是已分配的,数组其余部分的元素是自由的,但是垃圾回收器并不知道这种情况,所以程序员应当把这种情况告诉垃圾回收器,最简单的方法就是清空这些数组元素。
注:只要自己管理内存就应该警惕内存泄露。
java中内存泄露的情况
- 无意识的保持对象。
如实例08中stack的情况。 - 缓存
一旦你把对象放在缓存中,很容易忘掉它 ,而对象会在用完后很长一段时间内仍留在内存中。
这里我们可以用WeakHashMap代表缓存,当缓存过期之后,就会自动被删除。 监听器和回调
如果你实现了一个API,客户端在这个API上注册回调,却没有显式的取消注册,如果不采取性能就会聚集。
确保回调被当做垃圾回收的最佳方式是只保存他们的弱引用。例如WeakHashMap本地方法中的内存泄露
Java 通过native关键字建立的方法声明,没有方法体,而是有本地系统的方法库提供方法体。本地方法是调用其他语言程序的一个入口。所以会因为其他语言中造成内存的泄露。
内存泄露的危害
内存泄露通常不会表现出明显的失败,所以往往会在程序中运行多年,所以只能通过仔细检测代码,或者借助于Heap剖析工具才能发现问题。
第7条:避免使用终结方法
finalizer()方法是在对象被回收前调用的方法,由于垃圾回收的不确定性,这个方法的调用是不可预测的。
Ps:Java语言规范不仅不保证终结方法会被及时执行,而且根本就不能保证它会被执行,当一个程序终止时,某些已经无法访问的对象的finalize()方法甚至根本没有被执行。
为了终止对象中封装的资源,一般要自建终止方法,并在try-finally中调用,这样可以保证即使在使用对象时抛出异常,改终止方法也会执行。下面是个例子:
Foo foo = new Foo();
try{
}
finally{
foo.terminate();
}
finalize()的应用场景
- 充当安全网
当自建终结方法因某种原因没能调用时,finalize()可以作为一种备份保险机制。 - 终结本地对象
本地对象是由普通对象的本地方法委托的,在垃圾回收器回收了普通对象后本地对象由于并不是java的一部分因此不会被垃圾回收器回收这是就可以在普通对象的finalize()方法中回收本地对象,当然如果本地对象有必须被及时终止的资源时,仍应该使用显式的终止方法,这个终止方法既可以是本地方法,也可以调用本地方法。
确保终结链执行
finalize()链并不会自动执行,因此在当子类调用finalize时,超类的finalize()并不会自动调用,这样就要求手动调用超类,下面是个例子:
protected void finalize() throws Throwable{
try{
}
finally{
super.finalize();
}
}
另一种方法是采用终结方法守卫者,就是位每一个超类加一个匿名类,这个类只负责外围实例的finalize(),由于外围实例保存着终结方法守卫者的唯一引用,因此终结方法守卫者会和外围对象一起被销毁,当守卫者终结时会调用finalize()它会执行外围实例所期望的终结行为。实现如下面例子:
public class Foo{
private final Object finalizerGuarding = new Object(){
protected void finalize() throws Throwable{
}
};
}
(三)后记
Java中对象的创建关系对象何时创建,如何创建,是否有必要创建等,往往涉及的是程序的性能和API的易用性。对象的销毁考虑的是如何正确的清除无用对象,避免内存泄露和程序错误。总之,对象的创建和销毁无论对于类的设计这还是客户端程序员都是指的研究的,而符合一定的规范是相当必要的。