Effective Java整理笔记
时间:2017/4/28~2017/5/6
文档结构:Effective Java目录结构(没有第一章引言部分)
所有的源代码Github储存仓库:
https://github.com/Wsky51/EffectiveJava
本文档URL图和思维导图开源链接:
https://www.processon.com/view/link/590f2488e4b0230b25bcc588
https://www.processon.com/view/link/590ed5e7e4b014dc3acaaa64
第1章 创建和销毁对象
第1条 考虑用静态工厂方法替代构造器
一是有名称,可以通过名称来选择到底用哪个构造器。
二是不必在每次调用他们的时候创建一个新的对象。
三是他们可以返回类型的任何子类型的对象。
四是在创建参数化类型实例的时候,它们使代码变得更加简洁
比如Map<String,List<String>> m=new HashMap<String,List<String>>随着类型参数越来越长,说明也就越繁琐。静态工厂:
public static <K,V> HashMap<K,V> newInstance(){
return new HashMap<K,V>
}
Map<String,List<String>> m=HashMap.newInstance();
缺点:一是类如果不含有公有的或者受保护的构造器,就不能被子类化。
二是它们与其他的静态方法实际上没有任何区别
第2条 遇到多个构造器参数时要考虑用构建器
由于静态工厂和构造器有个共同的局限性:他们都不能很好地扩展到大量的可选参数。比如有多个参数以上的构造器、一些人一向习惯使用重叠构造器。
package com.wuyi.demo;
/**
* Created by LENOVO on 2017/4/28.
*/
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;
private int fat;
private int carbohydrate;
private int sodium;
public Builder(intservingSize,int servings){
this.servingSize=servingSize;
this.servings=servings;
}
public Builder calories(intval){
this.calories=val;return this;
}
public Builder fat(intval){
this.fat=val;return this;
}
public Builder carbohydrate(intval){
this.carbohydrate=val;return this;
}
public Builder sodium(intval){
this.sodium=val;return this;
}
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder b){
servingSize=b.servingSize;
servings=b.servings;
calories=b.calories;
fat=b.fat;
sodium=b.sodium;
carbohydrate=b.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;
// }
public static voidmain(String[] args) {
NutritionFacts build = new Builder(230, 8).calories(100).sodium(35).carbohydrate(27).build();
System.out.println(build);
}
}
比如,注释的代码部分,对于这种重叠构造器,一句话,重叠构造器可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然难以阅读,要是两个参数不小心写反了,不容易发现。所以,不推荐这种方式。
第二种方式是JavaBean方式,用Setter和Getter方法来获取设置参数。这种模式弥补了重叠构造器的不足。说的直白点就是创建实例很容易,代码易读。但JavaBean自身严重的缺点是在构造过程中JavaBean可能处于不一致的状态。试图使用处于不一致状态的对象会导致失败。JavaBean模式阻止了把类做成了不可变的可能,这就需要人付出额外的努力确保它的线程安全。当对象构造完成,并不允许解冻使用,通过手工冻结对象,可以弥补这些不足,但这种方式十分笨拙,在实践中很少使用。
最后第三种方法,也就上上面代码中没有注销的部分,既能保证像重叠构造器模式那样的安全性,也能保证像JavaBeans模式(JavaBean模式由于多个set方法造成了其线程数据不安全,故不是一种好的方案。个人理解)那么好的可读性。
Java传统的抽象工厂实现是Class对象,用newInstance方法充当build方法的一部分。newInstance方法总是企图调用类的无参构造器,这个构造器甚至是根本不存在的。
Builder模式的不足是为了创建对象,必须先创建它的构建器。虽然创建构建器的开销在实践中可能不明显,但是在某些十分注重性能的情况下,可能就成问题了。Builder模式还比重叠构造器模式更加冗长。
(个人理解总结)总之,重叠构造器是由于它的可读性参数赋值易出错的问题而不建议采用,JavaBeans模式是由于线程安全问题而不采用,通常最好的做法就是一开始就是用Builder构建器。因为它综合了可读性,线程安全的优点。
第3条 用私有构造器或枚举类型强化Singleton属性
为在Java1.5版本之前,实现Singleton有两种方法。这两种方法都要把构造器保持为私有的,并导出为公有的静态成员,以便允许客户端能够访问该类唯一的实例。例如下面代码中注释掉的部分。
public class Elvis {
// public static final Elvis INSTANCE=new Elvis();
// private Elvis(){}
private static finalElvis INSTACE=newElvis();
private Elvis(){
System.out.println("实例化了");
}
public static Elvis getInstace(){
return INSTACE;
}
}
一旦Elvis类被实例化,只会存在一个Elvis实例,不多也不少。客户端的任何行为都不会改变这一点。但是,通过反射机制享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它被要求创建第二个实例的时候抛出异常。
Singleton的第二种方法就是上面代码没注释的部分。为了让Singleton变成可序列化的,仅仅实现Serializable接口还不够。为了维护并保存Singleton,必须声明所有实例域都是瞬时的(transient)的,并提供一个readResolve方法。
另外,从JAVA1.5开始实现Singleton还有第三种方法。借助于enum枚举类型public enum Elvis{
INSTANCE;
public void leaveTheBuilding(){...}
}
它无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。这种单元素的枚举类型是实现Singleton的最佳方法。
第4条 通过私有构造器强化不可实例化的能力
企图通过将类做成抽象来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。由于只有当类不包含显示的构造器时u,编译器才会生成缺省的构造器,因此我们只要让这个类包含私有构造器,它就不能被实例化了。比如说下面的:
public class UtilityClass {
private UtilityClass(){
throw new AssertionError();
}
}
由于显示的构造器是私有的,所以不可以在该类的外部访问它。
简单来说,用private关键字来让类不能实例化。
第5条 避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速又流行。如果对象是不可变的,它就始终可以被重用。
对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。
除了重用那些不可变的对象之外,也可以重用那些已知不会被修改的可变对象。
主要做的就是将经常要用到的那一部分对象或者是值放到static。这样就可以保证当类实例的时候,无论多少次,static修饰的只实例化一次,可以避免重复创建不必要的对象,造成资源的浪费。(个人理解)
另外自动装箱
package com.wuyi.effectiveJava;
/**
* Created by LENOVO on 2017/4/28.
*/
public class AutoBoxing {
public static void main(String[] args) {
Long sum=0L;
for(longi=0;i<Integer.MAX_VALUE;i++){
sum+=i;
}
System.out.println(sum);
}
}
程序答案正确,但是就是因为Long sum=0L,得出结果比实际要慢很多。只是因为打错了一个字符。字符变量被声明成Long而不是long,意味着程序要构造大约2的31次方的Long实例。将sum的声明改为long,程序运行时间极大缩短。
结论就是:要优先使用基本类型而不是装箱基本类型(否则每次都会构造一个实例对象,这对于程序来说时间开销成本很大,所以要避免,个人理解),要当心无意识的装箱。
但是不要错误地认为内容暗示着“创建对象的代价很昂贵,要尽量避免创建对象”,相反,由于小对象的构造器只做很少量的显式工作,所以小对象的创建和回收是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是一件好事。
反之,通过维护自己的对象池来避免创建对象并不是一件好做法,除非池中的对象时非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接池的代价特别昂贵,因此重用对象就非常有意义了。但,一般而言,维护自己的对象池一般会把代码弄得很乱,同时增加内存占用,并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能就很容易超过轻量级对象池的性能。
第6条 消除过期的对象引用
JVM可以帮我们自动地收集垃圾,并不是意味着我们不再考虑内存管理的事了。如packagecom.wuyi.effectiveJava;
import java.util.Arrays;
import java.util.EmptyStackException;
/**
* Created by LENOVO on 2017/4/28.
*/
public class Stack {
private Object[] elements;
private int size=0;
private static final int DEFAULT_INITIAL_CAPACITY=16;
public Stack(){
elements=newObject[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object 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);
}
}
}
这段程序没有明显的错误,无论如何测试都可以通过测试,但是这个程序隐藏着一个问题。这段程序有一个内存泄露,随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。甚至失败,但是失败的情形相对比较少见。
那是哪里发生了内存的泄露呢?比如说栈先增长push进去各种元素,然后再收缩pop(),那么那些从栈中弹出的对象不会被收回,即使使用栈的程序不再引用这些对象,他们也不会收回,比如push(1),push(2),push(3),pop(),pop(),按理来说栈中现在只有1那一个元素,但是2,3这些对象并不会被JVM回收(个人理解),栈内部维护这些对象的过期引用。所谓过期引用是指永远也不会再被解除的引用。在上面的代码中,凡是在elements数组的活动部分之外的任何引用都是过期的,会对性能造成潜在的重大影响。活动部分是指elements中的下标小于size的那些元素。
问题的修复很简单:一旦对象引用已经过期,只需要清空这些引用就可以了。
也就是将对象赋值为null
Pop()方法修改如下
public Object pop(){
if(size==0){
throw new EmptyStackException();
}
elements[size]=null;
return elements[--size];
}
加一行清空代码即可。
还要注意的是,“对于每一个对象引用,一旦程序不再用到它,就把它清空。”其实这样做既没必要,也不是我们所期望的,因为这样做会把代码弄得很乱,清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用的最好方法就是让包含该引用代码的变量结束其生命周期。一般而言,只要类是自己管理内存,我们就应该警惕内存泄露问题,手工清理一些过期引用。
第7条 避免使用终结方法
终结方法通常不可预测。使用终结方法会导致行为不稳定、降低性能,以及可移植性等问题。根据经验,应该避免使用终结方法。在Java中,一般用try-finally块来完成回收各类资源。
显示的终止方法,在每个实例不再有用的时候调用这个方法。显示的终止方法典型的例子是InputStream、OutputStream上面的close方法,显式的终止方法通常与try-catch结构结合。
终结方法有两种合法用途。第一种用途是当所有者忘记调用前面段落中建议的显示终止方法时,终结方法可以充当“安全网”。虽然并不能保证终结方法会被及时的调用。第二种合理用途与对象的本地对等体有关。本地对等体是一个本地对象。总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则不要使用终结方法。
第2章 对于所有对象都通用的方法
第8条 覆盖equals时请遵守通用约定
equals()方法实现了等价关系,自反性,对称性,传递性,一致性。按照字面意思,即是数学中简单理论。
自反性:对于任何非null的对象而言。必须有x.equals(x)为true
对称性:对于任何非null的引用值,x.equals(y)值为true时,y.equals(x)也必须返回为true,比如一个CaseInsensitiveString
package com.wuyi.effectiveJava;
import java.util.ArrayList;
import java.util.List;
/**
* Created by LENOVO on 2017/4/29.
*/
public class CaseInsensitiveString {
private final Strings;
public CaseInsensitiveString(String s){
if(s==null){
throw new NullPointerException();
}
this.s=s;
}
@Override
public booleanequals(Object obj) {
if(obj instanceofCaseInsensitiveString){
return s.equalsIgnoreCase(((CaseInsensitiveString) obj).s);
}
if(obj instanceofString){
return s.equalsIgnoreCase((String) obj);
}
return false;
}
public static void main(String[] args) {
CaseInsensitiveString cis=new CaseInsensitiveString("Polish");
String s="polish";
List<CaseInsensitiveString> list=newArrayList <CaseInsensitiveString>();
list.add(cis);
System.out.println(list.contains(s));//返回false
List<String> slist=new ArrayList <String>();
slist.add(s);
System.out.println(slist.contains(cis));//返回true
}
}
问题在于,虽然CaseInsensitiveString类中的equals方法知道普通的字符串对象,但String类中的equals方法并不知道不区分大小写的字符串。因此.s.equals(cis)返回false,显然违反了对称性。一旦违反了equals规定,但其他对象面对我们的对象时,我们完全不知道这些对象的行为会怎么样。为了解决这个问题。只需把企图与string互操作的这段代码从equals去掉就可以了。
@Override
public boolean equals(Object obj) {
return (obj instanceof CaseInsensitiveString)&&((((CaseInsensitiveString) obj).s.equalsIgnoreCase(s)));
}
个人总结:equals方法不要出现与当前类型不一致的情况的判断,否则另外的那个类的equals方法不会通过判断,造成违反对称性的情况。
传递性,1=2,2=3,那么一定有1=3。
public class ThreePoint{
private final Point point;
private final int z;
public ThreePoint(intx,int y,intz){
point=newPoint(x,y);
this.z=z;
}
public Point asPoint(){
return point;
}
@Override
public booleanequals(Object obj) {
if(!(obj instanceof ThreePoint)){
return false;
}
ThreePoint threePoint = (ThreePoint) obj;
return threePoint.point.equals(point)&&threePoint.z==z;
}
}
根据16条的建议,复合优先于继承。这样做的目的保证不同的类之间的equals方法返回的是false。
个人总结:只要是在equals方法里面严格控制类的类型判断和逻辑语句的equals判断,便不会出现违反传递性原则的情况
一致性,如果两个对象相等,那么它们就必须始终保持相等,除非它们中有一个对象被修改了。当在写一个类的时候,应该仔细考虑它是否应该是不可变的。如果认为它应该是这样不可变的,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等。无论类是否是可变的,都不要使equals方法依赖于不可靠资源。如果违反,那想要满足这样的一致性的要求就十分困难。
非空性:比较对象不允许为空。且在书写equals代码的时候应该进行类型检查,不是同一个类型的直接返回false
总结,书写高质量的equals方法的要则如下:
1.使用==操作操作符检查“参数是否为这个对象的引用”。如果是,则返回true。这只不过是一种性能优化,如果比较操作会很昂贵的话,就值得这样做。
2.使用instanceof操作符检查“参数是否为正确类型”。若果不是,则返回false。一般来说正确的类型就是写equals方法的那个类。有些情况是指该类所实现的某个接口。如果类实现的接口改进了equals规定,允许在实现了该接口的类之间的比较,那么就使用接口。集合接口都具有这样的特性
3.把参数转化成正确的类型。因为之前进行过instanceof判断,所以确保会成功。
4.对于该类中每一个关键域,检查参数中的域是否与该域对象中对应的域相匹配。另外,在这之前一定要确保是可访问的。注意一点的是,对于基本类型用==号就可以了(除了float和double,他们应该用Float.compare和Double.compare(a1,a2)方法,相等返回0,大于返回1,小于返回-1).对于对象引用域,调用equals方法。对于数组用Arrays.equals方法。域的比较顺序可能会影响到equals方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域。
范式是什么??
5.当编写完equals方法的时候,应该问自己是否满足对称性传递性一致性。如果否定就必须要更改equals方法代码。最后编写equals方法总要覆盖hashcode()方法,不要试着让equals方法过于智能。不要将equals声明中的Object对象替换为其他的类型。
public boolean equals(Object obj) {
if(this==obj){
return true;
}
if(!(obj instanceof ThreePoint)){
return false;
}
ThreePoint threePoint = (ThreePoint) obj;
return threePoint.point.equals(point)&&threePoint.z==z;
}
public static void main(String[] args) {
ThreePoint p1=null;
ThreePoint p2=new ThreePoint(1,2,3);
// System.out.println(p1.equals(p2));//抛出NullPointerException异常
System.out.println(p2.equals(p1));//返回false
}
第9条 覆盖equals时总要覆盖hashCode
一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖了hashCode方法。如果不这样,就会违反Object.hashCode规定。从而导致该类无法结合所有基于散列的集合一起运作。
·在应用程序执行的过程中,只要对象的equals方法的比较操作所用到的信息没有被更改,那么对于这同一个对象调用多次,hashCode方法必须始终如一地返回同一个整数。
·如果两个对象根据equals(Object)方法比较的是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。如果不同,那么调用hashCode方法有可能在逻辑上也是相等的,但是我们应该尽量让他们产生截然不同的hashCode以提高hashTable的性能。
所以没有覆盖hashCode违反了Object规范。
一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”。理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。但要想完全达到这种理想的情形是非常困难的。但相对接近这种理想情况并不困难。以下原则步骤:
1,把某个非0的常数值,比如说17,保存在一个名为result的int类型的变量中。
2.对于对象中每个关键域f(指equals方法中涉及的每个域),完成下面步骤。
a.为该域计算int类型的散列码c:
1.如果是boolean类型,true设值为1false设置0
2.如果是byte,char,short,计算(int)f
3.如果该域是long类型,则计算(int)(f^f(>>>32))
4.如果是float,计算Float.floatToIntBits(f)
5.如果是double类型,则计算Double.doubleToLongBits(f)然后按照第三条计算。
6.如果是一个对象引用,则调用hashCode方法。
7.如果是个数组,则把每一个元素当做单独的域处理。如果数组每一个元素都很重要,则利用Arrays.hashCode()方法。
b.按照公式,把步骤2.a计算得到的散列码c合并到result中:
result=31*result+c;(之所以选择31,是因为他是一个奇素数。如果是偶数,并且乘法溢出的话,信息就会丢失,因为与2乘等于移位运算。使用素数的好处并不很明显,但是习惯上都使用素数计算散列结果。31有个很好的特性,即用移位和减法来代替乘法,可以得到更高的性能:31*i==(i<<5)-i)
3.返回result
4.写完测试。
如下面的代码,equals方法是按照第8条书写得:
package com.wuyi.effectiveJava;
/**
* Created by LENOVO on 2017/4/29.
*/
public class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(intareaCode,int prefix,intlineNumber){
this.areaCode= (short) areaCode;
this.prefix= (short) prefix;
this.lineNumber= (short) lineNumber;
}
@Override
public booleanequals(Object obj) {
if(obj==this){
return true;
}
if(!(obj instanceof PhoneNumber)){
return false;
}
PhoneNumber p = (PhoneNumber) obj;
return p.areaCode==areaCode&&p.prefix==prefix&&p.lineNumber==lineNumber;
}
@Override
public inthashCode() {
int result=17;
result=31*result+areaCode;
result=31*result+prefix;
result=31*result+lineNumber;
return result;
}
}
如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。
不要试图从一散列码计算中排除一个对象的关键部分阿里提高性能。
第10条 始终要覆盖toString
如打印上一个PhoneNumber的实例的时候,输出的是com.wuyi.effective Java.PhoneNumber@9b448依次是类的名称,@号和无符号的十六进制表示法。查看源码:returngetClass().getName() + "@"+ Integer.toHex String(hashCode());建议所有的子类都覆盖这个方法。它可以使类使用起
来更加舒适
第11条 谨慎地克隆clone
Cloneable接口的目的是作为对象的一个mixin接口,表明一个对象允许这样的克隆。但它并没有达到这个目的。其主要缺陷在于它缺少一个clone方法,Object的clone方法是受保护的。如果实现Cloneable接口是要对某个类起到作用,类和他的超类都必须遵循一个相当复杂,不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的机制:无需调用构造器就可以创建对象。
拷贝对象往往会导致创建它的类的一个新实例,但它同时也会要求拷贝内部的数据结构。这个过程中没有调用构造器。但是,不调用构造器的规定太强硬了。行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。以下的部分代码
public class PhoneNumberimplements Cloneable
@Override
protected Object clone()throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args)throws CloneNotSupportedException {
PhoneNumber p=new PhoneNumber(123,355,321);
PhoneNumber clone = (PhoneNumber)p.clone();
System.out.println(p.getClass()==clone.getClass());//true
System.out.println(clone.prefix);//355
}
如果希望在一个类中实现Cloneable,并且它的超类都提供行为良好的clone方法。我们从clone()中得到的对象可能会接近于最终要返回的对象,也可能相差甚远,这要取决于这个类的本质。从每个超类的角度来看,这个对象将是原始对象功能完整的克隆。在这个类中声明的域将等同于被克隆对象中相应的域。另外,1.5版本引入了协变返回类型作为泛型,就是说覆盖@Override方法的返回类型可以是被覆盖方法的返回类型的子类。如下
@Override
protected PhoneNumber clone()throws CloneNotSupportedException {
return (PhoneNumber)super.clone();
}
这体现了一点:永远不要让客户去做任何类库能够替客户完成的事情。
如果对象中包含域引用了可变的对象就像之前的Stack这个类。由于private Object[] elements是可变的,使用上述这种简单的clone实现可能会导致灾难性的后果。实际上clone方法就是另外一个构造器,必须确保它不会伤害到原始对象,并确保正确地创建被克隆对象中的约束条件。所以为了使Stack类中的clone方法正常工作,它必须要拷贝栈的内部信息。最容易的做法就是在elements数组中递归地调用clone,就像这样:
@Override
protected Stack clone()throws CloneNotSupportedException {
Stack result = (Stack) super.clone();
result.elements=elements.clone();
return result;
}
推论:clone是潜复制,不存在对象引用(个人理解,可能有偏差错误)
所以如果一个对象是final的时候,就不能复制了
简而言之,所有实现Cloneable接口的类都应该用一个公有的方法覆盖clone。此公有方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用代替原来指向这些对象的引用。
另外一个实现对象拷贝的好办法就是提供一个拷贝构造器或拷贝工厂。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类。如:public Yun(Yum yum);拷贝工厂是类似于拷贝构造器的静态工厂:public static Yum newInstance(Yum yum);Cloneable具有上述那么多问题,所以可以肯定地说,其他的接口都不应该扩展这个接口,为了继承而设计的类也不应该实现这个接口。有些专家级别的程序员干脆从来不去覆盖clone方法,也不调用它,除非拷贝数组。
第12条 考虑实现Comparable接口
重写其中的int compareTo(T t)方法。CompareTo方法的通用约定与equals方法相似。将这个对象与指定对象比较,当该对象小于、等于、或大于指定对象的时候,分别返回一个负整数,0或者正整数。如果由于指定对象的类型而无法与该对象比较,则抛出ClassCastException异常。同样由compareTo方法施加的同等性测试,也一定遵循相同于equals约定所施加的限制条件:自反性,对称性,传递性。
另外,注意一点,书上第55页中,谈到BigDecimal类,它的compareTo方法和equals方法不一致。如果我们创建了HashSet,并添加new BigDecimal(“1.0”)和new BigDecimal(“1.00”)那么最终会有两个实例,因为BigDecimal类的compareTo方法和equals不一致。然而,如果使用TreeSet而不是HashSet执行同样的过程,集合中将只保存一个元素,因为这两个BigDecimal实例在通过compareTo方法进行比较的时候是相等的。
还要注意一点:@Override
public int compareTo(PhoneNumber o) {
if(areaCode>o.areaCode){
return 1;
}
if(areaCode<o.areaCode){
return -1;
}
return 0;
}
这个方法可行,但是还可以改进。因为compareTo并没有指定返回值的大小,而是指定了返回值的符号。可以利用这一点来简化代码,或许能提高速度。
@Override
public int compareTo(PhoneNumber o) {
int res=areaCode-o.areaCode;
if(res!=0){
return res;
}
return 0;
}
这项技巧在这里可以工作的很好,但是用起来要非常小心。除非确信相关的域不会为负值,或者更一般的情况:最大和最小的可能域值差小于或等于INTEGER.MAX_VALUE(2^31-1),否则就不要用这种方法。这不是一个纯粹的理论问题:它已经在实际的系统中导致了失败。这些失败可能非常难以调试,因为这样的compareTo方法对于大多数的输入值都能正常工作。
第3章 类和接口
第13条 使类和成员的可访问性最小化
区分设计良好的模块与设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。然后,模块之间只通过他们的API通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为信息隐藏或封装,是软件设计的基本原则
如果一个包级私有的顶层类只是在某一个类的内部被用到,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。这样可以将它的可访问性范围从包中的所有类缩小到了使用它的那个类。但是降低不必要公有类的可访问性,比降低包级私有的顶层类的更重要的多。
实例域(什么是实例域???)绝对不能是公有的。同样的建议也适用于静态域,只有一种例外情况。类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容。这是安全漏洞的一个常见根源。
总而言之,应该尽可能地降低可访问性。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都是不可变的。
第14条 在公有类中使用访问方法而非公有域
类的数据域永。远不要被暴露。应该提供封装功能。
Class Point{
public double x;//数据域被暴露
public double y;//同上,用getter和setter方法
}
比如Javabean这种方法。如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误----假设这些数据域确实描述了该类所提供的抽象。这种方法比访问方法的做法更不会产生视觉混乱。让公有类直接暴露域虽然从来都不是种好方法,但是如果域是不可变的,这种做法的危害就比较小一些。
总之,公有类永远都不应该暴露可变的域。虽然还是会有问题,但是让公有类暴露不可变的域其危害比较小。但有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是可变还是不可变的。
第15条 使可变性最小化
不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。存在不可变的类有许多理由:不可变的类比可变类更加易于设计、实现和使用。他们不容易出错,且更加安全。让类成为不可变,要遵守下面5条原则:
1.不要提供任何会修改对象状态的方法。
2.保证类不会被扩展,一般做法是加final
3.使所有的域都是final的
4.使所有的域都变成私有的
5.确保对任何可变组件的互斥访问
不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏。这无疑是获得线程安全最容易的办法。实际上,没有任何线程会注意到其他线程对于不可变对象的影响。所以,不可变对象可以被自由的共享,不可变类应该充分利用这种优势,鼓励客户端尽可能地重用现有的实例。要做到这一点,一个很简单的方法就是对于频繁使用的值,加final。
不可变对象可以被自由地共享导致的结果是,永远也不需要进行保护性拷贝了。因为这些拷贝始终等于原始对象,所以不应该为不可变的类提供clone方法或者拷贝构造器。不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。创建这种对象的代价可能很高。特别是对于大型对象的情形。
为了确保不可变性,类绝对不允许自身被子类化。除了“使类成为final”这种方法之外,还有另外一种更加灵活的办法可以做到这一点。让不可变的类变成final的另外一种方法就是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器。如下所示:
public class Complex {
private final double re;
private final double im;
private Complex(doublere,double im){
this.re=re;
this.im=im;
}
public static Complex valueOf(doublere,double im){
return new Complex(re,in);
}
}
它经常是最好的替代方法。它最灵活,因为它允许使用多个包级私有的实现类。这样静态工厂具有很多其他的优势,在第一条中已经写到过。
总之,坚决不要为每一个get方法编写一个相应的set方法。除非有很好的你有要让类成为可变的类,否则就应该是不可变的。不可变的类有许多优点,唯一的缺点是在特定的情况下存在潜在的性能问题。只有当我们确认有必要实现令人满意的性能时,才应该为不可变类提供公有的可变配套类。如果类不能被做成是不可变的,仍然应该尽可能地限制他的可变性。因此除非是有令人信服的理由要是域变成是非final的,否则要使每个域都是final的
第16条 复合优先于继承
对于同一个包的继承是非常安全的,因为是在同一个程序员的控制之下。但是对于普通的具体类进行跨越包边界的继承,则是非常危险的。
与封装不同,继承打破了封装性。举例说明:
public class InstrumentedHashSet<E>extends HashSet<E> {
private int addCount;
public InstrumentedHashSet(){}
public InstrumentedHashSet(intinitCap,float loadFactor){
super(initCap,loadFactor);
}
@Override
public booleanadd(E e) {
addCount++;
return super.add(e);
}
@Override
public booleanaddAll(Collection<? extendsE> c) {
addCount+=c.size();
return super.addAll(c);
}
public int getAddCount(){
return addCount;
}
public static void main(String[] args) {
InstrumentedHashSet<String> s=newInstrumentedHashSet <String>();
s.addAll(Arrays.asList("t1","t2","t3"));
System.out.println(s.getAddCount());//返回6
}
}
类看起来合情合理但是不能工作。addCount应该是3,但结果是6。出错的原因就在于在HashSet的内部,addAll方法是基于它的add方法来实现的,虽然只要去掉addALL方法就可以修正这个子类。虽然得到的类可以正产工作,但是不能保证随着发行版主JDK的不同而变化,因此这样的类是很脆弱的。导致子类脆弱的一个相关的原因就是他们的超类在后续的发行版本中可以获得新的方法。
上面问题的来源都来自于覆盖动作,如果扩展一个类的时候,仅仅是增加新的方法,而不覆盖现有的方法,这也并非完全没有风险,因为如果后期JDK中对超类添加一个方法,但这个方法的名字和我们扩展类中一个方法同名问题就又出现了。
有一种方法可以避免所有问题,就是在新类中增加一个私有域,它引用现有类的一个实例。这种设计被称作复合。
只有当子类真正是超类的子类型时,才适合继承,另外当试图扩展类的时候,还会把超类的API缺陷扩展到子类当中。
第17条 要么为继承而设计,并提供文档说明,要么就禁止继承
首先,该类的文档必须精确地描述覆盖每个方法所带来的影响。换句话说,就是该类必须有文档说明它可覆盖的方法的自用性,详细说明那些方法那些构造器是可覆盖的方法,是以什么顺序调用的等等。
好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。对于为了继承而设计的类,唯一的测试方法就是编写子类。另外,构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。如下面这段程序:public class Super {
public Super(){
overrideMe();
}
public void overrideMe(){
System.out.println("Super()");
}
}
public class Sub extends Super{
private final Date date;
Sub(){
date=newDate();
}
@Override
public voidoverrideMe() {
System.out.println(date);
}
public static void main(String[] args) {
Sub sub=new Sub();
sub.overrideMe();
}
}
返回结果null
Sun Apr 30 17:47:52 CST 2017
原因就是在Super调用构造器的时候,OverrideMe方法内打印日期这个时候日期没有被初始化,所以打印为null,如果在overrideMe里面打印日期里的某一种方法,则会抛出NullPointerException异常。如果决定在一个为了继承而设计的类中实现cloneable和Serializable接口,就应该意识到,因为clone和readObject方法在行为上非常类似于构造器,应
很明显,为了继承而设计类,对这个类会有一些实质性的限制。在某些情况下,这样的决定是正确的,比如抽象类,包括接口的骨架实现。但是另外一些情况,这样的决定是明显错误的,比如不可变的类。
对于这个问题最佳的解决方法是,对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。方有两种方法可以禁止子类化,一种是把类声明为final的,另外一种就是把所有的构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来代替构造器
第18条 接口优于抽象类
通过导出的每个重要的接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。按照惯例,骨架实现被称为AbstractInterface,比如AbstractSet
设计接口必须非常谨慎,接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。我们必须在初次设计的时候就保证接口是正确的。
简而言之,接口通常是定义允许多个实现的类型的最佳途径。如果导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类
第19条 接口只用于定义类型
有一种接口为常量接口,interface里面的常量都默认为static和final的,不可改变。但常量接口模式是对接口的不良使用。实现常量接口,会导致把这样的实现细节西楼到该类的导出API中去。另外,非final类实现了这个接口的话,它的所有子类的命名空间也会被接口中的常量所污染。总之,接口应该只被用来定义类型,他们不应该被用来导出常量。
第20条 类层次优于标签类
eg下面的标签类:
public class Figure {
enum Shape{RECTANGLE,CIRCLE};
final Shape shape;
double length;
double width;
double radius;
Figure(double radius){
shape=Shape.CIRCLE;
this.radius=radius;
}
Figure(double length,doublewidth){
shape=Shape.RECTANGLE;
this.length=length;
this.width=width;
}
double area(){
switch (shape){
case CIRCLE:
return Math.PI*(radius*radius);
case RECTANGLE:
return length*width;
default:throw newAssertionError();
}
}
这种标签类有许多缺点,因为实例承担了属于其他风格的不相关的域,一个花,标签类过于冗长、容易出错,并且效率低下。标签类正是类层次的一种简单的仿效。
abstract class Figure{
abstract double area();
}
class Circle extendsFigure{
final double radius;
Circle(double radius){this.radius=radius;}
double area(){returnMath.PI*(radius*radius);}
}
class Rectangle extends Figure{
final double length;
final double width;
Rectangle(double length,doublewidth){
this.length=length;
this.width=width;
}
@Override
doublearea() {
return length*width;
}
}
类层次保证了确保对每个抽象方法都有一个实现。这样杜绝了swithch case而导致运行时失败的可能性。总之,标签类很少有适用的时候。当我们想要编写一个包含显示标签域的类时,应该考虑一下,这个标签类是否可以取消,这个类是否可以用类层次来代替。
第21条 用函数对象表示策略
Java没有提供函数指针,但是可以用对象引用,调用对象上的方法通常是执行该对象上的某项操作。例如我们进行实现comparator的compare(T a,T b )方法。
简而言之,函数指针的主要用途是实现策略模式。为了在java中实现这一模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体的策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域导出,其类型为该策略接口。
第22条 优先考虑静态成员类
嵌套类有四种:静态成员类,非静态成员类,匿名类,和局部类。除了第一种,其他都称为内部类。静态成员类和非静态成员类之间的唯一的区别是static修饰符。尽管语法很相似,但这两种嵌套类有很大的不同。非静态成员类的每一个实例都隐含着与外围类的一个外围实例相关联,在没有外围实例的情况下,想要创建非静态成员类的实例是不可能的。
另外,匿名类出现在表达式中,必须简短否则会影响到程序的可读性。局部类也一样,如果太长,就把他做成成员类。
第4章 泛型
第23条 请不要在新代码中使用原生态类型
每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称(泛型的擦除机制),例如,List<String>相对应的原生态类类型是List。
使用泛型最主要的目的是出于安全性和表述性方面的考虑。泛型有子类化的规则,List<String>是原生态类型;List的一个子类型,而不是参数化类型List<Object>的子类型。因此如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List<Object>这样的参数化类型,则不会。
public static void main(String[] args) {
List<String> strings=new ArrayList <String>();
unsafeAdd(strings,new Integer(1));
String s = strings.get(0);
}
static void unsafeAdd(List list,Object obj){
list.add(obj);
}
不要在新代码中使用原生态类型,这条规则有两条例外,两者都源于“泛型信息可以在运行时被擦除”这一事实。在类文字中必须使用原生态类型。“泛规范不允许使用参数化类型(虽然允许数组类型和基本类型),换句话说Lisy.class,String[].class和int.class都合法,但是List<String.class>和List<?>.class则不合法。
第二条例外与instanceof操作符有关。由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符类型山使用instanceof操作符是非法的。用无限制通配符类型代替原生态类型,对Instanceof操作符的行为不会产生任何影响。这个时候<>和?就显得多余了。下面是利用泛型来使用instanceof的首选方法:
if(o instancof Set){
Set<?> m=(Set<?>)o;
}
注意,一旦确定这个o是Set,就必须把他转换成通配符类型Set<?>,而不是转换化成原生态类型Set,这是受检验的转化,因此不会导致编译警告。
总结:Set<Object>是个参数化类型,表示可以包含任何对象类型的一个集合;Set<?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合;Set则是个原生态类型,它脱离了泛型系统,前两种是安全的,最后一种是不安全的。
第24条 消除非受检警告
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下才可以用一个@SuppressWarnings(“unchecked”))注解来禁止这条警告。应该在尽可能小的范围内使用SuppressWarnings注解。每当使用Suppress Warnings这个注解时,都要添加一条注释,说明为什么这么做是对的。更重要的是,可以尽量减少其他人修改代码后导致计算不安全的概率。要尽可能最大的努力消除这种警告。
第25条 列表优先于数组
数组与泛型,有两个重要的不同点。一,数组是协变的,但是泛型不是(由于擦除机制),我们可能认为,泛型是有缺陷的,但实际上可以说数组才是有缺陷的。
List<Object> ol=newArrayList<Long>();
ol.add("dasdasd");//编译的时候就不通过
Object[] array=newLong[1];
array[0]="dsadas";//运行的时候抛出ArrayStoreException异常
上面两种都不能将String放进容器中,但是数组是在运行的时候才知道不行,而列表则在编译的时候就发现错误了。显然更好。
数组与泛型的第二个区别,数组是具体化的。因此数组会在运行时才知道并检查他们的元素类型约束。相比之下,泛型则是通过擦除来实现的。因此泛型只在编译时强化他们的类型信息,并在运行时丢弃他们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。(极为重要!!!)
正是上面的区别,所以数组和泛型不能很好的混合使用。例如,创建泛型,参数化类型或者类型参数的数组是非法的。
为什么创建泛型数组是非法的?因为它不是类型安全的。
当我们得到的泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或者简洁性,但是换回的却是更高的类型安全性和互用性。
第26条 优先考虑泛型
private E[]elements;
public void f(){
elements=(E[])new Object[10];
}
如第25条中所述,你不能创建不可具体化的类型的数组,如E。每当编写用数组支持的泛型的时候,都会出现这个问题。解决办法有两种,一,直接绕过泛型数组的禁令,创建一个Object的数组,并将它转化为数组类型。如上面的代码,编译器不能证明我们的程序是安全的,但我们可以证明且必须确保未受检验的转换不会危及到程序的类型安全性。一旦证明了未受检的转化是安全的,就要尽可能小的范围内禁止警告。
第二种方法是将elements域的类型从E[]改为Object[]。
E result=(E)elements[--size];
return result;
至于具体选择哪一种,看个人喜好,出于安全性问题,建议选择第二种方案,但是由于第二种方案要多次转化成E,而不是只转化成E[],这也是第一种方案之所以更加常用的原因。
总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保他们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。只要时间允许,就要把所有的类型都泛型化。
第27条 优先考虑泛型方法
就如同类泛型化可以从中受益一样。方法也一样。静态工具方法尤其适合于泛型化。Collections中的所有算法方法都实现泛型化了。例如下面
// public static Set union(Set s1,Set s2){
// Set result=new HashSet(s1);
// result.addAll(s2);
// return result;//方法可以编译,但有两条警告
// }
public static<E> Set<E> union(Set<E> s1,Set<E> s2){
Set<E> result=newHashSet <E>();
result.addAll(s2);
return result;
}
应该优先考虑泛型的方法。union方法的局限性在于,三个集合的类型必须全部相同。利用有限性的通配符类型,可以使这个方法变得更加灵活。
第28条 利用有限制通配符来提升API的灵活性
为了获得最大限度的灵活度,要在表示生产者或者消费者的输入参数上使用通配符类型。本条建议的原型可以参考Thinking in Java里面的泛型通配符和上界下界那一部分。PECS表示:producer-extends,consumer-super。
第29条 优先考虑类型安全的异构容器
原型可以参
第5章 枚举和注解
第30条 用enum代替Int常量
Java的枚举本质上是int值,枚举类型是真正的final。因为客户端既不能创建枚举类型实例,也不能对他进行扩展,因此很可能没有实例,而只有声明过的枚举常量。也就是说枚举是实例受控的。比较好可以构造单例模式。
为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域都应该为final的packagecom.wuyi.effectiveJava;
/**
* Created by LENOVO on 2017/5/2.
*/
public enum Plant {
MERCURY(3.302e+23,2.439e6),
VENUS(4.869e+24,6.052e6),
Earth(5.975e+24,6.378e6),
MARS(6.419e+23,3.393e6);
public double mass;
private final double radius;
private final double surfaceGravity;
private static final double G=6.67300E-11;
Plant(double mass,doubleradius){
this.mass=mass;
this.radius=radius;
surfaceGravity=G*mass/(radius*radius);
}
public double mass(){returnmass;}
public double radius(){returnradius;}
public double surfaceGravity(){returnsurfaceGravity;}
public double surfaceWeight(doublemass){
return mass*surfaceGravity;//F=ma
}
public static void main(String[] args) {
dasdsa.main(args);
}
}
class dasdsa{
public static void main(String[] args) {
Plant p=Plant.MERCURY;
System.out.println(p.mass);
}
}
以上的枚举示例对于大多数枚举方法来说已经够了
枚举常量的方法最好事先乘私有的或者包级私有的方法,每个枚举常量都带有一组隐匿的行为,这使得包含该枚举的类或者包在遇到这种常量时都可以做出适当的反应。除非迫不得已要将枚举方法导出至他的客户端,否则都应该将他声明为私有的,如果有必要则声明为包级私有的。
package com.wuyi.effectiveJava;
/**
* Created by LENOVO on 2017/5/2.
*/
public enum Operation {
PLUS("+"){doubleapply(double x,doubley){return x+y;}},
MINUS("-"){doubleapply(double x,doubley){return x-y;}},
TIMES("*"){doubleapply(double x,doubley){return x*y;}},
DIVIDE("/"){doubleapply(double x,doubley){return x/y;}};
private final Stringsymbol;
Operation(String symbol){
this.symbol=symbol;
}
abstract double apply(doublex,double y);
public static void main(String[] args) {
test.main(args);
}
public void f(){
System.out.println("这是枚举方法");
}
@Override
publicString toString() {
return symbol;
}
}
class test{
public static void main(String[] args) {
double a=2.0,b=4.0;
int c=0;
for(Operation o:Operation.values()){
double result = o.apply(a, b);
System.out.print(result);
}
}
}
不另外,从代码的可读性来看,在枚举类型中添加switch语句十分简洁,但是一旦出现了后期更改枚举类,比如添加一个新的元素的时候,维护起来就很麻烦,总之,不要轻易地使用switch语句。一般来说,枚举会优先使用comparable而非int。与int常量相比,枚举有个小小的性能缺点,即装载和初始化枚举时会有空间和时间的成本。
什么时候应该用枚举,当需要一组固定的常量的时候,如行星,菜单等等。如果多个枚举常量同时共享相同的行为,那么要考虑策略模式。
第31条 用实例域代替序数
枚举类型不要用ordinal()这个方法,虽然这个枚举不错,但是维护起来就像一场噩梦。有一种很简单的办法解决这个问题,就是永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。
Enum规范中谈到Ordinal()这个方法,:大多数程序员并不需要这个方法,它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构。除非我们是在编写这种数据结构,否则最好完全避免使用ordinal方法。
第32条 用EnumSet代替位域
谈将枚举类型的元素用在集合中,一般用int枚举模式,这种将or位运算符将几个常量合并到一个集合中,称作位域。Java.util包提供的EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合。在内部的具体实现细节上,每个EnumSet内容都表示为位矢量。总而言之,正是因为枚举类型要用在集合中,所以没有理由用位域来表示它。我们用EnumSet
第33条 用EnumMap代替序数索引
这一条和第32条类似,只不过说是将EnumSet和EnumMap用在不同的地方而已。总而言之,最好不要用序数来索引数组,而要使用EnumMap。
第34条 用接口模拟可伸缩的枚举
虽然枚举类型(不可以继承枚举类型和其他类,默认为final的,自己标注)不是可扩展的,但接口类型则是可扩展的,这就给枚举类型一个扩展的空间。使用这种模式,就有可能让一个枚举类型去扩展另一个枚举类型。注意在枚举中,不必像在不可扩展的枚举中所做的那样,利用特定于实例的方法实现来声明抽象的apply方法。这是因为抽象的方法是接口的一部分。
第35条 注解优先于命名模式
JDK1.5之前,一般用命名模式来表明有些程序元素需要通过某种工具或者框架进行特殊处理。但是这种方法有一些缺点:1是文字拼写错误会导致失败。2是无法确保他们只用于相应的程序元素上。3是他们没有提供将参数值与程序元素关联起来得好办法。所以在jdk1.5之后,我们优先采用注解这种模式。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interfaceTest {
}
既然有了注解,就完全没理由再使用命名模式了,所有的程序员都应该使用Java平台所提供的预定义的注解类型。
第36条 坚持使用@Override注解
总之一句话,对于我们重新覆盖重写的方法,一定都要加@Override方法就可以了,这可以保证我们避免出现各种不必要的错误。
第37条 用标记接口定义类型
标记接口是没有包含方法声明的接口,而只是指明了一个类实现了具有某种属性的接口。例如Serializable接口实现对象的序列化。标记接口有两点胜过标记注解:1是标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。2是他们可以被更加精确地进行锁定
至于什么时候用标记接口什么时候用标记注解,很显然,如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口可以用来实现或者扩展接口,如果标记只是应用给类和接口,就要问问自己我要编写一个还是多个只接受有这种标记的方法呢》如果是,就应该优先使用标记接口而非注解。
总之,标记接口和标记注解都各有用处。如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择。如果想要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择。
第6章 方法
第38条 检查参数的有效性
绝大多数方法和构造器对于传递给他们的参数值都会有些限制。比如有些对象的引用不能是null。对于未被导出的方法,作为包的创建者,我们可以控制这个方法将在哪些情况下被调用,因此也可以,也应该确保只将有效的参数值传递进来。因此,对非公有的方法通常应该使用断言assert来检查他们的参数比如:assert a!=null;断言如果失败,则会抛出AssertionError。但不要从这条内容中得出这样的结论:对参数的任何限制都是好事。相反,在设计方法时,应该使他们尽可能地通用,并符合实际的需要。假如方法对于它能接受的所有参数值都能完成合理的工作,对参数的限制就应该是越少越好。然而,通常情况下,有些限制对于被实现的抽象来说是固有的。总之,每当编写方法或者是构造器时,应该考虑他的参数有哪些限制。并写到文档中去。
第39条 必要时进行保护性拷贝
进行保护性拷贝的目的是避免内部信息受到攻击,对于构造器的每个可变参数进行保护性拷贝是必要的。保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是对针对拷贝之后的对象,而不是对原始对象。
package com.wuyi.effectiveJava;
import java.util.Date;
/**
* Created by LENOVO on 2017/5/3.
*/
public class Period {
private final Date start;
private final Date end;
public Period( Date start, Date end){
if(start.compareTo(end)<0){
throw new IllegalArgumentException(start+"after"+end);
}
this.start=newDate(start.getTime());
this.end=newDate(end.getTime());
}
public Date getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
@Override
publicString toString() {
return start+":"+end;
}
public static void main(String[] args) {
Date start=new Date();
Date end=new Date();
Period p=new Period(start,end);
System.out.println(p);
p.getStart().setYear(78);
System.out.println(p);
}
}
比如上面代码(P160页书上的例子)就采用了保护性拷贝,这样做,就可以使这些域真正地封装在了对象的内部。
参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果是否定的,就必须对该对象进行保护性拷贝,并且让拷贝对象而不是原始对象进入到数据结构中。因此,在把内部数组返回给客户端之前,应该总要进行保护性拷贝。
第40条 谨慎设计签名方法
1.谨慎地选择方法的名称,方法的名称应该始终遵循标准的命名习惯
2.不要过于追求提供便利的方法
3.避免过长的参数列表
第41条 慎用重载
package com.wuyi.effectiveJava;
import java.math.BigInteger;
import java.util.*;
/**
* Created by LENOVO on 2017/5/3.
*/
public class CollectionClassfier {
public static String classify(Set<?> set){
return "Set";
}
public static String classify(List<?> list){
return "List";
}
public static String classify(Collection<?> c){
return "Unknown Collection";
}
public static void main(String[] args) {
HashSet <String> strings = new HashSet <>();
Collection<?>[] collections={newHashSet <String>(), new ArrayList <BigInteger>(),new HashMap<String,String>().values()};
for(Collection<?> c:collections){
System.out.println(classify(c));
}
System.out.println(classify(strings));
}
}
该方法打印三次”Unknown Collection”,原因是因为classify方法被重载了,而要调用哪个重载方法是在编译时做出决定的
对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的。选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行时类型,当一个子类包含的方法声明与其祖先类中的方法声明具有同样的签名时,方法就被覆盖了。选择工作是在编译时进行的,完全基于参数的编译时的类型。因此,应该避免胡乱地使用重载机制。
总之“能够重载方法”并不意味着就“应该重载方法”。对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。
第42条 慎用可变参数
量可变参数int sun(int... args){}在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果凭经验确定无法承受这一成本,但是又需要可变参数的灵活性,还有一种模式可以如愿以偿。假设确定对某个方法95%的调用会有3个或者更少的参数就声明该方法的5个重载,每个重载方法带有0到3个普通参数,参数数目超过3个就进行可变参数方法。
总之,在定义参数数目不定的方法时,可变参数是一种很方便的方法,但是不应该被过度滥用。如果使用不当,会产生混乱的结果。
第43条 返回零长度的数组或者集合,而不是null
返回null而不是零长度的数组也会使返回数组或者集合的方法本身变得更加复杂。(不必要的代码量更多)
返回类型为数组或集合的方法没理由返回null,而不是返回一个零长度的数组或者集合。
第44条 为所有导出的API元素编写文档注释
文档编写要则:
1.应该简洁地描述出它和客户端之间的约定,这个约定应该说明这个方法做了什么,而不是说明它如何做到的
2.文档注释应该列举出这个方法的所有前提条件和后置条件。一般,前提条件是由@throws标签针对未受检的异常所隐含的描述;每一个未受检的异常都对应一个前提违例。同样我们也可以在一些受影响的参数的@param标记中指定前提条件。
3.除了前提条件和后置条件,每个方法还应该在文档中描述它的副作用。所谓副作用是指系统状态中可以观察到的变化。
4.方法的文档中应该让每个参数都有一个@param标签,以及一个@return标签,以及对于该方法抛出的每一个异常,无论是受检的还是未受检的,都有一个@throws标签。举例如下:
/**
* Retrun the elements at the specified position in this list
* <p>
* <p>This method is<i>not</> guaranteed to run in constant
* time.In some implements it may run in time proportional
* to the element position
*
* @paramindex args of element to ruturn;must be
* non-negative and less than size of this list
* @returnthe element at the specified position in this list
* @throwsIndexOutOfBoundsException if the index is out of range
* ({@codeindex<0||index>=this.size()})
*/
第7章 通用程序设计
第45条 将局部变量的作用域最小化
将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。过早地声明局部变量不仅会使它的作用域过早地扩展,而且结束得也过于晚了。局部变量的作用域从它被声明的点开始扩展,一直到外围块的结束处。如果变量是在“使用它的块”之外被声明的,当程序退出该快之后,该变量仍是可见的,几乎每一个局部变量的声明都应该包含一个初始化表达式。
第46条 将for-each循环优先于传统的for循环
for-each循环在简洁性和预防Bug方面有着传统的for循环无法比拟的优势,并且没有性能损失。
第47条 了解和使用类库
能使用标准类库就使用标准类库,我们应该把更多的时间花在应用程序上而不是底层细节上。总而言之,就是不要重新发明轮子,直接用Java类库给我们提供的轮子。
第48条 如果需要精确的答案,请避免使用float和double
System.out.println(1.03-0.42)答案是0.09999999999998.
解决这个问题的办法是使用BigDecimal、int、long进行精确计算。
总而言之,对于任何需要精确答案的计算任务,不要使用float和double。
第49条 基本类型优先于装箱类型
基本类型和装箱基本类型有三个主要区别:
1.基本类型只有值,而装箱基本类型则具有与他们的值不同的同一性。
2.基本类型只有功能完备的值,而每个装箱基本类型除了它对应的基本类型的所有功能值之外,还有个非功能值null、
3.基本类型通常比装箱基本类型节省时间和空间
什么时候应该使用装箱基本类型呢? 1是作为集合中的元素、键、值的时候。2是不能讲基本类型放在集合中。
总之,当可以选择的时候,基本类型优先于装箱基本类型。
第50条 如果其他类型更适合,则尽量避免使用字符串
1.字符串不适合代替其他的值类型
2.字符串不适合代替枚举类型
3.字符串不适合代替聚集类型
4.字符串也不适合代替能力表
如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。若使用不当字符串会比其他类型来得更加笨拙,更不灵活,速度更慢,也更容桂出错。经常被错误地用字符串开代替的类型包括基本类型、枚举类型和聚集类型
第51条 当心字符串连接的性能
字符串连接很方便,但是它不适合运用在大规模的场景中。为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级时间。
如果项目数量巨大,为了获得可以接受的性能,应该使用StringBuilder来替代String,不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要。相反,应该使用StringBuilder的append方法。
第52条 通过接口引用对象
应该使用接口而不是用作类作为参数的类型。更一般的来讲,应该优先使用接口而不是类来引用对象。如果有合适的接口类型存在,那么对于参数,返回值,变量和域来说,就都应该使用接口类型进行声明。
第53条 接口优先于反射机制
反射功能只是在设计时被用到。通常,普通引用程序在运行时不应该以反射方式访问对象。如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。对于有些程序,他们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过他们可以引用这个类。如果是这种情况,就可以反射方式创建实例,然后通过他们的接口或者超类,以正常的方式访问。
总之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,但它也有缺点。如果编写的程序必须要与编译时的未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类
第54条 谨慎地使用本地方法
JNI编程也就是通俗意义上讲的混合编程(C/C++/Matlab)注--个人理解因为使用本地方法来访问提高性能的做法不值得提倡。使用本地方法有一些严重的缺点,因为本地语言不是安全的。所以,如果本地代码只是做少量的工作,本地方法就可能降低性能。最后一点,需要“胶合代码”的本地方法编写起来单调乏味,并且难以阅读。
第55条 谨慎地进行优化
普遍意义上来讲,优化的弊大于利,特别是不成熟的优化,在优化的过程中,产生的软件可能既不迅速,也不正确,而且还不容易修正,努力避免那些限制性能的决策。要考虑API设计决策的性能后果。另外,在每次试图做优化之前和之后,要进行性能测量。总而言之,不要费力去编写快速的程序--应该编写好的程序,速度自然会随之而来,在设计系统的时候,特别是在设计API,线路层协议和永久数据格式的时候,一定要考虑性能的因素。
第56条 遵循普遍接受的命名惯例
这个没什么好说的,画一张表示意:
标识符类型 |
例子 |
包 |
com.wuyi.demo com.syswin.toon |
类或接口 |
LinkedHashMap,HttpServlet |
方法或者域 |
remove,ensureCapacity |
常量域 |
MIN_VALUE |
局部变量 |
i,houseNumber |
类型参数 |
T,E,K,V,X,T1,T2 |
第8章 异常
第57条 只针对异常的情况才使用异常
异常应该只用于异常的情况下;它们永远不应该用于正常的控制流。否则,会导致程序运行性能的下降。
第58条 对可恢复的情况使用受检异常,对编程错误使用运行时异常
Java提供了三种可抛出结构:受检的异常,运行时异常和错误。在决定使用受检的异常或是未受检的异常时,主要的原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常。
有两种未受检的可抛出结构:运行时异常和错误。在行为上两者是等同的:他们都是不需要也不应该被捕获的可抛出结构。如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。用运行时的异常来表明编程错误。
第59条 避免不必要地使用受检的异常
目前的主流意见是优先使用非受检异常,异常受检的本质并没有为程序员提供任何好处,它反而需要付出努力,还使程序更加复杂。
第60条 优先使用标准的异常
专家追求并且通常也能够实现高度的代码重用。代码重用是值得提倡的,这是一条通用的规则,异常也不例外。下面是常用的异常:
异常 |
使用场合 |
IllegalArgumentException |
非null的参数值不正确 |
IllegalStateException |
对于方法调用而言,对象状态不合适 |
NullPointerException |
在禁止使用null的情况下参数值为null |
IndexOutOfBoundsException |
下标参数值越界 |
ConcurrentModificationException |
在禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportedOperationException |
对象不支持用户请求的方法 |
虽然他们是Java平台类库中最常用被重用的异常,但是,在条件许可下,其他的异常也可以被重用,不过,一定要确保抛出异常的条件与该异常的文档中描述的条件一致。最后,一定要清楚,选择重用哪个异常并不总是那么精确。
第61条 抛出与抽象相对应的异常
如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由底层抽象抛出的异常时,往往会发生这种情况。
为了避免这个问题,更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种方法被称为异常转译。如下:
try {
}catch(LowerLevelException e){throw new HigherLevelException}
就是将捕获的异常转发到高级异常(请求转发类似,个人理解)
一种特殊的异常转译形式被称为异常链,如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。低层的异常被传到高层的异常,高层的异常提供访问方法来获得低层的异常。处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。
处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。如果无法避免低层异常,次选方案是,让更高层来悄悄地绕开这些异常,从而将高层方法的调用者与低层的问题格力开来。
总之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。
第62条 每个方法抛出的异常都需要文档
始终要单独地声明受检异常,并且利用Javadoc的@throws标记,准确地记录下抛出每个异常的条件。使用Javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。
总之,要为我们编写的每个方法所能抛出的每个异常建立文档,对于未受检和受检的异常,以及对于抽象的和具体的方法也都一样。要为每个受检异常提供单独的throws字句,不要为未受检的异常提供throws字句。
第63条 在细节消息中包含能捕获失败的信息
当程序由于未被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹,这一节主要就是讲这个s.printStackTrace()打印栈轨迹所包含的一些有用信息(个人理解)。
为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。
所以,异常的字符串表示法主要是让程序元或者域服务人员用来分析失败的原因。因此,信息的内容比可理解性要重要得多。为异常信息提供一些访问方法是合适的。提供这样的访问方法对于受检的异常,比对于未受检的异常更为重要,因为失败--捕获信息对于从失败中恢复是非常有用的。程序员希望通过程序的手段来访问未受检异常的细节。然而,即使对于未受检的异常,作为一般原则提供这些访问方法也是明智的。
第64条 努力使失败保持原子性
一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。
原子性是要么一起成功,要么一起失败
有几种途径可以实现这种效果。
1.是设计一个不可变的对象
2.是在操作执行前检查参数的有效性,或者调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
3.获得原子失败性的办法远远没有那么常用,做法是编写一段恢复代码。
4.在对象的一份临时拷贝上执行。
第65条 不要忽略异常
try{
...
}catch(SomeException e){ }
空的catch块会使异常达不到应该由的目的,即强迫我们处理异常的情况。
把异常记录下来还是明智的做法,因为如果这些异常经常发生,我们就可以调查异常的原因了。
第9章 并发
第66条 同步访问共享的可变数据
要理解同步的概念,就要理解Java内存模型。
画一张URL图来说明Java内存模型
避免本条中所讨论的问题的最佳方法是不共享数据。要么共享不可变的数据,要么压根不共享数据。换句话说,就是将可变数据限制在单个线程中。如果采用这一策略,对它建立文档就很重要,以便它可以随着程序的发展而得到维护。
总之,当多个线程共享可数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败和安全性失败。如果只需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式。
第67条 避免过度同步
在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。
在同步区域之外被调用的方法称为开放调用,除了可以避免死锁之外,开放调用还可以极大地增加并发性。通常我们应该在同步区域内做尽可能少的工作。虽然同步的成本下降,但是永远不要过度同步,当我们不确定的时候,就不要同步我们的类,而是应该建立文档,注明它不是线程安全的。相关代码放入到Github中
总之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更一般的来讲,要尽量限制同步区域内部的工作量。当我们设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在多核时代,这比永远不要过度同步来得更加重要。只有当我们有足够的理由一定要在内部同步类的时候,才应该这么做,同时应该将这个决定清楚地写到文档中去。
第68条 executor和task优先于线程
Java 1.5版本中添加了java.util.concurrent并发工具包,
ExecutorService executor=Executors.newSingleThreadExecutor()
下面是为执行一个runnable的方法:
executor.execute(runnable):
优雅的终止:
Executor.shutdown()
Executors还有newFixedThreadPool(),newCachedThreadPool()等方法
不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。现在关键的抽象是工作单元,称作任务。总之一句话,尽量多使用并发工具包里的类库而要使用Thread来创建线程。
第69条 并发工具优先于wait和notify
在Java1,5之后几乎没有理由再使用wait和notify了。既然正确使用wait和notify比较困难,就应该用更高级的并发工具来代替。比如,除非迫不得已,否则就应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable。更一般地来讲,应该优先使用并发集合,而不是使用外部同步的集合。
对于wait()方法始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
总之,没有理由在新代码中使用wait和notify,即使有也是极少的。如果我们在维护使用wait和notify的代码,无比确保始终是利用标准模式从while循环内部调用wait,一般情况下,应该优先使用notifyAll而不是notify,如果使用notify一定小心以确保程序的活性。
第70条 线程安全性的文档化
一个类为了可被多个线程安全地使用,必须在文档中清楚说明它所支持的线程安全性级别。
有如下线程安全性级别:
1.不可变的
2.无条件的线程安全
3.有条件的线程安全
4.非线程安全
5.线程对立的
类的线程安全说明通常放在它的文档注释中,但是带有特殊线层安全属性的方法则应该在他们自己的文档注释中说明他们的属性。没有必要说明枚举类型的不可变性。
总之,每个类都应该利用字斟句酌的说明或者线程安全注解,清楚地在文档中说明他的线程安全属性。如果编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步方法。这样可以防止客户端程序和子类的不同步干扰。
第71条 慎用延迟初始化
就像大多数优化一样,对于延迟初始化,最好的建议是:“除非绝对有必要,否则就不要这么做”,延迟初始化一方面降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。在大多数情况下, 正常的初始化要优先于延迟初始化。
如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initaization holder class模式,保证类要被用到的时候才会被初始化
Private static class FieldHolder{
Static final FieldType field=computeFieldValue();
Static FieldType getField(){return FieldHolder.field;}
}
这种模式的魅力在于,getField方法没有被同步,并且只执行了一个域访问,因此延迟初始化实际上并没有增加任何访问成本。
如果出于性能的考虑需要对实例域使用延迟初始化,就使用双重检查模式。
Private volatile Field field;
FieldType getField(){
FieldType result=field;
if(result==null){ //第一次检查
Synchronized(this){
result=field;
If(result==null){ //第二次检查
field=result=computeFieldValue();
}
}
}
}
简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,既可以使用相应的延迟初始化方法。对于实例域,就使用双重检查模式;对于静态域,则使用lazy initalization holder class idiom。对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式。
第72条 不要依赖于线程调度器
任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可抑制的(因为在一台电脑上可能运行地很好,在另外一台电脑上就可能错误百出,因为线程调度在每台电脑上的运行时不一样的,个人注释)要编写健壮的响应良好的,可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。如果线程没有在做有意义的工作,就不应该运行。
不要企图Thread.yield来修正程序,可能会成功,但是代码是不可移植的,因为红字的注解部分说明。Thread.yield没有可测试的语义。更好的解决办法是重新构造应用程序,以减少可并发运行的线层数量。
Thread,yield的唯一用途是在测试期间人为地增加程序的并发性。此外,应该使用Thread.sleep(1)代替Thread.yield来进行并发测试。
简而言之,不要让应用程序的正确性依赖于线程调度器。否则,结果得到的应用程序将既不健壮,也不具有可移植性,所以,不要依赖Thread.yield或者线程优先级。
第73条 避免使用线程组
ThreadGroup API非常弱,线程组已经过时,没必要使用它们,而且很多功能都有缺陷,直接忽略掉就可以了。
第10章 序列化
第74条 谨慎地实现Serializable接口
实现Serializable接口而付出的代价:
1.一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性(这是最大的代价)
2.增加了出现Bug和安全漏洞的可能性
3.随着类发行新的版本,相关的测试负担也增加了。
所以为了继承而设计的类应该尽可能少地实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。
总之,千万不要认为实现Serializable接口会很容易。除非一个类在用了一段时间后被抛弃,否则使用Serializable就是一个很严肃的承诺,必须认真对待。如果一个类是为了继承而设计的,则更加需要加倍小心。对于这样的类而言,在“允许子类实现Serializable”接口或“禁止子类实现Serializable接口”,两者之间的一个折中的方案使,提供一个可访问的无参构造器。这种设计方案允许子类实现Serializable接口。
第75条 考虑使用自定义的序列化形式
第一条建议就是如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受,一般来讲,只有当我们自行设计的自定义序列化形式与默认的序列化形式基本相同时,才能接受默认的序列化形式。
如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于默认的序列化形式。即使确定了默认的序列化形式是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性。
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有下面缺点:
1.它使这个类的导出API永远地束缚在该类的内部表示法上
2.它会消耗过多的空间
3.它会消耗过多的时间
4.它会引起栈溢出
当我们决定要将一个类做成可序列化的时候,要仔细考虑应该用什么样的序列化形式。只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能用默认的序列化形式;否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响。
第76条 保护性地编写readObject方法
这和第39条类似,readObject安全性的问题,在反序列化的过程中,一些方法并没有完成足够的保护性拷贝。当一个对象呗反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的。因此对于每个可序列化的不可变类,如果它的包含有了私有的可变组件,那么在它的readObject方法中,必须要对这些组件进行保护性拷贝。另外,保护性拷贝是在有效性检查之前进行的。编写更加强大健壮的readObject方法:
1.对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一级别。
2.对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常,这些检查动作应该跟在所有的保护性拷贝之后
3.如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口
4.无论是直接方式还是间接方法,都不要调用类中任何可被覆盖的方法。
第77条 对于实例控制,枚举类型优先于readResolve
应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolve方法,并确保该类的所有实例域都为基本类型,或者是transient的。
第78条 考虑使用自定义的序列化形式
如第74条中,决定实现Serializable接口,会增加出错和出现安全问题的可能性,因为它导致实例要利用语言之外的机制来创建,而不是用普通的构造器,然而,使用序列化代理模式可以极大地减少这些风险。
序列化代理模式有两个局限性。它不能与可以被客户端扩展的类兼容。它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为我们还没有这个对象,只有它的序列化处理
总之,每当发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式
绘制的本书思维导图如下:
总结
第11,28,29,76,77,78条阅读不够清晰明白,文档结构按照书本上的目录结构进行,在每一条自己认为应该着重注意的地方和自己的理解都加了红字突出标记,无论怎么表述都没有书本上作者的原话说的更加清晰彻底。前前后后记录文档结构花费2天半的时间,包括一些URL图和思维导图的绘制,书上的源代码大多数都敲了一遍,相应的将本次阅读产生的源代码上传到Github,地址在文档结构的顶端有链接。作为记录的一部分。觉得这样的记录方式比较好,每看完一本书,1是记录下word文档,2是将源代码保存到Github,3是绘制出整个书本的思维导图和相应的UML图。
下一步阅读计划任务:
《Java Concurrency in Practice》5月25号之前做完读书笔记
《重构,改善既有代码的设计》6月8号之前做完读书笔记
《Java与模式(阎宏)》6月22号之前做完读书笔记
私下自行解决数据结构和算法分析和TCP/IP协议族