Effective Java 第二章 创建和销毁对象

1. 用静态工厂方法代替构造器

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

此方法有5个优势

  1. 方法有名称。相较于构造器,可以更加确切地描述返回的对象
  2. 不用每次调用的时候都创建一个新的对象。
  3. 可以返回原返回类型的任何子类类型的对象
  4. 方法返回的的对象的类可以随着每次调用而发生变化。(通过修改静态工厂方法的参数值)
  5. 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。(服务提供者框架、通过反射)。

此方法有两个缺点

  1. 类如果不含公有的或者受保护的构造器,就不能被子类化。但是这会因祸得福,程序员会倾向于使用复合(composition),而不是继承,这正是不可变类型需要的。如果你的类设计就是用来继承的,那就不能把构造器设置成私有的了。
  2. 静态工厂方法的第二个缺点在于,程序员很难发现他们。API文档中并不会把他们标识出来。

总结:
静态工厂和公有构造器各有用处,大多数时候静态工厂更加合适。因此切忌第一反应是提供公有构造器,而不考虑静态工厂。

2. 遇到多个构造器参数时要考虑使用构建器

类中编写静态成员类Builder来实现建造者模式。

//建造者(Builder)模式
//一个账号,账号、密码是必选参数,性别、email是可选参数
public class AccountFacts{

	private final int accountNumber;
	private final int password;
	private final int sex;
	private final String email;

	public static class Builder{
	
	 	//必选参数
		private int accountNumber;
		private int password;
		
		 //可选参数,要设置默认值
		private int sex = 1;//默认是男性
		private String email = "null";

		public Builder(int accountNumber,int password){
			this.accountNumber = accountNumber;
			this.password= password;
		}
		
		public Builder sex(int val){
			sex = val;
			return this;
		}
		public Builder email(String val){
			email = val;
			return this;
		}

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

	private AccountFacts(Builder builder){
	
	accountNumber=builder.accountNumber;
	password=builder.password;
	sex=builder.sex;
	email=builder.email;
	
	}

}

客户端代码

//账号是1234,密码是123,性别是男性,email没有填写
AccountFacts zcyAccount = new AccountFacts.Builder(1234,123).sex(1).build();

Builder模式也适用于类层次结构。但是代码比较长,这里就不做分享了。建议阅读书中的细节。

相较于构造器,builder的优势在于,它支持多个可变参数。同时可以多次调用某一个方法来传入多个参数到一个域中。
劣势在于,为了创建对象,要先创建它的构建器,有一部分开销,影响性能。同时,代码比较冗长。

总结:
如果类的构造器或者静态工厂有多个参数,Builder模式是个不错的选择。

3. 用私有构造器或者枚举类型强化Singleton属性

Singleton:指仅仅被实例化一次的类。

私有构造器(单例模式-饿汉)

public class BBA{
	private BBA(){...};//私有构造器
	private static final BBA INSTANCE = new BBA();//静态、不可变
	public static BBA getInstance(){
	return INSTANCE;
	}
}

私有构造器(单例模式-懒汉)

public class BBA{
	private BBA(){...};//私有构造器
	private static volatile BBA instance;//只声明,不创建对象
	public static BBA getInstance(){
		//调用getInstance()时再创建对象
		if(instance == null){
			sychronized(A.class){
				if(instance==null){
					instance = new BBA();
				}
			}
		}
		return instance;
	}
}

枚举

//声明一个包含单个元素的枚举类型
public enum Elvis{
	INSTANCE;
}

总结:
单例模式拥有灵活性缺点:在序列化的时候必须声明所有实例域都是瞬时(transient)的。否则每次反序列化一个序列化的实例时,都会创建一个新的实例。
单元素的枚举类型更加简介,并且无偿提供了序列化机制。即使在面对复杂的序列化和反射攻击时,都能绝对防止多次实例化。虽然目前还没有被广泛使用,但是单元素的枚举类型经常成为实现Singleton的最佳方法

4. 通过私有构造器强化不可实例的能力

只包含静态方法或者静态域的类,例如java.lang.Math或者java.util.Arrays这些工具类,它们不希望被实例化,因为实例化对它们来说没有任何意义
在实操过程中,通过把类做成抽象类来强制该类不可被实例化是不行的

原因如下:

  • 该类可以被子类化,并且该子类可以被实例化
  • 这样子做会误导用户,以为这种类是专门为了被继承而设计的

解决方案让类包含一个私有构造器。(这样子的话编译器就不会自动生成缺省的构造器)

//不可被实例化的类
public class UtilityClass{
	private UtilityClass(){
		throw new AssertionError();
	}
}

当然,这么做也有副作用,它使得一个类不能被子类化

5. 优先考虑依赖注入来引用资源

有许多类会依赖一个或多个底层的资源。即这个类可能会依赖其他的类的实例。
最好的采取做法是,当创建一个新的实例时,就将该实例所依赖的资源传入到构造器中。这就是依赖注入的一种形式

public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }
}

优点:依赖注入的对象资源具有不可变性,因此多个客户端可以共享依赖对象。同时极大提升了灵活性可重用性可测试性
缺点:依赖注入会导致大型项目结构凌乱,可以通过使用依赖注入框架解决,如 Dagger、Guice、Spring

6. 避免创建不必要的对象

  • 一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。
    反例:String s = new String("BMW");
    改进:String s = "BMW";

  • 尽量调用静态工厂方法,而不是使用构造器。因为构造器每次调用都会创建一个新的对象,而静态工厂方法不会这样做。

  • 有些对象创建成本比其他对象高很多的时候,建议缓存下来重用

    class Test{
        //省略构造函数
        public int calculateThatValue(){
            int value;//假设这是一个若赋值了就不变的,且要经过很复杂的运算才能算出来的值
            //假设这里是很复杂的一段代码,需要运行很长时间
            return value;
        }
    }
    

    改进:

    class Test{
        private final int VALUE_COSTLONGTIME;//假设这是一个不变的,且要经过很复杂的运算才能算出来的值
        //省略构造函数
        //静态方法块,在类第一次被加载时候执行
        static{
            int value;
            //假设这里是很复杂的一段代码,需要运行很长时间
            VALUE_COSTLONGTIME = value;//把计算好的值赋值给final常量
        }
    }
    
  • 当对象重用与否不明显时,考虑使用适配器(也叫视图)来避免创建不必要对象。
    适配器是指这样一个对象:把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例
    例如HashMap的keySet方法实现:

    public Set<K> keySet() {
            Set<K> ks = keySet;
            return (ks != null ? ks : (keySet = new KeySet()));
        }
    

    可以看到,第一行实现代码是把keySet赋值给ks,return 后面是一个三元表达式,当ks不为空时返回ks,否则创建一个KeySet返回。(也就是keySet为空时创建一个KeySet对象返回)
    keySet在HashMap中的定义:transient volatile Set<K> keySet = null;
    其中,volatile关键字修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。

  • 避免无意识的使用自动装箱
    基本类型优先,当心无意思的自动装箱
    例: long 类型声明成Long

总结:
不要错误理解成尽可能避免创建对象。小对象的创建和回收动作是非常廉价的。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是一件好事。
同时,通过维护自己的对象池来避免创建对象也不是一种好的做法。除非池中的对象非常重量级(例如数据库连接池)。得益于现代JVM高度优化的垃圾回收器,维护对象池消耗的性能很容易就会超过直接创建。

7. 消除过期的对象引用

以下是内存泄漏的三个常见来源:

  • 无意识的对象保持
    下面程序中的,从栈中弹出来的对象不会被当作垃圾回收。即使程序不再引用这些对象,它们也不会被回收。栈内部会维护着这些对象的过期引用

    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];
    	}
    
    	private void ensureCapacity(){
    		if(elements.length == size)
    			elements = Arrays.copyOf(elements,2*size+1);
    	}
    }
    

    可以看到,凡是在elements数组的“活动部分”之外的任何引用都是过期的。活动部分是指elements中下标小于size的那些元素

    修复方法:

    	//一旦对象引用过期,清空这个引用。
    	//弹出栈的元素就是过期的,把它清空
    	public Object pop(){
    	if(size==0)
    		throw new EmptyStackException();
    	Object result = elements[--size];
    	elements[size] = null;//解除过期引用
    	return result ;
    	} 
    

    要注意的是:
    1.清空对象应该是一种例外,而不是一种规范行为。最紧凑的作用域范围来定义每一个变量时,这些对象引用会被包含它的变量结束生命周期,而不用手动结束。
    2.**只要类是自己管理内存,程序员就应该警惕内存泄露问题。**一旦元素被释放掉,它包含的任何对象引用都应该被清空。

  • 缓存
    当把对象引用放到缓存中,很容易被遗忘,这会导致它不再用之后很长一段时间内仍然存留在内存中。
    解决方式: 在缓存之外存在对某个项的键的引用。

  • 监听器和其他回调

总结:
由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具,才能发现内存泄漏的问题。因此,如果能够在使用内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。

8. 避免使用终结方法和清除方法

终结方法(finalizer)不可预测并且危险,一般情况下是不必要的。

Java9中用清除方法(cleaner)代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢的,一般情况下也是不必要的。

  • 终结方法和清楚方法的缺点在于不能保证被及时执行。这意味着,注重时间(time-critical)的任务不应该由终结方法或者清除方法来完成。
  • Java语言规范不仅不能保证终结方法或者清除方法会被及时地执行,而且根本就不保证它们会被执行。因此,永远不应该依赖终结方法或者清除方法来更新重要的持久状态。
    例如,如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止。未被捕获的异常会使对象处于破坏的状态(corrupt state),如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为
  • 使用终结方法和清除方法有非常严重的性能损失
  • 终结方法有严重的安全问题。构造器抛出的异常,本足以防止对象继续存在。有了终结方法的存在,这一点就做不到了。因此要编写一个空的final的finalize方法。(当然,如果是final类就不需要了,因为这种攻击方式是通过运行恶意子类的终结方法实现的,final方法没有子类,自然不用处理。)

Q: 如果类的对象中封装的资源(例如文件或者线程)确实需要终止,应该怎么做才能不用编写终结方法或者清楚方法呢?
A: 让类实现AutoCloseable接口。客户端在每个实例不再需要的时候调用close方法,一般利用try-with-resources确保终止,即使遇到异常也是如此 。

终结方法和清除方法的好处:

  • 当资源所有者忘记调用close方法的时候,可以充当“安全网”。虽然不能保证及时清理资源,但迟一点清理总比不清理好。当然,如果要编写这样的安全网终结方法,就要认真考虑清楚,这种保护是否值得付出这样的代价
  • 处理对象的本地对等体(native peer),它由普通对象通过本地方法委托给本地对象(native object)。因而,不会被垃圾回收器回收。如果它没有关键资源或者不影响性能,可以交给终结方法或者清除方法,如果有关键资源或者性能无法接受,应该具有一个close方法。

总结:
除非是作为安全网,或者是为了终止非关键的本地资源,否则不要使用清除方法,对于在Java9之前的发行版本,尽量不要使用终结方法。若使用了终结方法或者清除方法,则要注意它的不确定性性能后果

9. try-with-resources 优先于 try-finally

try块和finally块都有可能抛出异常,若同时抛出异常,则finally块中的第二个异常将会覆盖try块的异常,使我们无法追踪到程序错误的根源,例如下面这段代码示例:

@Test
public void tryFinallyTest() throws Exception {
  try {
    throw new Exception("try block Exception");
  } finally {
    throw new Exception("finally block Exception");
  }
}

从执行结果中可以看出,try块中的异常没有显示执行结果中可以看出,try块中的异常没有显示

解决方案:
自java7开始加入了try-with-resource语法并引入了AutoCloseable接口,只要相关资源的类或接口实现或继承了这个接口,就可以不显式调用close()方法,自动关闭资源,并且不会有上述"异常覆盖"的情况发生
我们同时用两种形式的try语句来进行测试,throwException()方法累计抛出的异常。

public class TryTest implements AutoCloseable {
  private int count = 0;

  @Test
  public void tryResourceTest() throws Exception {
    try(TryTest test = new TryTest()) {
      test.throwException();
    }
  }

  @Test
  public void tryFinallyTest() throws Exception {
    TryTest test = new TryTest();
    try {
      test.throwException();
    } finally {
      test.close();
    }
  }

  public void throwException() throws Exception {
    count++;
    throw new Exception("the No." + count + " Exception occurs");
  }

  @Override
  public void close() throws Exception {
    throwException();
  }
}

try-finally语句块的执行结果
try-finally语句块的执行结果
try-with-resources语句块的执行结果
try-with-resources语句块的执行结果
我们可以成功看到我们真正想要看到的第一个异常信息,第二个异常被标注为抑制。

总结:
因此在处理必须关闭的资源时,我们始终要优先考虑用 try-with-resources,而不是用try-finally,这样得到的代码将更加简洁、清晰,产生的异常也更有价值

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值