首页 > 嗟来之食 > 和我一起学Effective Java之创建和销毁对象 – JohnTsai –
2016
07-04

和我一起学Effective Java之创建和销毁对象 – JohnTsai –

前言
主要学习创建和销毁对象:

1.何时以及如何创建对象
2.何时以及如何避免创建对象
3.如何确保它们能够适时地销毁
4.如何管理对象销毁之前必须进行的清理动作

正文
一、用静态工厂方法代替构造器
获取类的实例的常用方法有:

1.公有的构造器
2.公有的静态工厂方法

下面通过Boolean类(基本类型boolean的包装类)的简单示例来学习:
//公有的构造器
public Boolean(String s) {
this(parseBoolean(s));
}
//公有的静态工厂方法
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
静态工厂方法相对于构造器的优势:
1.有名称
具有适当名称的静态工厂方法更易使用,产生的代码更易阅读。可用名称来突出它们之间的区别。

如构造器BigInteger(int,int,Random)返回的BigInteger可能为素数,若用名为BigInteger.probablePrime的静态工厂方法来表示,显得更加清楚。

2.不必每次调用它们的时候都创建一个新对象
不可变类可以使用预先构建好的实例,或者是将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。

例如之前的Boolean的静态工厂方法就说明了这项技术,这种方法类似于FlyWeight模式。如果程序经常请求创建相同的对象,并且创建对象的代价很高,这项技术极大地提升了性能。

//缓存起来的Boolean实例
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);
}
静态工厂方法为重复的调用返回相同的对象。
3.可返回原返回类型的任何子类型的对象
4.创建参数化类型实例时,可使代码变得更加简洁

由于《Effective Java》这本书在编写的时候,JDK1.7还没有推出,因此在调用参数化类的构造器时,类型参数都必须要指明。而静态工厂方法能替我们实现类型推导的功能。

//JDK7之前
Map<String,List<String>> map = new HashMap<String,List<String>>();
//JDK7
Map<String,List<String>> m = new HashMap<>();
//使用静态工厂方法
public class MyHashMap extends HashMap {
public static <K,V> HashMap<K,V> newInstance(){
return new HashMap<K,V>();
}

//静态工厂方法实现类型推导
Map<String,List<String>> m = MyHashMap.newInstance();
}

静态工厂方法的缺点:

1.类如果不含公有的或受保护的构造器,就不能被子类化。
2.静态工厂方法和其他的静态方法实际上没有任何区别。

/*
*静态工厂方法的惯用名称 
*/
//valueOf:返回的实例与它的参数具有相同的值
//String.valueOf(int)方法
public static String valueOf(int value) {
return Integer.toString(value);
}

//of:valueOf的简洁版
//EnumSet.of(E)方法
public static <E extends Enum<E>> EnumSet<E> of(E e) {
EnumSet<E> set = EnumSet.noneOf(e.getDeclaringClass());
set.add(e);
return set;
}

//getInstance:返回的实例是通过方法的参数来描述的。
//newInstance:返回的每个实例与其他的所有实例不同。
//getType:Type表示工厂方法返回的对象类型
//newType:与newInstance类似

二、多个构造器参数考虑用构建器
静态工厂和构造器的局限性:不能很好地扩展到大量的可选参数
对于有大量的参数的类的编写:
/**
*重叠构造器模式
*提供一个只有必要参数的构造器,第二个有一个可选参数,第二个有两个,依此类推。
*/

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;
}
}

此方法虽可行,但如果有许多参数时,客户端代码很难编写,并且难以阅读。
/**
*JavaBeans模式
*无参构造方法创建对象,使用setter方法设置必选或可选参数。
*/
public class NutritionFactsWithJavaBeans {
/**
* 必选元素
*/
private int servingSize;
private int servings;
/**
* 可选元素
*/
private int calories;
private int fat;
private int sodium;
private int carbohydrate;

/**
* 无参构造方法
*/
public NutritionFactsWithJavaBeans(){

}
/**
* Setter方法
*/
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}

public void setServings(int servings) {
this.servings = servings;
}

public void setCalories(int calories) {
this.calories = calories;
}

public void setFat(int fat) {
this.fat = fat;
}

public void setSodium(int sodium) {
this.sodium = sodium;
}

public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}

public static void main(String[] args) {
NutritionFactsWithJavaBeans cocaCola =
new NutritionFactsWithJavaBeans();

cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
JavaBeans模式创建实例容易,代码易读。但有很严重的缺点:

1.构造过程中可能出现不一致的状态,调试困难。
2.因为有set方法,使得在JavaBeans模式中,不能将类变为不可变的,需要额外的努力来确保它的线程安全。

重叠构造器的安全性+JavaBeans模式的可读性====>Builder模式

易写易读,模拟了具名的可选参数。build方法可检验约束条件。
public class NutritionFactsWithBuilder {
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 sodium = 0;
private int carbohydrate = 0;

public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}

public Builder calories(int calories) {
this.calories = calories;
return this;
}

public Builder fat(int fat){
this.fat = fat;
return this;
}

public Builder sodium(int sodium){
this.sodium = sodium;
return this;
}

public Builder carbohydrate(int carbohydrate){
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFactsWithBuilder build(){
return new NutritionFactsWithBuilder(this);
}
}

private NutritionFactsWithBuilder(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) {
NutritionFactsWithBuilder nutritionFactsWithBuilder
= new NutritionFactsWithBuilder.Builder(20,30).calories(3).build();
}

}

用私有构造器或枚举类型强化Singleton属性
Singleton表示仅仅被实例化一次的类。
实现Singleton的方法:

1.public static final域实现

public class SingletonClazz{
//public属性将本类唯一实例暴露出去
public static final SingletonClazz INSTANCE = new SingletonClazz();
//构造方法私有 保证全局唯一性
private SingletonClazz(){
}
public void test(){
System.out.println("test method");
}
}
缺点:利用反射机制可调用到私有构造方法
try {
Constructor<SingletonClazz> constructor = SingletonClazz.class.getDeclaredConstructor();
constructor.setAccessible(true);

SingletonClazz clazz = constructor.newInstance();
clazz.test();
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}

如何防止调用私有构造方法,修改构造方法,在创建第二个实例的时候抛出异常即可。
private SingletonClazz(){
if(INSTANCE!=null)
throw new RuntimeException("cannot create more than one instance of SingletonClazz");
}

2.静态工厂方法实现

public class SingletonClazz {

private static final SingletonClazz INSTANCE = new SingletonClazz();
private SingletonClazz(){

}
public static SingletonClazz getInstance(){
return INSTANCE;
}

public void test(){
System.out.println("test method");
}
}
这个方法相比于之前的方法的优势是,灵活性,不需要更改API的前提下,可以改变该类是否应该为Singleton。
使Singleton类变成为可序列化的:

实现Serializable接口
需要声明所有实例为瞬时的(transient)
提供一个readResolve()方法。

3.编写一个包含单个元素的枚举类型来实现
public enum SingletonClazz {
INSTANCE;

public void test(){
System.out.println("test method");
}
}
优点:简洁,并提供了序列化机制,而且能保证不会被多次实例化(即使是面对复杂的序列化或是发射攻击的时候)—–>实现Singleton的最佳方法
通过私有构造器强化不可实例化的能力
在缺少显式构造器的情况下,编译器会自动提供一个公有的,无参的缺省构造器。
public class DefaultConstructor {
//没有显示构造器
public static void main(String[] args) {
//使用编译器提供的公有的,无参的缺省构造器
DefaultConstructor defaultConstructor = new DefaultConstructor();
}
}
通过将类做成抽象类来实现不可实例化的目的是不可取的。继承抽象类,子类也可以被实例化。同时也会误导用户,以为是为了继承而设计的。
public abstract class AbstractClazz {
//通过抽象类来实现不可实例化是不可取的
public static void main(String[] args) {
//仍旧可以通过继承抽象类,来达到实例化子类的目的
SubClazz subClazz = new SubClazz();
}
}

class SubClazz extends AbstractClazz{

}
正确做法是:将构造器显式地声明为私有的。
public class UtilityClazz {
//防止类被实例化
private UtilityClazz(){
throw new AssertionError();
}

}

避免创建不必要的对象
最好能重用对象,而不是在每次需要的时候创建一个相同功能的新对象。
重用不可变对象。
public class NoNeedObject {

public static void main(String[] args) {
String s1 = new String("12345");//错误做法
String s2 = "12345";//正确做法
String s3 = "12345";
String s4 = new String("12345");

check(s1,s2);//不同
check(s2,s3);//相同 重用了对象
check(s1,s4);//不同

}
public static void check(String one,String two){
//==比较的是两个引用是否指向同一个对象
if(one == two)
System.out.println("same address");
else
System.out.println("different address");
}
}

重用那些已知不会被修改的可变对象。
class Person{
private final Date birthDate;

public Person(Date birthDate){
this.birthDate = birthDate;
}

public boolean isBabyBoomer(){
//没有必要的对象创建
//每次判断都会生成Calendar,Date,TimeZone实例
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;
}
}
用一个静态的初始化器来避免上面那种效率低下的情况。
class Person{
private final Date birthDate;

public Person(Date birthDate){
this.birthDate = birthDate;
}

private static final Date BOOM_START;
private static final Date BOOM_END;

static {
System.out.println("创建Calendar等对象");
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;
}
}

自动装箱(JDK5引入):Java编译器能在基本类型和包装类之间自动转换。如int和Integer,double和Double等等。
相关学习链接:
1.https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
2.https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html

public static void main(String[] args) {
//使用包装类Long
long beforetime = System.currentTimeMillis();
Long sum = 0L;
for(long i = 0;i<Integer.MAX_VALUE;i++){
sum+=i;
}
long aftertime = System.currentTimeMillis();

System.out.println("Long—>sum="+sum);
System.out.println("time="+(aftertime-beforetime));

//使用基本类型long
beforetime = System.currentTimeMillis();
long sum2 = 0L;
for(long i = 0;i<Integer.MAX_VALUE;i++){
sum2+=i;
}
aftertime = System.currentTimeMillis();
System.out.println("long—>sum="+sum2);
System.out.println("time="+(aftertime-beforetime));

}
运行结果:
Long—>sum=2305843005992468481
time=9642
long—>sum=2305843005992468481
time=777
结论:优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。
注意: 很多规定只是建议,不要矫枉过正,犯了教条主义的错误,一定要与实际的开发情况结合,实事求是。

不要错误地认为“应该尽可能地避免创建对象”,应该是“避免创建不必要的对象”,注意是不必要的!
通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象非常重要。

消除过期的对象引用
手工管理内存的语言:C或C++
具有垃圾回收功能的语言:Java,简化程序员的工作
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_SIZE = 16;

public Stack(){
elements = new Object[DEFAULT_INITIAL_SIZE];
}

public void push(Object e){
ensureCapacity();
elements[size++] = e;
}

/**
* stack: push(1) push(2) push(3) push(4) push(5) pop() pop()
* 5
* 4
* 3 —->栈顶
* 2
* 1
*
* 栈内部维护着过期的引用,也就是永远不会再被解除的引用,如4和5
* fixed:
* elements[size]=null;
*
*/
public Object pop(){
if(size==0)
throw new EmptyStackException();
return elements[–size];
}

private void ensureCapacity() {
if(elements.length==size){
elements = Arrays.copyOf(elements,2*size+1);
}
}
}

上面这段代码存在内存泄露的问题,当栈先是增长,然后弹出,从栈中弹出的对象都不会被当做垃圾回收。
栈内部维护着这些对象的过期引用,也就是永远不会被解除的引用。栈数组中下标大于或等于size的元素的引用都是过期的引用。
在支持垃圾回收的语言中, 内存泄露(也就是无意识的对象保持)很隐蔽。
修复办法:一旦对象引用过期,就清空这些引用。

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

容易导致内存泄露的几种情形:

类自己管理内存
缓存
监听器和其他回调

避免使用终结方法
终结方法(finalizer):不可预测,危险,一般情况下是不必要的。
终结方法的缺点:

不能保证会被及时地执行,而且根本不会保证它们会被执行。(所以不应该依赖终结方法来更新重要的持久状态)
严重的性能损失

终止类的对象中封装的资源(文件或线程),只需提供一个显式的终止方法。不需要编写终结方法。
例子:

InputStream,OutputStream,java.sql.Connection的close方法
java.util.Timer的cancel方法

一般与try-catch结合起来使用,以确保及时终止

最后编辑:
作者:
这个作者貌似有点懒,什么都没有留下。

留下一个回复