《Effective Java》梳理笔记

1> 对象的创建和销毁

1.1> 优先考虑使用静态工厂方法代替构造器

静态工厂方法相对于构造器的优势描述
静态工厂方法可以自定义名称常用的构造器名称只能和类名相同,而静态工厂方法则可以拥有更有意义更有指向性的名称。
不必在每次调用时都创建一个新对象在需要一个对象时,将预先创建好的对象返回,或者将构建好的对象缓存起来重复利用。
可以返回任何子类型的对象静态工厂方法可以根据传入的参数决定返回特定的子类型对象,使得代码更灵活。

1.2> 遇到多个构造器参数时要考虑使用构建器(builder)

  当有需要使用多个参数构造一个对象时,最容易想到的方法是:重叠构造器,首先创建一个全属性构造器,然后一一创建所需的去掉某些可选属性的构造器。如下我写了仅仅八个属性的Person类,并使用重叠构造器方法为其产生多个适用于不同场景的构造器:

public class Person {
	// 必要属性
	private final String id;
	private final String name;
	// 可选属性
	private byte age;
	private char gender;
	private float stature;
	private float weight;
	private String nativePlace;
	private String nation;
	
	// 包含所有属性的构造器
	public Person(String id, String name, byte age, char gender, 
				float stature, float weight, String nativePlace, String nation) {
		super();
		this.id = id;
		this.name = name;
		this.age = age;
		this.gender = gender;
		this.stature = stature;
		this.weight = weight;
		this.nativePlace = nativePlace;
		this.nation = nation;
	}
	
	// 下面为去掉某些可选属性的构造器
	public Person(String id, String name, byte age, char gender, 
							float stature, float weight, String nativePlace) {
		this(id, name, age, gender, stature, weight, nativePlace, null);
	}
	
	public Person(String id, String name, byte age, char gender, 
							float stature, float weight) {
		this(id, name, age, gender, stature, weight, null);
	}
	
	public Person(String id, String name, byte age, char gender) {
		this(id, name, age, gender, 0, 0);
	}
	
	public Person(String id, String name, byte age) {
		this(id, name, age, '男');
	}
	
	public Person(String id, String name) {
		this(id, name, (byte)0);
	}
}

  重叠构造器是最容易想到的方法了,但在属性较多的时候会使得代码难以阅读,且难以编写,一长串类型相同的属性也容易产生微妙的错误,例如参数位置填错但它们类型相同,编译器根本无法检查出错,但在运行中程序逻辑必定有问题。而比它稍微高级些的,就是使用JavaBean模式来编写这个类。在这种模式下,首先调用无参构造函数来创建对象,然后使用setter方法来一一为其中属性赋值:

public class Person {
	// 必要属性。请注意这里无法使用final修饰符
	private String id;
	private String name;
	// 可选属性
	private byte age;
	private char gender;
	private float stature;
	private float weight;
	private String nativePlace;
	private String nation;
	
	// 无参构造函数
	public Person() {
		super();
	}

	// setter
	public void setId(String id) {
		this.id = id;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setAge(byte age) {
		this.age = age;
	}

	public void setGender(char gender) {
		this.gender = gender;
	}

	public void setStature(float stature) {
		this.stature = stature;
	}

	public void setWeight(float weight) {
		this.weight = weight;
	}

	public void setNativePlace(String nativePlace) {
		this.nativePlace = nativePlace;
	}

	public void setNation(String nation) {
		this.nation = nation;
	}
}

  JavaBean模式也是比较常用的方法,它弥补了重叠构造器方法代码难以阅读的问题,同时也不会出现传参错误。但它有自己的缺陷:
1、在javabean的构造过程中,它可能处于不一致的状态。即构造对象与属性赋值操作被分离开,这使得调用者可能获取到尚未完成赋值操作的对象,程序将会出现莫名其妙的错误,而且java异常栈指示的错误位置往往并非其真实位置;
2、javabean使得将类设置为不可变的操作更为困难。通常我们只需要在规定为不可变的属性上使用final修饰符,但javabean不允许这么做,因为它的逻辑是先构造后赋值。
  现在就可以引入第三种方式:构建器模式(builder),它既能保证重叠构造器法的安全性,亦能保证javabean模式的可读性,还可以支持不可变的类构造对象。builder模式并不直接生成想要的对象,而是让调用者利用所有必要的参数调用构造器(或者静态工厂),先得到一个builder对象,然后在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后再调用无参的build方法来生成想要的不可变的对象,如此,就保证了先赋值后构造的逻辑。这个builder是它构建的类的静态成员类。如下:

public class Person {
	// 必要属性
	private final String id;
	private final String name;
	// 可选属性
	private final byte age;
	private final char gender;
	private final float stature;
	private final float weight;
	private final String nativePlace;
	private final String nation;
	
	// 内部构建器builder类
	public static class Builder {
		// 构建器中属性与外部类相同,因为外部类构造器/静态工厂方法还需要使用构建器来为属性赋值
		// 必要属性
		private final String id;
		private final String name;
		// 可选属性
		private byte age;
		private char gender;
		private float stature;
		private float weight;
		private String nativePlace;
		private String nation;
		
		public Builder(String id, String name) {
			this.id = id;
			this.name = name;
		}
		
		public Builder age(byte age) {
			this.age = age;
			return this;
		}
		
		public Builder gender(char gender) {
			this.gender = gender;
			return this;
		}
		
		public Builder stature(float stature) {
			this.stature = stature;
			return this;
		}
		
		public Builder weight(float weight) {
			this.weight = weight;
			return this;
		}
		
		public Builder nativePlace(String nativePlace) {
			this.nativePlace = nativePlace;
			return this;
		}
		
		public Builder nation(String nation) {
			this.nation = nation;
			return this;
		}
		
		// 使用builder对象构造外部类对象。
		public Person build() {
			return new Person(this);
		}
	}
	
	// 使用builder对象为属性赋值
	public Person(Builder builder) {
		super();
		this.id = builder.id;
		this.name = builder.name;
		this.age = builder.age;
		this.gender = builder.gender;
		this.stature = builder.stature;
		this.weight = builder.weight;
		this.nativePlace = builder.nativePlace;
		this.nation = builder.nation;
	}
}

  后续可以使用下列方法创建参数可选的Person类对象:

Person person = new Person.Builder("id", "name")
								.age((byte)0).gender('男').nation("汉").build();

  不过builder模式就一定完美吗?不,要想构造一个对象就要先创建它的构建器builder,在对性能要求很高时,就值得考量了。它的代码比重叠构造器还要冗长,因此它只在参数数量较多时使用,比如四个以上。特别是在很多参数都为可选的时候,builder模式应该是优先考虑的。使用构建器使得代码更易于阅读和编写,同时保证安全性。

1.3> 用私有构造器或枚举类型强化单例模式(singleton)

  当需要系统只持有某个类的唯一对象时,比如文件系统、任务管理器等。可以使用单例模式。实现单例模式有两种常用方法,它们都需要将构造器私有化
  法1、将构造器私有化,并暴露出final修饰的公有化静态成员以供使用。 如下:

public class SingletonModel1 {
	public static final SingletonModel1 INSTANCE = new SingletonModel1();
	
	private SingletonModel1() {
		
	}
}

  法2、将构造器私有化,并通过静态工厂方法来暴露出实例。如下:

public class SingletonModel1 {
	private static final SingletonModel1 INSTANCE = new SingletonModel1();
	
	private SingletonModel1() {

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

  但是调用者可以通过反射机制来攻击此单例模式,先获取此类的构造器,然后通过setAccessible()方法使得构造器可访问。因此我们需要稍作修改,来防止此类攻击:

public class SingletonModel1 {
	private static final SingletonModel1 INSTANCE = new SingletonModel1();
	// 全局检测开关,第一次创建对象时关闭,防止反射二次创建对象。
	private static boolean isFirst = true;
	
	private SingletonModel1() {
		synchronized (SingletonModel1.class) {
			if (isFirst) {
				isFirst = false;
			} else {
				throw new RuntimeException("抱歉,此操作会破坏单例模式,已终止。");
			}
		}
	}
	
	public static SingletonModel1 getInstance() {
		return INSTANCE;
	}
}

  而不仅反射能攻击单例模式,反序列化操作也可以生成一个新的实例对象从而破坏单例模式。因此我们需要在单例模式设计的类中添加readResolve()方法来防止单例模式被反序列化破坏。可以理解为只要JVM检测发现待反序列化的类中带有readResolve()方法,就会调用该方法按程序员的指定逻辑来执行反序列化操作:

private Object readResolve(){
	return INSTANCE;
}

  再等等,不仅仅反序列化能破坏单例模式,克隆操作也能如此!因此我们还需要在单例模式类中重写clone方法,使其直接返回已创建的实例:

@Override
protected Object clone() throws CloneNotSupportedException {
	return INSTANCE;
}

  至此,一个完整的单例模式类就构建出来了。而在Java1.5版本后,我们还有第三种方式来实现单例模式。那就是使用单元素的枚举类型。它堪称“实现Singleton的最佳方法”。首先枚举类型实例创建默认即是线程安全的, 同时防止反射创建对象的可能,并且反序列化时依然为单例。

public enum SingletonModel2(){
	INSTANCE;
}

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

  通常,我们会将只含有静态方法和静态域的工具类规定为“不可实例化”的。而将类做成抽象类是不行的,因为可以很容易地使用子类来继承而后实例化。因此,我们需要彻底地将不想被实例化的类的构造器私有化,禁止其他类访问。如下:

public class DontInstantiable {
	// 抛出断言错误终止程序,以禁止本类中使用构造器。而private修饰符同时禁止外部类使用构造器
	private DontInstantiable() {
			throw new AssertionError();
	}
}

1.5> 避免创建不必要的对象

  如果一个对象是不可变的,那它就始终可以被重用。因此对于同时提供了构造方法和静态工厂方法的类,应该使用它的静态工厂方法,我们也应给自己编写的不可变类提供静态工厂方法;
  如果一个对象是可变的,但它后续不会被修改,那么我们应该将它放进静态代码块中,而不是每次调用就创建一个新的。尤其是Calendar类的实例,创建代价极其昂贵。例如:

// 最原始的写法,每次调用isPost_90方法时都会产生Calendar实例,效率极低。
public class Person {
	private final Date birthDate;
	public boolean isPost_90() {
		Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		cal.set(1990, Calendar.JANUARY, 1, 0, 0, 0);
		Date startDate = cal.getTime();
		cal.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
		Date endDate = cal.getTime();
		return birthDate.compareTo(startDate) >= 0 && 
			   birthDate.compareTo(endDate) < 0;
	}
}
// 优化后,将不需要修改的可变对象放入静态代码块中,只在类加载时创建一次。
/*
 * 在这里讨论一下延迟初始化的情况:将代码放入静态代码块中时,每次类加载会实例化Calendar一次,
 * 然而假设并没有调用isPost_90方法,Calendar依然会实例化一次。
 * 而延迟初始化甚至可以做到只在第一次调用isPost_90方法时实例化Calendar。
 * 但实际上并不提倡这样做,因为延迟初始化会使方法的实现更加复杂,从而无法在此基础上显著提升性能
 */
public class Person {
	private final Date birthDate;
	private static final Date START_DATE;
	private static final Date END_DATE;
	static {
		Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		cal.set(1990, Calendar.JANUARY, 1, 0, 0, 0);
		Date startDate = cal.getTime();
		cal.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
		Date endDate = cal.getTime();
	}
	public boolean isPost_90() {
		return birthDate.compareTo(START_DATE) >= 0 && 
			   birthDate.compareTo(END_DATE) < 0;
	}
}

  而在Java1.5版本后,还有一种隐式地创建多于对象的方法:自动装箱。自动装箱使得基本类型和包装类型可以混用,但它们在性能上依然有比较明显的差别,因为包装类型会根据基本类型创建一次包装类型的实例对象。因此要优先使用基本类型,当心无意识地大量自动装箱。例如:

/*
 * 此代码在+= i操作时,每次都会自动装箱创建一个Long类型实例,因此将有2^31个多余的对象被创建。
 * 优化仅仅是将sum改为long类型
 */
Long sum = 0L;
for( long i = 0L; i < Integer.MAX_VALUE; i ++) {
	sum += i;
}

  不必因此而觉得“创建对象就是代价昂贵的,我们要尽可能避免创建对象,那么我们来使用对象池吧”,事实上小对象的创建和销毁都是很廉价的。所以如果想要维护一个自己的对象池,那么必须确保这些对象是非常重量级的,比如数据库连接池。而轻量级对象池可以说是得不偿失,JVM自身的垃圾回收器轻易就能超过轻量级对象池的性能。

1.6> 消除过期的对象引用

  消除过期的对象引用是针对于:无意识的对象保持来说的,即内存泄漏。内存泄漏有三种常见场景:栈、缓存、监听器和回调。
  栈:讲栈事实上是指所有会自己管理内存的类,栈只是其中一个例子。即在一个元素出栈后,栈依然会维持该元素的引用,这也导致了垃圾回收器认为该引用依然有效,从而不回收该内存区域。内存泄露就产生了。这种情况的解决办法也很简单,那就是元素出栈或者释放后,程序员再手动将该引用置空。这样不仅垃圾回收器能回收该内存区域,而且在以后对该引用有错误的运用时,程序会报NullPointerException,而不是悄悄地运行下去导致你抓一晚上虫子的惨剧。
  缓存:缓存主要是需要判断其中项是否有意义来解决内存泄漏问题。有以下几种情况:
1、当所要缓存的项的生命周期由该键的外部引用而不是由值决定的,则可以使用WeakHashMap
2、当所要缓存的项的价值随时间推移而降低,那么我们需要给缓存一个后台线程来周期性清理过期项。或者在添加新项目时同时清理,比如LinkedHashMap类利用removeEldestEntry()方法清理,其他复杂缓存则必须使用java.lang.ref
  回调:当一个客户端在API中注册了一个回调后,又没有取消注册,那么除非API自主清理这些无用对象,否则也会不断产生内存泄漏。而确保回调能立即被当作垃圾回收的方法是只维持它的弱引用,比如只将它们保存为WeakHashMap的键。
  要检测内存泄漏可以使用Heap剖析工具。

1.7> 避免使用终结方法finalizer

  因为终结方法通常是不可预测的,也就是指自从调用终结方法后,它并不会立刻执行,甚至根本不保证被执行,从调用开始到真正执行的时间间隔是任意长的。且不同的JVM中终结方法的实现是大相径庭的,因此它会使得程序不稳定、性能降低、可移植性下降。也不要使用System.gc()System.runFinalization()这两个方法,因为它们确实增加了终结方法执行的机会,但是依然不保证一定被执行。
  而终结方法还有一个麻烦是:当未被捕获的异常在终结过程中被抛出,那么该异常会被忽略,程序不会停止,不会打印异常栈,并且终结过程也会中断。此时对象会处于中断状态,而且因为未被终结,所以依然可以调用到它,调用它会导致任何不确定的行为。
  如果需要终止某个资源,那么只需提供一个显式的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法,在一个私有域中记录下自己是否已经被终止了。其他的方法就必须检查这个域后使用,如果发现已经被终止,则抛出异常。典型例子:Java各种流中的close()方法。显式终止方法需要配合try-catch块使用,以保证即使有异常抛出,显式终结方法也能被调用。
  而终结方法为什么还存在呢?因为它还是有两种合法用途:
1、当客户端忘记调用显示的终结方法时,终结方法可以充当安全网。毕竟迟一些终结比永远不终结好些。但如果终结方法发现资源还未终止,应该在日志中记录一条警告,方便向客户端报告这个bug,它应该被修复;
2、当普通对象通过本地方法委托给一个本地对象,就形成了本地对等体。而本地对等体不是一个普通对象,因此垃圾回收器不会知道它。当本地对等体被回收的时候,其中的本地对象无法被回收,此时就需要终结方法。

2> 对于所有对象都通用的方法

2.1> 覆盖equals方法时请遵守约定

  自反性:对于任何非null的引用x,x.equals(x)必须返回true;
  对称性:对于任何非null的引用x和y,当且仅当x.equals(y)返回true时,y.equals(x)必须返回true;
  传递性:对于任何非null的引用x,y和z,如果x.equals(y)返回true,并且y.equals(z)返回true时,x.equals(z)也必须返回true;
  一致性:对于任何非null的引用x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)返回的结果必须不变。
  实现高质量equals方法的诀窍
  1、第一步:使用==操作符检查“参数是否为本对象的引用”;
  2、第二步:使用instanceof操作符检查“参数是否为正确的类型”;
  3、第三步:将参数转换成正确的类型;
  4、第四步:对于该类中的每个关键的域,要检查参数中的域是否与该对象中对应的域相匹配;
  5、覆盖equals时总要覆盖hashCode
  6、不要企图让equals太过智能,其中的代码逻辑越简单越好;
  7、不要将equals声明中的参数类型:Object替换为其他类型(当然@Override注解也不允许这么做);

2.2> 覆盖hashCode规范

  1、对象的equals方法比较操作中用到的数据未被修改,则hashCode值不变;
  2、两对象equals方法为true,即相等,则hashCode值也相等;
  3、两对象equals方法为false,即不等,hashCode值不一定不等。
  4、equals方法比较操作中用到的数据也应该被hashCode计算;
  5、冗余域(可被其他关键域计算得到的域)不必也不能被hashCode计算在内,
  下面给出一种简单的解决办法
  1、把某个非零的常数值,比如说17,保存在一个名为resultint类型的变量中;
  2、对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
    a.为该域计算int类型的散列码c:
      i、如果该域是boolean类型,则计算(f ? 1 : 0);
      ii、如果该域是bytecharshort或者int类型,则计算(int)f;
      iii、如果该域是long类型,则计算(int)(f ^ (f >>> 32)
      iv:如果该域是float类型,则计算Float.floatToIntBits(f)
      v:如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值。
      vi:如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。
      vii:如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。
    b、按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:result = 31 + result + c;
  3,返回result
  4.写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的散列码”。要编写单元测试来验证你的推断。如果相等的实例有着不相等的散列码,则要找出原因,并修正错误。

2.3> 始终要覆盖toString

  应该在注释中说明输出字符串中的各部分的意思,使其他人能快速理解输出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值