Effective Java——创建和销毁对象

Effective Java——创建和销毁对象

《Effective Java》这本书体现了绝大多数下的最佳程序设计的实践,它关心的是如何写出清晰、正确、健壮、灵活和可维护的程序来。这本书包含的内容非常丰富,这本书我就不多介绍了,只能默默的说一句,作为一名java开发错过了这本书难免会成为一个小遗憾,所以还是建议有时间的小伙伴能够去看看这本书,时间挤挤总还是有的。

1、考虑用静态工厂方法代替构造器

要获取一个实例,通常会提供一个公有的构造器,但还有另外一种方法——提供一个公有的静态工厂方法。
注意,这里的静态工厂方法与设计模式里的工厂方法模式不是一个概念:
静态工厂方法通常指的是某个类里的静态方法,通过调用该静态方法可以得到属于该类的一个实例;
工厂方法模式是一种设计模式,指的是让具体的工厂对象负责生产具体的产品对象,这里涉及多种工厂(类),多种对象(类),如内存工厂生产内存对象,CPU工厂生产CPU对象;

最大区别是:简单工厂模式里的静态工厂方法会创建各种不同的对象(不同类的实例),而静态工厂方法一般只创建属于该类的一个实例(包括子类)

1.1 例子

假设我们需要写一个一定范围内产生随机数的类RandomIntGenerator,该类有两个成员属性:最小值min和最大值max,
假设我们的需求是需要创建三种类型的RandomIntGenerator对象,
1、大于min,小于max;
2、大于min 小于Integer.MAX_VALUE;
3、大于Integer.MIN_VALUE 小于max
(1)使用构造器
以下代码不仅可读性差,不看注释很难知道其创建的对象的具体含义,而且在设计最后一个构造方法的时候,还报错
public class RandomIntGenerator {

	private int min = Integer.MIN_VALUE;
	private int max = Integer.MAX_VALUE;

	/**
	 * 大于min 小于max
	 */
	public RandomIntGenerator(int min, int max){
		this.min = min;
		this.max = max;
	}

	/**
	 * 大于min 小于Integer.MAX_VALUE
	 */
	public RandomIntGenerator(int min){
		this.min = min;
	}

	/**
	 * 大于Integer.MIN_VALUE 小于max
	 *  报错,构造器重复了
	 */
	public RandomIntGenerator(int max){
		this.max = max;
	}
}
(2)使用静态工厂方法
public class RandomIntGenerator {

	private int min = Integer.MIN_VALUE;
	private int max = Integer.MAX_VALUE;

	/**
	 * 大于min 小于max
	 */
	public RandomIntGenerator(int min, int max){
		this.min = min;
		this.max = max;
	}
	/**
	 * 大于min 小于max
	 */
	public static RandomIntGenerator between(int min, int max){
		return new RandomIntGenerator(min, max);
	}
	/**
	 * 大于min 小于Integer.MAX_VALUE
	 */
	public static RandomIntGenerator biggerThan(int min){
		return new RandomIntGenerator(min, Integer.MAX_VALUE);
	}

	/**
	 * 大于Integer.MIN_VALUE 小于max
	 */
	public static RandomIntGenerator smallerThan(int max){
		return new RandomIntGenerator(Integer.MIN_VALUE, max);
	}
}

1.2 jdk中的实例

JDK中的Boolean类的valueOf方法可以很好的印证这个优势,在Boolean类中,有两个事先创建好的Boolean对象(True,False)
public final class Boolean implements java.io.Serializable,Comparable<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;
	}
}

1.3 静态工厂方法比构造器的优劣势

优势:
  • 有名称,可读性强
  • 调用的时候,不需要每次都创建一个新对象
  • 可以返回原返回类型的任何子类型对象
劣势:
  • 如果类不含public或protect的构造方法,将不能被继承
  • 与其它普通静态方法没有区别,没有明确的标识一个静态方法用于实例化类

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

静态工厂和构造器有个共同的局限性:不能很好地扩展到大量的可选参数。以下3种方法可以解决该问题,但应优先考虑构建器。

2.1 重叠构造器模式

我们初学的时候都会选择 重叠构造器(telecoping constructor)模式 。在这种情况下,第一个构造器是实例化对象必须的参数,第二个会多一个参数,就这样叠加,最后是一个有所有参数的构造器。
public class Person { 
	private final String name; 
	private final int age; 

	private final String address; 
	private final String phone; 

	public Person(String name, int age) { 
		this(name,age,null); 
	} 


	public Person(String name, int age, String address) { 
		this(name,age,address,null); 
	} 

	public Person(String name, int age, String address, String phone) { 
		super(); 
		this.name = name; 
		this.age = age; 
		this.address = address; 
		this.phone = phone; 
	} 

	@Override 
	public String toString() { 
		return "name:"+name+" age:"+age+" address:"+address+" phone:"+phone; 
	} 

} 
缺点:重叠构造器可行,但当有很多的参数的时候,客户端的代码就会很难编写并且不容易阅读我们在使用的时候,必须很仔细的看每一个参数的位置和含义。

2.2 JavaBeans模式

这种模式下,使用无参的构造方法创建对象,然后调用setter 方法给属性设置值。
public class Person { 
	private String name; 
	private int age; 

	private String address; 
	private String phone; 

	public void setName(String name) { 
		this.name = name; 
	} 
	public void setAge(int age) { 
		this.age = age; 
	} 
	public void setAddress(String address) { 
		this.address = address; 
	} 
	public void setPhone(String phone) { 
		this.phone = phone; 
	} 

	@Override 
	public String toString() { 
		return "name:"+name+" age:"+age+" address:"+address+" phone:"+phone; 
	} 

} 
缺点:
  • 构造的过程分到了几个调用中,在构造JavaBeans的时候可能会不一致
  • 类无法仅仅通过检验构造器参数的有效性来保证一致性!
  • 对象的不一致会导致失败,JavaBeans模式阻止了把类做为不可变的可能,需要程序员做额外努力来保证它线程安全

2.3 构建器模式

public class Person { 
	private final String name; 
	private final int age; 

	private final String address; 
	private final String phone; 

	public static class Builder{ 
		private final String name; 
		private final int age; 

		private String address = null; 
		private String phone = null; 

		public Builder(String name,int age){ 
			this.name = name; 
			this.age = age; 
		} 

		public Builder address(String val){ 
			address = val; 
			return this; 
		} 

		public Builder phone(String val){ 
			phone = val; 
			return this; 
		} 

		public Person builder(){ 
			return new Person(this); 
		} 
	} 

	private Person(Builder builder){ 
		this.name = builder.name; 
		this.age = builder.age; 
		this.address = builder.address; 
		this.phone = builder.phone; 
	} 

	@Override 
	public String toString() { 
		return "name:"+name+" age:"+age+" address:"+address+" phone:"+phone; 
	} 

}
public class test {

	public static void main(String[] args) {
		Person p = new Person.Builder("tom", 18).address("深圳").phone("110").builder();
		System.out.println(p.toString());
	}
}
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是不错的选择,特别是当大多数参数都是可选的时候。
  • 与重叠构造器相比,builder牧师的客户端更易与阅读和编写
  • 与JavaBeans相比,更加的安全

3、使用枚举类型实现单例

Singleton指仅仅被实例化一次的类,当然也要考虑单例在多线程下的安全性,如饿汉式单例、双检锁式单例、静态内部类式的单例,但如果Singleton加上“implements Serializable”的字样,它就不再是一个 Singleton。
一个类实现了 Serializable接口,我们就可以把它往内存地写再从内存里读出而"组装"成一个跟原来一模一样的对象, 从内存读出而组装的对象破坏了单例的规则。单例是要求一个JVM中只有一个类对象的,而现在通过反序列,一个新的对象克隆了出来。

3.1 readResolve方法

实现序列化的Singleton,为了维持并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法。这样,当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了,单例规则也就得到了保证。
深复制工具类:
public class DeepCopy {

	public static Object copy(Object obj){
		Object o=null;
		if(obj==null)
			return o;
		try{
			ByteArrayOutputStream bos = new ByteArrayOutputStream(); 
			ObjectOutputStream oos = new ObjectOutputStream(bos);
			oos.writeObject(obj);
			oos.close();

			ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());  
			ObjectInputStream ois = new ObjectInputStream(bis);
			o = ois.readObject();
			ois.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return o;
	}
}
public class Singleton implements Serializable{    

	private volatile static Singleton instance;   

	private Singleton(){}  

	public static Singleton getInstance(){  
		if(instance == null){  
			synchronized (Singleton.class) {
				if(instance == null)
					instance = new Singleton();
			}    
		}    
		return instance;    
	}   

	private Object readResolve() {
		return instance;
	}

	public static void main(String[] args) throws InstantiationException, IllegalAccessException, 
									IllegalArgumentException, InvocationTargetException {
		Singleton instance=Singleton.getInstance();
		Singleton i1=(Singleton) DeepCopy.copy(instance); //通过反序列化生成新的实例
		System.out.println(i1==instance);  //true

		Constructor<?> constructor =Singleton.class.getDeclaredConstructors()[0];  //通过反射机制生成新的实例
		constructor.setAccessible(true);
		Singleton i2=(Singleton)constructor.newInstance();
		System.out.println(i2==instance);  //false
	}
} 
可以看见,通过反射机制,设置AccessibleObject.setAccessible(true),改变构造器的访问属性,调用构造器生成了新的实例。也就是说该方法不能防止反射攻击。

3.2 枚举类Singleton

通过枚举实现Singleton更加简洁,同时枚举类型无偿地提供了序列化机制,可以防止反序列化的时候多次实例化一个对象。枚举类型也可以防止反射攻击,当你试图通过反射去实例化一个枚举类型的时候会抛出IllegalArgumentException异常
public enum Singleton{

	INSTANCE;
	
	public void method(){
		System.out.println("666");
	}

	public static void main(String[] args) {
		Singleton instance=Singleton.INSTANCE;
		instance.method();
		
		Singleton otherInstance=(Singleton) DeepCopy.copy(instance);
		System.out.println(otherInstance==instance);   //true
		
	}
}  
public enum Singleton{

	INSTANCE;

	public static void main(String[] args) throws InstantiationException, IllegalAccessException, 
	IllegalArgumentException, InvocationTargetException {
		Singleton instance=Singleton.INSTANCE;

		Singleton otherInstance=(Singleton) DeepCopy.copy(instance);  //通过反序列化生成新的实例
		System.out.println(otherInstance==instance);   //true

		Constructor<?> constructor =Singleton.class.getDeclaredConstructors()[0];  //通过反射机制生成新的实例
		constructor.setAccessible(true);
		Singleton i2=(Singleton)constructor.newInstance();
		System.out.println(i2==instance); 

	}
}  
抛出异常:

最后,不管采取何种方案,请时刻牢记单例的四大要点:
  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
  • 反射攻击安全

4、避免创建不必要的对象

(1)最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象,如果对象是不可变的,那它就始终可以被重用。
如:String s=new String("hjy") 和String s="hjy"
前者每次执行都会创建一个新的String实例,而“hjy”本身就是一个String实例,这种用法在一个频繁调用的方法中,就会创建大量String实例;而后者只创建了一个String实例,对象保存在常量池中,可以被重用。

(2)对于同时提供了静态工厂方法和构造器的不可变类,建议使用静态工厂方法,以避免创建不必要的对象。例如,静态工厂方法,Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则返回已创建的对象。
	public static void main(String[] args) {
		Integer a=Integer.valueOf(2);
		Integer b=Integer.valueOf(2);
		System.out.println(a==b);  //true
	}
(3)有一种创建多余对象的新方法,称作自动装箱,自动装箱使得基本类型和引用类型之间的差别变得模糊起来。

	public static void main(String[] args) {
		Long sum = 0L;
		for(long i = 0; i < Integer.MAX_VALUE; i++){
			sum += i;
		}
		System.out.println(sum);
	}
这段程序算出的结果是正确的,但是比实际情况要慢的多,只因为打错了一个字符。变量sum被声明成Long而不是long,意味着程序构造了大约2的31次方个多余的Long实例。结论很明显:要优先使用基本类型而不是引用类型,要当心无意识的自动装箱。

5、消除过期的对象引用

内存溢出:程序在申请内存时,没有足够的内存空间供其使用,出现out of memory
内存泄漏:为某对象申请内存后,无用时没有释放已申请的内存空间,造成内存浪费
造成内存泄漏的常见来源主要有以下3种:
  • 没有及时清除过期的对象引用
只要类是自己管理内存的,程序员就需要警惕内存泄漏的问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
//弹出栈的对象引用应该被清除
public Object pop() {  
	if (size == 0)  
		throw new EmptyStackException();  
	Object result = elements[--size];  
	elements[size] = null //清空引用 
		return result;  
}  
  • 缓存:放在缓存的引用可停留很长时间,解决方法——只要在缓存之外存在对某个项的键的引用,该项就有意义这样的缓存的话,就可以使用WeakHashMap代表缓存,因为当缓存中的项过期的时候,它们就会自动被删除掉。
  • 监视器和其他回调:注册的回调却没有显示取消注册,那么就会积累。确保回调立即被当作垃圾的最佳方法是只保存它们的弱引用


猜你喜欢

转载自blog.csdn.net/hjy132/article/details/78387886