Effective Java总结

一、创建和销毁对象

1、考虑用静态工厂方法代替构造器
静态工厂方法和设计模式中的工厂方法模式不同

静态工厂方法和构造器不同的优势在于:

(1)静态工厂方法可以自定义方法名称,而构造器的方法名称只能和类名相同。

(2)静态工厂方法不必在每次调用它们的时候都创建一个新的对象。

(3)静态工厂方法可以返回原返回类型的任何子类型的对象。

(4)静态工厂方法在创建参数化实例类型的时候,他们使代码更加简洁。

缺点:

(1)类如果不含公有的或者受保护的构造器,就不能被子类化。

(2)静态工厂方法与其他的静态方法实际上没有任何区别。

2、遇到多个构造器参数时要考虑使用构建器(创建者模式)
静态工厂和构造器都有个共同的局限性:无法很好的扩展到大量的可选参数。

当类有大量属性存在时

(1)重叠构造器模式可行,但是客户端代码会非常难写和阅读。比如即使写好了很多构造器。但调用时参数颠倒,编译不会出错,但是运行会报错,会有很多隐患存在。

(2)JavaBeans模式,使用无参构造器来创建对象,然后调用setter方法来设置每个属性的参数。<在构造过程中javaBeans可能处于不一致的状态>

eg:

线程A: 获取person,对其name age sex 就行set操作

线程B: 获取person,对其进行get操作

这时候会出现一种情况,在线程A中没有set完毕,线程B就开始取相应的属性

那么就会造成javabean不一致的状态;随之相关就是线程安全的问题,所以javabean作为数据的一个填充,要进行必要的保护性拷贝>

(3)Builder模式:不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器,得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后,客户端调用无参的builder方法来生成不可变的对象。这个builder是构建的类的静态成员类。

服务提供者框架:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。

3、用私有构造器或者枚举类型强化Singleton属性
singleton指仅仅被实例化一次的类,通常用来代表那些本质上唯一的系统组件。

4、通过私有构造器强化不可实例化的能力
对于有些工具类不希望被实例化,实例对它没有任何意义。

5、避免创建不必要的对象
适配器:adapter<视图view>,适配器是指这样一个对象,它把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,他不需要创建多个适配器实例。

创建多余对象的方法:自动装箱<允许开发者将基本类型和装箱基本类型混用,按需要自动装箱和拆箱>

比如:Long和long

如果是使用Long,sum + = i,i为long,则每次计算一次会构造一个Long实例,当循环很多次时,会创建大量的无用实例。

结论:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱操作。

并不意味这“创建对象的代价很大,要避免创建对象”,而是小对象的创建只需要做很少的工作,通过构建这样的对象来实现程序的清晰性时很好的。相反,通过维护自己的对象池来避免创建对象不是一件好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象实例是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。

6、消除过期的对象引用
清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用的最好的方法是让包含了该引用的变量结束掉生命周期。

一般而言,只要类是自己管理内存,那么开发人员就应该警惕内存泄露问题。

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

7、避免使用终结方法(finalize)
java提供finalize()方法,垃圾回收器准备释放内存的时候,会先调用finalize()。

不使用的原因:

(1)java语言不仅不保证终结方法被及时执行,而且不保证其会被执行。

(2)终结方法有非常严重的性能损失。

正确使用终结方法

(1)防止用户使用了创建了对象后,并未使对象提供的显式终止方法(如果有)。终结方法可以充当安全网。

(2)本地对等体(是一个本地对象,普通对象通过本地方法委托给一个本地对象)不是一个普通对象,垃圾回收器不会回收它。一般情况下,终结方法需要能够完成所有必要的工作释放资源,如果需要即时释放资源,那么就需要给本地对等体指定一个显式的终止方法。

二、对于所有对象都通用的方法

8、覆盖equals时先遵守通用约定
(1)使用==操作符检查“参数是否为这个对象的引用”

(2)使用instanceof操作符检查“参数是否为正确的类型”

(3)把参数转换成正确的类型

(4)对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配

(5)equals方法应该满足:对称性、传递性、一致性、自反性和非空性

覆盖equals时总要覆盖hashCod

不要企图让equals方法过于严格智能

不要将equals方法声明中的Object对象替换成其他的类型

9、覆盖equals方法时总要覆盖hashCode
场景:例如hashMap中会使用到散列码,put方法将key存储在一个散列桶中,两个相等的实例具有不同的散列码会导致get方法返回null

覆盖hashCode方法,必须遵守的通用约定

(1)在应用程序执行期间,equals所用到的信息没有被修改的情况下,调用多次hashCode方法都返回同一个整数

(2)如果两个对象equals,那么这两个对象调用hashCode返回一样的整数结果。相等的对象必须具有相等的散列码

(3)如果两个对象不equals,那么这两个对象调用hashCode不要求返回一样的整数结果

结论:哈希码的作用是为了提高哈希表的性能,让数据在哈希表中分布的更均匀。不覆盖equals方法不影响其他功能的使用,但是覆盖了equals一定要覆盖hashCode。

10、始终要覆盖toString
(1)在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。

(2)toString的打印信息无论是否指定格式,都应该在文档中明确的表达自己的意图。

(3)无论是否指定格式,都为toString返回值中包含的所有信息,提示一种编程式的访问途径。

11、谨慎的覆盖clone
Cloneable接口的目的是作为一个对象的mixin接口,表明这样的对象允许被克隆。遗憾的是,它并没有达到这个目的,其主要的缺陷在于它缺少一个clone方法,而object的clone方法是受保护的。

即使借助于反射,也不能因为一个对象实现了cloneable接口就可以调用clone方法,因为不能保证该对象一定具有可以访问的clone方法。尽管存在这样那样的缺陷,该设施仍然被广泛的使用,所以问题在于如何实现一个具有良好功能clone方法。

Cloneable接口的作用:它决定了Object中受保护的clone方法实现的行为–>如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就抛出CloneNotSupportedException异常。这是接口的一种极端非典型的用户,也不值得效仿。通常情况下,实现接口是为了表明类可以为它的客户做什么,然而对于Cloneable接口,它改变了超类中受保护的方法的行为。

clone方法就是另外一个构造器,必须确保它不会伤害到原始的对象,并确保正确的创建被克隆对象的约束条件。

clone架构与引用可变对象的final域的正常用法是不相兼容的,除非在原始对象和克隆对象之间可以安全的共享此可变对象。在一些情况下,为了使类成为可克隆的,可能有必要从某些域中去掉final修饰符。

如果扩展一个实现了Cloneable的类,那么一定要实现一个行为良好的clone方法,除此之外别无他法。否则,最好提供某些其他的途径来替代对象拷贝,或者干脆不提供这样的功能。另一个实现对象拷贝的好方法是提供一个拷贝构造器或拷贝工厂。

拷贝工厂是类似拷贝构造器的静态工厂:

public static Yum newInstance(Yum yum);

好处:

(1)不依赖于某种很有风险的、语言之外的对象创建机制;

(2)不要求遵守尚未定制好文档的规范;

(3)不会与final域正常使用发生冲突;

(4)不会抛出不必要的异常;

(5)不需要进行类型登录;

att:

除非拷贝数组,要不然不要覆盖clone方法,也不去调用它。对于一个专门为了继承而实现的类,如果你未能提供行为良好的受保护的clone方法,它的子类就不可能实现Cloneable接口。

12、考虑实现Comparable接口
和equals相似,实现该接口需要满足:自反性、传递性以及对称性。一般情况下equals()为true时compareTo()的结果是0,如果违反了这个条件,需要加上说明“注意:该类具有内在的排序功能,但是与equals不一致”

eg:

set集合是通过compareTo()方法来添加判断元素的。new BigDecimal(“1.0”)和new BigDecimal(“1.00”)两个equals是不相等的,但是将该对象添加进TreeSet和HashSet集合时,该集合中就只有一个元素,这样就会造成一个误导。但是也并不是致命性的错误。

att:

在自定义compareTo方法的时候,如果使用了return i-j的形式,那么需要注意数值溢出的问题,i是一个很大的正数,j是一个很大的负数,这种失败可能会难以调试,因为在大部分的情况下结果值都是正常的。所以整数可直接用i>j的格式来比较,浮点域用Double.compare(a,b)和Float.compare(a,b)而不是用关系操作符直接比较。

三、类和接口

域:是指属性field

实例域:是实例化对象的属性

静态域:是被static修饰的属性

一个对象被创建10次,有10个不用的实例域,但是公用一个静态域。类加载时,只有静态域和静态块赋值。

13、使类和成员的可访问性最小化
信息隐藏(封装):模块之间只通过API进行通信,一个模块不需要知道其他模块的内部工作情况。

对于类和接口,只有public和package-private(同一根目录下的可以访问)两种修饰。不可声明为private类型,因为如果class可以是private的,那么就只有这个class自己可以访问,别的class都不可以,那这个类是没有必要存在的。

对于成员:private、package-private、protected(子类可以访问)、public。

如果一个类实现了Serializable接口,这些域有可能会被泄露<leak>到导出API中。

限制:子类中的访问级别不允许低于超类中的访问级别。特殊情形:如果一个类实现了一个接口,那么接口中所有的类方法在这个类中也都必须被声明为共有的。之所以如此,是因为接口中的所有方法都隐含这公有访问级别。

实例域绝不能是公有的。如果属性是非final的,或者是一个指向可变对象的final引用,那么一旦使这个属性成为公有的,就放弃了对存储在这个属性中的值进行限制的能力。这也意味着放弃了强制这个属性不可变的能力。同时,当这个属性被修改时,也失去了对它采取任何行动的能力。因此,包含公有可变域的类并不是线程安全的。即使域是final的,并且引用了不可变的对象,当把这个域变成公用的时候,也就放弃了“切换到一种新的内部数据表示法”的灵活性。(???翻译的很别扭,大概意思就是将属性设置成公有的不安全,应该设置成私有的,然后提供get和set方法)

14、在共有类中使用访问方法而非公有域
在public修饰的类中,将属性定义成private,然后提供共有的getter和setter访问方法。而不是将属性设置成public。

如果设置成公有域,缺点有:

(1)这种类的数据域是可以被直接访问的,不满足封装性

(2)如果属性值表示发生变化,涉及的修改范围会很多,会需要将所有的a.field都改变(在实际中很少出现要改属性名的情况)

如果一个类需要在它所在的包的外部进行访问,那么就要提供访问的方法。如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据库并没有本质的错误。(因为作用范围有限)。

结论:

(1)公有类永远都不应该暴露可变的属性;

(2)暴露不可变的属性的危害较小;

(3)无论可变还是不可变,使用包级私有或者私有的嵌套类来暴露域;

15、使可变性最小化
为了使类成为不可变,要遵循以下规则:

(1)不要提供任何会改变对象属性的方法

(2)保证类不会被扩展,定义成final

(3)使所有的属性都是final的

(4)使所有的属性域都是私有的

(5)确保对于任何可变组件的互斥访问(如果类具有指向可变对象的属性,那么必须确保该类的客户端无法获得指向这些对象的引用)

16、复合优先于继承(多用组合,少用继承)
实现继承:当一个类扩展另一个类的时候

接口继承:当一个类实现一个接口的时候,或者当一个接口实现另一个接口的时候

与方法调用不同的是,继承打破了封装性:子类依赖超类中特定功能的实现细节,超类的实现会随着版本的不同而更新,然后子类就必须跟着适应这种变化。除非超类是专门为了扩展而设计的,并且具有良好的文档说明。

复合:不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。

包装类:为一个类提供新方法。

回调框架:对象把自身的引用传递给其他的对象,用于后续的调用。

只有当子类真正是超类的子对象,两者的关系是is-a的关系时,才适合用继承。

17、要么为继承而设计,并提供文档说明,要么就禁止继承
(1)该类必须有文档说明它可覆盖的方法的自用型

当为了继承而设计类的时候:

(1)尽可能少的暴露受保护的成员,因为每个方法或者域都代表了一项关于实现细节的承诺。又不能暴露的太少,因为漏掉的受保护方法可能会导致这个类无法被真正的继承。

(2)对于为了继承而设计的类,唯一的测试方法就是编写子类。

(3)在为了继承而设计有可能被广泛使用的类时,必须意识到,对于文档中所说明的自用模式,以及对于其受保护方法和域中所隐含的实现策略,你实际上已经做出了永久的承诺。

(4)构造器决不能调用可被覆盖的方法。(在父类super的构造方法中调用了overrideMethod方法,子类在初始化的时候,会调用父类构造方法,同时会调用被覆盖的overrideMethod方法<子类overrideMethod方法>,调用时对象还没有初始化,可能会出现各种问题。)

扩展:

(1)子类继承父类,子类的构造方法必须调用super()即父类的构造方法,而且必须放在构造方法的第一行。

(2) clone()和readObject()方法在行为上都非常类似于构造器,所以无论是clone()还是readObject(),都不可以调用可覆盖的方法,不管是直接调用还是间接调用。

(3)clone():覆盖版本的方法在子类的clone方法有机会修正被克隆对象的状态之前先被运行。

(4)readObject():覆盖版本的方法在子类的状态被反序列化之前先被运行。

(5)如果决定在一个为了继承而设计的类中实现Serializable,并且该类有一个readResolve()或者writeReplace()方法,就必须将其声明成protected而不是private。

(6)对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。

禁止类子类化的两种方法:

(1)类声明成final类型

(2)将构造方法私有化,增加静态工厂方法来替代构造器

结论:如果一个类要被继承,那么要确保这个类永远不会调用它的任何可覆盖方法,即消除这个类中可覆盖方法的自用特性。

消除类中可覆盖方法的自用特性:将每个可覆盖的方法的代码体<逻辑代码实现>移动到一个私有的“辅助方法中”,并且让每个可覆盖的方法调用它的私有辅助方法。<感觉这个地方法不对,如果父类构造方法中还是调用了可覆盖的方法,在初始化子类的时候还是会出现错误>

18、接口优于抽象类
java提供两种机制用来定义允许多个实现的类型:接口和抽象类

区别:抽象类允许包含某些方法的实现,抽象类中所有抽象方法必须被重写,java单继承

API(Application Programming Interface,应用程序编程接口)

包装类模式:接口使得安全地增强类的功能成为可能。通过对导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。

使用抽象类来定义允许多个实现的类型,比使用接口相比有一个明显的优势:抽象类的演变比接口的演变要容易得多,如果在后续得发行版本中,你希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理得默认实现。然后,该抽象类得所有现有实现都将提供方新的方法。而对于接口,无法做到,因为一旦接口新增方法,那么实现类都需要增加新的方法。接口一旦被公开发行广泛使用,就很难再做出改变。

但是接口还是定义允许多个实现的类型的最佳途径。

19、接口只用于定义类型
当类实现接口时,接口就充当了可以引用这个类的实例的类型(type)。

常量接口:接口不包含任何方法,只包含静态的final属性,使用这些常量的类实现这个接口,以避免用类名来修饰常量名。类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这个实现细节泄漏到类的导出API中。

常量的静态导入:import static com.java.science.Phy…<jdk 1.5之后>

接口应该只被用来定义类型,而不应该被用来导出常量。

20、类层次优于标签类
标签类: 带有两种甚至更多种风格的类的实例的类,并包含表示实例风格的(tag)域。例如一个类,既可以表示圆形又可以表示矩形。标签类过于冗长,容易出错,并且效率低下。

总结:

标签类很少有适用的时候,当你想要编写一个包含显示标签域的类时,应该尽可能的用类层次来代替,考录将其重构到一个层次接口中去。<以圆形矩形标签类为例,可以抽象出形状figure的抽象类,然后使circle类和rectangle类继承figure类>

21、用函数对象表示策略
结合策略模式理解

函数指针的主要用途就是实现策略模式,为了在java中实现策略模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类<FlyWithWings、FlyWithRocket>。当一个具体策略只被使用一次时,通常使用匿名类的声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态类final域被导出,其类型为该策略接口。

Duck类中定义FlyBehavior flyBehavior的属性,并提供setFlyBehavior()方法,在测试类中使用setFlyBehavior(new FlyWithWings())或者setFlyBehavior(new FlyWithRocket())来定义具体的行为。

22、优先考虑静态成员类
嵌套类:被定义在另一个类的内部的类,嵌套类存在的目的应该只是为它的外围类提供服务。如果嵌套类将来可能用于其他某个环境中,它就应该是顶层类。

嵌套类类型:除了静态成员类,后三种被成为内部类。

静态成员类(是外部类的静态成员,不过是类形式,和静态属性方法的访问规则一样)

(1)作为静态的嵌套类,它属于它的外部类(enclosing class),但它不是外部类的实例

(2)类声明可以使用所有的访问修饰符

(3)它仅仅可以访问外部类的静态字段和静态方法,私有的也可以

(4)它可以定义静态和非静态的成员

非静态成员类(同外部类的普通属性和方法)<应用:适配器>

(1)它们通常也称为内部类(inner class)

(2)类声明可以使用所有的访问修饰符

(3)像外部类的成员变量和成员方法一样,内部类也可以认为是外部类的一个成员属性

(4) 可以访问外部类所有的成员,包括静态和非静态的

(5) 除了static final修饰的编译时常量成员外,内部只能定义非静态成员

局部类(在{}中定义类)

(1)可以定义在一个方法里面或者{}代码块里面

(2)类声明不能使用任何访问修饰符

(3) 可以访问外部类所有的成员,包括静态和非静态的

(4) 除了static final修饰的编译时常量成员外,内部只能定义非静态成员

(5) 仅仅在所在的方法块或者代码内有效。

匿名类(没有定义直接new,Runnable)

匿名类——没有名称的类,其名称由Java编译器给出,一般是形如:外部类名称+$+匿名类顺序,没有名称也就是其他地方就不能引用,不能实例化,只用一次,当然也就不能有构造器。匿名类根据位于地方不同分为:成员匿名类和局部匿名类。

(1)匿名类是一次性使用的类,用于实现一个接口或者抽象类仅仅临时用一次,不能复用。

(2)可以访问外部类所有的成员,包括静态和非静态的。

(3)除了static final修饰的编译时常量成员外,内部只能定义非静态成员

(4)是唯一一种不能定义构造方法不能使用继承功能不能使用实现接口功能的类。

常见用法是动态的创建函数对象new和创建过程对象Runnable、Thread或者TimeTask实例。

InterfaceA a = new InterfaceA() {};//成员匿名类
    public static void main(String[] args){
        InterfaceA a = new InterfaceA() {};//局部匿名类
        //以上两种是通过实现接口实现匿名类,称为接口式匿名类,也可以通过继承类
        OuterClass test = new OuterClass(){};//继承式匿名类
        //还可以是位于参数上
        new Thread(new Runnable() {
            @Override
            public void run() {
            }
        }).start();//属于局部匿名类一种
    }
    private interface InterfaceA{}
}

匿名类不能使用任何关键字和访问控制符,匿名类和局部类访问规则一样,只不过内部类显式的定义了一个类,然后通过new的方式创建这个局部类实例,而匿名类直接new一个类实例,没有定义这个类。匿名类最常见的方式就是回调模式的使用,通过默认实现一个接口创建一个匿名类然后,然后new这个匿名类的实例。

总结:

如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外部对象的引用,那么就把做成非静态的,否则就做成静态的。假设这个嵌套类属于一个方法的内部,如果只需要在一个地方创建实例,并且已经有了一个预设的类型可以说明这个类的特征,就做成匿名类,否则就做成局部类。

四、泛型

23、不要在代码中使用原生态类型
泛型:List声明中具有一个或者多个类型参数的泛型类或者接口。

原生态类型:Set、List。对于一个方法unsafeAdd(List list,Object o)这里的List就是原生态类型。

无限制的通配符类型:为原生态类型的安全替代方法List<?> list,使用情况为要使用泛型,但是不确定或者不关心实际的类型参数。仅仅表示包含某种未知对象类型的一个集合

有限制的通配符类型:List

参数化类型:List 表示可以包含任何对象类型的一个集合

总结

使用List这样的原生态类型会在运行时导致异常,会失掉泛型在安全性和表述性方面的优势,因此不要在新代码中使用。原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。可以使用List这种参数化类型。

24、消除非受检警告
非受检警告:非受检强制转换警告unchecked cast warnings、非受检方法调用警告非受检普通数组创建警告,以及非受检转换警告unchecked conversion warnings.

非受检警告很重要,不要忽视它们。每一条警告都可能在运行时抛出ClassCastException异常。要尽最大的努力去消除这个警告。如果无法消除非受检警告,同时确定引起警告的代码时类型安全的,可以在尽可能小的范围用@SuppressWarnings(“unchecked”)注解禁止在编译的时候出现该警告,于此同时要添加注释将该警告消除的原因记录下来。

25、列表优先于数组
数组:是协变类型的。协变的意思就是Number是Integer的父类,那么Number[] data = new Integer[]; 是成立的。

列表:是不可变类型。就是说无法List list = new List();

无法发生协变才是正确的方式,从技术上来说,泛型是不可具体化的。

不可具体化:运行期间虚拟机获得的信息不能够比编译期间更多(擦除导致的问题)。擦除:使泛型可以与没有使用泛型的代码随意进行互用。

只有通过无限通配符(?)才能让泛型变为具体化。

由于这一特性,导致使用泛型的数组(T[] a)和利用泛型转型((T) a)是类型不安全的,因为虚拟机根本不知道转化成那种类型。所以对泛型使用可变参数列也是不安全的。

数组和泛型有着非常不同的类型规则。数组是协变的并且是可以具体化的;泛型是不可变的并且是可以擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型无法很好的混合使用。如果混合使用编译出错,应尽快的用列表代替数组。

26、优先考虑泛型
使用泛型比使用在客户端中进行转换的类型更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端。

27、优先考虑泛型方法
泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味这要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加方便,且不会破坏客户端。

28、利用有限制通配符来提升API的灵活性
为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型

在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活更多。如果编写的是将广泛使用的类库,则一定要适当地利用通配符类型。基本的原则是:producer-extends、consumer-super(PECS)。另外所有的comparable和comparator都是消费者。

29、优先考虑类型安全的异构容器
集合API说明了泛型的一般用法,限制了每个容器只能有固定数目的类型参数。可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用Class对象作为键。以这种方式使用的Class对象称为类型令牌,当然也可以自定义键类型。

type token:一个类的Class类型被用在方法中,来传达编译时和运行时的类型信息。

异构:Map<Class<?>,Object>,不像普通map,它的每个键都可以有一个不同的参数化类型。

五、枚举和注解

enum Day {//其实跟类的定义很像,枚举属性相当于属性变量。只不过枚举属性可以设置初始值,有很多枚举类自定义的方法
    MONDAY(1),//若带参数需要提供构造函数,要不然报错
    TUESDAY(2);
    private final int number; //成员全局属性,仅仅可初始化一次
    Day(int number){
        this.number = number;
    }
    public int getNumber(){
        return number;
    }
}

30、用enum代替int常量
在枚举没有出来之前,在类中使用静态常量的形式来表示枚举,这样的类往往只有属性。

枚举类型:一组固定的常量组成合法值的类型。

与int常量相比,枚举类型的优势是不言而喻的。枚举要易读得多,也更加安全,功能更加强大。许多枚举都不需要显式的构造器或者成员,但许多其他枚举则受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”。只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要有限于启动自有值的枚举。如果多个枚举变量同时共享相同的行为,则考虑策略枚举。

31、用实例域代替序数
许多枚举天生就于一个单独的int值相关联。所有的枚举都有一个ordinal方法,它返回每一个枚举常量在类型中的数字位置。但是当枚举顺序变化,ordinal()方法返回的值也会变化,这种变化可能于最开始期望的序列不一样,

大多数情况下不需要用到ordinal方法,它是用于设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构的。除非是在编写这种数据结构,否则最好完全避开ordinal方法。

永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中,如下:
enum Day {//其实跟类的定义很像,枚举属性相当于属性变量。只不过枚举属性可以设置初始值,有很多枚举类自定义的方法
    MONDAY(1),//若带参数需要提供构造函数,要不然报错
    TUESDAY(2);
    private final int number; //成员全局属性,仅仅可初始化一次
    Day(int number){
        this.number = number;
    }
    public int getNumber(){
        return number;
    }
}

32、用EnumSet代替位域
看不懂,不知道是什么意思

正是因为枚举类型要用在集合<Set>中,所以没有理由用位域来表示它。EnumSet类集合域有很强的简洁和性能优势,但是截至在java1.6版本,它都无法创建不可变的EnumSet,但是这一点很可能会在之后的java版本中修正。

33、用EnumMap代替序数索引
EnumMap集Map的丰富功能、类型安全、快速等优点于一身。

不要用序数来索引数组,而是使用EnumMap。如果表达的关系是多维的,就使用EnumMap<…,EnumMap<…>>。在编码时一遍情况下都不使用Enum.ordinal,即使用也非常少,通常使用第31条“用实例域代替序数”。

34、用接口模拟可伸缩的枚举
操作码:其元素表示在某种机器上的操作。比如Operation类中的±*/之类的。

虽然枚举类型不是可扩展的,但接口类型则是可扩展的。

虽然无法编写可扩展的枚举类型,但是可以通过编写接口以及实现该接口的基础枚举类型。对它进行模拟。这样允许客户端编写自己的枚举来实现接口。如果API是根据接口编写的,那么在可以使用基础枚举类型在任何地方,也都可以使用这些枚举。

除非需要灵活的合并多个实现类型的操作,否则可能最好使用有限制的类型令牌。

35、注解优先于命名模式
有了注解之后没有必要使用命名模式了。

命名模式的缺点:

(1)文字拼写错误会导致失败,且没有任何提示

将一个测试方法写成tsetMethod而不是testMethod,没有任何提示。测试方法没有执行,但是也没有报错,会给人测试正确的假象。

(2)无法确保它们只用于相应的程序元素上。

(3)没有提供参数值于程序元素关联起来的好方法。

test的测试方式,后来直接加@Test注解即可。

36、坚持使用Override注解
使用override注解来覆盖超类声明,编译的时候就可以防止大量错误。

37、用标记接口定义类型
标记接口:没有包含方法声明的接口,只是指明一个类实现了具有某种属性的接口。例如,考虑Serializable接口,通过实现这个接口,可以表明类的实例可以被写到ObjectOutputStream(或者“被序列化”)。

标记接口优于标记注解的原因:

1、标记接口定义的类型是由被标记类的实例实现的;标记注解没有这样的类型。这个类型允许在编译时捕获异常信息,而使用标记注解要在运行时才能捕获异常信息。

2、标记接口可以被更加精确地进行锁定。如果注解类型利用@Target(ElementType.TYPE)声明,它就可以被应用到任何类或者接口。假设有一个标记只适用于特殊接口的实现。如果将它定义成一个标记接口,就可以用它将唯一的接口扩展成它适用的接口。

如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口可以用来实现或者扩展接口。如果标记只应用给类和接口,那应该优先使用标记接口而非注解。

如果想要定义类型,一定要使用接口。

六、方法

如何处理参数和返回值,如何设计方法签名,如何为方法编写文档。

38、检查参数的有效性
每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写在文档中,并且在这个方法体的开头处,通过显式的检查来实施这些限制。养成这种习惯是非常重要的。

39、必要时进行保护性拷贝
在一个类中,如果设置了开始时间start和结束时间end,并给定限制:start<end,那么在这个类中,不要应该提供任何可以修改start和end的数值,除非确保这个限制不会被修改或者客户端是安全的。

关键在于Date类是一个可变类(而String是一个不可变类,可放心使用),当我们在客户端获取Date类的实例引用时,如果就可以对其内部状态进行修改。如上代码所示。

public Date getStart() {

       return new Date(start.toString());      //保护性拷贝

}

保护性拷贝:是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。这里我们修改了getStart方法,返回一个同原来值相同的新对象。这样,即使客户端获得了该对象的引用,也无法修改对象的内部状态,从而使该对象不可变。

不要引入可变对象对类的内部状态做出篡改,都应该使用不可变的对象作为对象内部的组件,这样就不必再为保护性拷贝操心。

40、谨慎设计方法签名
(1)方法名称的选择要遵守命名习惯

(2)不要过于追求提供便利的方法。方法复杂会使结构复杂,同样的方法太多会使接口实现者和接口用户的工作变得复杂。

(3)方法的参数列表不应该过长,目标是四个参数以下。

41、慎用重载
对于重载方法的选择是静态的,但是对于被覆盖的方法的选择则是动态的。

覆盖机制是规范,但是重载机制是例外。重载机制并不能完美的符合开发人员的期望,很有可能开发人员以为调用的接口和实际运行时被调用的接口不一致。避免胡乱的使用重载机制。安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。特别是如果方法使用了可变参数,那就不要重载它。

一个类的多个构造器总是被重载的。在许多情况下,可以选择导出静态工厂,而不是构造器。对于构造器,还不用担心重载和覆盖的相互影响,因为构造器不可能被覆盖。

Java语言中添加了泛型和自动自动装箱之后,破坏了List集合;
public static void main(String[] args) {
    Set<Integer> set = new TreeSet<>();
    List<Integer> list = new ArrayList<>();
    for (int i = -3;i<3;i++){
        set.add(i);
        list.add(i);
    }
    for (int i = 0 ;i<3;i++){
        set.remove(i);
        list.remove(i);
        System.out.println("set:"+set);
        System.out.println("list:"+list);
        System.out.println();
    }
}

一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。

42、慎用可变参数
可变参数:接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传给方法。

不必改造具有final数组参数的每个方法;只当确实是在数量不定的值上执行调用时才使用可变参数。如果使用不当,会产生混乱的结果。

在重视性能的情况下,使用可变参数机制要特别小心。可变参数的每次调用都会导致进行一次数组分配和初始化。如果无法承受这一成本,但是又需要可变参数的灵活性,那么可以:

public void foo(){…}

public void foo(int a1){…}

public void foo(int a1,int a2){…}

public void foo(int a1,int a2,int a3){…}

public void foo(int a1,int a2,int a3,int …rest){…}

95%的情况下会调用三个及三个以下的方法。

43、返回零长度的数组或者集合,而不是null
对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要额外处理返回null的情况。当客户端对这种情况没有处理时,就会出现错误。

总结:返回类型为数组或集合的方法没理由返回null,而是要返回一个零长度的数组或者集合。

44、为所有导出的API元素编写文档注释
(1)为了正确的编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前添加一个文档注释。

(2)方法的文档注释应该简洁地描述出它和它客户端之间的约定。

七、通用程序设计

讨论java语言的具体实现,讨论局部变量的处理、控制结构、类库的用法、各种数据类型的用法以及反射和本地方法。

45、将局部变量的作用域最小化
与第13条:使类和成员的可访问性最小化本质是类似的。将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。

(1)在第一次使用局部变量的地方声明。

(2)每个局部变量的声明都应该包含一个初始化表达式。

46、for-each循环优先于传统的for循环
for(Element e:Elements){};

(1)for-each循环隐藏了迭代器或者索引变量,避免了混乱和出错的可能。

(2)for-each循环的性能比for循环好。

无法使用for-each循环的情形

(1)过滤——如果需要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法。

(2)转换——如果需要遍历列表或者数组,并取代他部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。

(3)平行迭代——如果需要并行的遍历多个集合,就需要显示的控制迭代器或者索引变量,以便所有迭代器或者索引变量可以得到同步迁移。

在上面的任何一种情况下,就要使用普通的for循环,要警惕上面的陷阱。

47、了解和使用类库
java.lang、java.util、java.io…

java.util.concurrent(高级并发工具来简化多线程的编码任务)

48、如果需要精确的答案,请避免使用float和double
float和double类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。float和double尤其不适用与货币计算,因为要让一个float和double精确地表示10的任何负次方值都是不可能的(0.1,0.01,0.001…)

解决这个问题的正确方法是使用BigDecimal、int或者long进行货币计算。

int:数值范围不超过9位十进制数字(要求是整数,转换单位去掉小数点即可)

long:数值返回不超过18位十进制数字(要求是整数,转换单位去掉小数点即可)

BigDecimal:与使用基本运算类型相比,这样做不方便,而且很慢。但是其提供舍入的方式并且当数值范围超过18位必须用BigDecimal。

49、基本类型优于装箱基本类型
基本类型:int、float、double、boolean

引用类型:String、List

每个基本类型都有对应的引用类型:装箱基本类型,Integer、Float、Double、Boolean

区别:

(1)基本类型只有值,而装箱基本类型具有和它们的值不同的同一性。两个装箱类型的值是一样的,但是这两个对象在其他方法不是对等的。

(2)基本类型只有功能完备的值,但是装箱类型除了对应基本类型的所有功能值之外,还有非功能值null。

(3)基本类型比装箱类型更加节省时间和空间。

注意:

对装箱类型使用==操作符几乎都是错误的,其表明对对象进行同一性检测。

当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。如果装箱类型没有被初始化,那么自动拆箱会得到空指针异常。

当程序装箱了基本类型时,会导致高开销和不必要的对象创建。

什么时候使用装箱基本类型:

(1)作为集合中的元素、键和值。List而不是List。

(2)在进行反射调用的时候,必须使用装箱基本类型。

50、如果其他类型更何合适,尽量避免使用字符串
不适合使用字符串的情形:

(1)字符串不适合代替其他的值类型。<当一段数据从文件、网络等其他场合进入程序的时候,通常以字符串的形式存在。但是如果是数值形式就应该转换成数值形式,是布尔类型就转换成布尔类型>

(2)字符串不适合替代枚举类型。

(3)字符串不适合代替聚集类型。

(4)字符串不适合代替能力值。key值<不可伪造的键、能力键、唯一键值><实际上就是在某一场景中,用户输入唯一值来控制程序运行,但是存在恶意客户端可能输入相同的字符串,对已存数据进行修改。所以使用key而不是字符串来代表这个唯一值>

总结:如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。字符串会比其他的类型更加笨拙、更不灵活、速度更慢,也更容易出错。经常被错误的用字符串来代替的类型包括基本类型、枚举类型和聚集类型。

51、当心字符串连接的性能
为了连接n个字符串而重复使用字符串连接操作符,需要n的平方级时间。这是由于字符串不可变而导致的结果。当两个字符串被连接在一起时,它们的内容都要被拷贝。

为了获得性能,在连接字符串时,需要使用StringBuilder的append方法来代替String。

52、通过接口引用对象
第40条:使用接口而不是类作为参数的类型。更一般的讲,应该优先使用接口而不是类来引用对象。如果又合适的接口类型存在,那么对于参数、返回值、变量和域来说,这都应该使用接口类型进行声明。

直接使用类引用对象的情况:

(1)如果没有合适的接口存在,完全可以用类而不是接口来引用对象。

(2)对象属于一种框架,而框架的基本类型是类,不是接口。如果对象属于这种基于类的框架,就应该用相关的基类来引用这个对象,而不是它的实现类。

(3)类实现了接口,但是它提供了接口中不存在的额外的方法。如果程序依赖这些额外方法取实现某些功能,那就应该实现类。

总结:用接口引用对象会使得程序更加灵活;否则就使用类层次结构中提供了必要功能的最基础的类。

53、接口优先于反射机制
反射机制:提供了“通过程序来访问关于已装载的类的信息”的能力。

(1)给定一个类的Class实例,你可以获得这个类的Constructor的实例(类型为Constructor),Method的实例(类型为Method),Field的实例(类型为Field),分别代表了Class实例所代表类的Constructor(构造器),Method(方法)和Field(域)。(2)Constructor,Method和Field实例使你能够通过反射机制操作它们的底层对等体(就是这个类的构造器,方法,域),通过调用Constructor,Method和Field实例上的方法,(3)你可以构造底层类的实例,调用底层类的方法,并访问底层类中的域。反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在。

然而这种能力需要付出代价:

(1)丧失了编译时类型检查的好处,包括异常检查。如果程序企图通过反射机制调用不存在的或者不可访问的方法,编译时没有错误,但是运行时它将会失败。

(2)执行反射访问所需要的代码非常笨拙和冗长。阅读困难,编写乏味。

(3)性能损失。

反射功能只是在设计时被用到。通常,普通应用程序在运行时不应该以反射的方式访问对象。如果只是以非常有限的形式来使用反射机制,虽然需要付出少许代价,但是可以获得许多好处。

对于有些程序,它们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用到这个类。如果是这种情况,就可以以反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。

总结:反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,但是它也有一些缺点。如果你编写的程序必须要和你编写的程序编译时未知的类一起工作,如有可能,就应该使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。

或者动态的调用多个方法的组合。用反射的形式比较方便

54、谨慎的使用本地方法
本地方法:用本地程序设计语言(C/C++)来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,并返回到Java程序设计语言。

本地语言的用途:

(1)提供了“访问特定于平台的机制”的能力,比如访问注册表和文件锁。

(2)提供了访问遗留代码库的能力,从而可以访问遗留数据。

(3)本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。但是除非是很有必要,确实可以提高性能,否则不提倡使用本地方法来提高性能。

总结:本地语言不安全,为了和java契合,需要写一些代码来“粘合”,不易阅读。在使用本地方法之前需要仔细考虑,如果是必须使用本地方法来访问底层资源,或者遗留代码库,那么就使用本地代码,并且开发完之后需要仔细测试。

55、谨慎地进行优化
关于优化:

优化弊大于利,产生的结果可能是既不快速也不正确,而且很不容易修正

不要因为性能去牺牲合理的结构,努力写好的程序而不是快的程序

但是这不意味着完成程序前可以忽略性能问题

(1)实现上的问题可以通过后期优化得到优化,但是遍布全局的结构性性能缺陷是不可能修复的,除非重做系统。

(2)设计之初必须全盘考虑,系统完成后进行基本结构调整会导致系统结构很不好,而且很不方便维护和改进。

(3)努力限制那些限制性能的设计决策,API 设计对性能的影响是非常实际的。

(4)最难以更改的组件是那些指定了模块之间交互关系和模块与外界交互关系的组件。最主要的要数API、线路层协议、永久数据格式,而且可能对本该达到的性能产生严重限制。要考虑API 设计决策的性能后果。

总结:不要费力的去编写快速的程序,应该努力的编写好的程序,速度自然而然会随之而来。

56、遵守普通接受的命名惯例
(1)包的名称应该是层次状的,用句号分隔每个部分。包名称应该包含一个或者多个描述该包的组成部分。

(2)类和接口的名称,包括枚举和注解类型的名称,都应该包括一个或者多个单词,每个单词的首字母大写。

(3)方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例,但是方法和于首字母小写。

(4)常量:包含一个或者多个大写的单词,多个单词用下划线隔开。

(5)类型参数:单个大写字母,E、T、V、X

八、异常

57、只针对异常的情况才使用异常
(1)异常机制的设计初衷是用于不正常的情况,所以很少会有JVM的实现试图对它们进行优化,使得与显式的测试一样快速。

(2)把代码放在try-catch块中反而会阻止了现代JVM实现本来可能要执行的某种特定优化。

(3)对数组进行遍历的标准模式并不会导致冗余的检查。有些现代的JVM实现会将它们优化掉。

实际上,在现代的JVM实现上,基于异常的模式比标准模式要慢得多。异常应该只用于异常的情况下,它们永远不应该用于正常的控制流。设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。

58、对可恢复的情况使用受检异常,对编译错误使用运行时异常
java程序设计语言提供三种可抛出结构

受检的异常:如果期望调用者能够适当的恢复,那应该使用受检的异常。

运行时异常:程序错误。

错误

受检异常:必须要捕获;

非受检异常:可捕获也可以不捕获;

受检异常和非受检异常:都是在程序运行时出现的错误,但是受检异常和非受检异常的处理方式是不一样的。

受检异常:增加代码的健壮性,但是为了对异常进行抛出、捕获和处理异常需要增加较多代码,会降低代码的可读性。

如果异常影响到了系统运行的安全性和正确性的时候,必须对受检异常进行处理,否则这些受检异常是可以装换成非受检异常。

59、避免不必要地使用受检的异常
过分的使用受检异常会使API使用起来很不方便,如果没有必要或者没有能力写受检异常,那就不要写。

60、优先使用标准的异常
重用标准异常时要确保抛出异常的条件与标准异常的文档中的描述一致。

61、抛出与抽象相对应的异常
异常转译:更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。异常转译使得异常语义更加明确

异常链:允许逐层进行异常分析。底层的异常对于调试导致高层异常的问题非常有帮助。

如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证他抛出的所有异常对高层也适合才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:他允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。

62、每个方法抛出的异常都要有文档
始终要单独的声明受检的异常,并且利用Javadoc的@throws标记,准确的记录下抛出每个异常的条件。

如果一个方法可能抛出多个异常类,不要抛出这些异常类的某个超类。比如永远不要声明一个方法“throws Exception”,或者声明“throws Throwable”,这是非常极端的例子。这样的声明不仅没有提供任何指导信息,而且大大的妨碍了该方法的使用,因为他实际上掩盖了该方法在同样的执行环境下可能抛出的任何其他异常。

使用Javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。当缺少由throws声明产生的方法标头时,由Javadoc的@throws标签所产生的文档就会提供明显的提示信息,以帮助程序员区分受检的异常和未受检的异常。

总结:要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检和受检的异常,以及对于抽象的和具体的方法也都一样。要为每个受检异常提供单独的throws子句,不要为未受检的异常提供throws子句。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。

63、在细节消息中包含能捕获失败的信息
对于受检异常,应该尽可能的打印一些详细的、可能导致异常发生的信息,以便在发生错误的时候可以快速的定位失败原因。

64、努力使失败保持原子性
失败原子性:失败的方法调用应该使对象保持在被调用之前的状态。

(1)将对象设置成不可变的;

(2)对可变对象执行操作之前检查参数的有效性;

(3)调整计算处理过程的顺序,使得任何可能失败的计算部分都在对象状态被修改之前发生。

(4)编写恢复代码,用来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。

(5)在对象的一份临时拷贝上执行操作,当操作完成之后再勇临时拷贝中的结果代替对象的内容。

错误通常是不可恢复的,当方法抛出错误时,不需要努力保持原子性。

65、不要忽略异常
用空的catch块处理异常,将会导致程序在遇到错误的情况下悄然地执行下去,从而产生更大的错误。

九、并发

线程机制允许同时进行多个活动。

66、同步访问共享的可变数据
关键字synchronized(同步)

(1)当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态;

(2)同步不仅可以组织一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。

为了提高性能,在读写原子数据的时候,应该避免使用同步。这个建议是非常危险而且错误的。因为,虽然读写原子数据都是原子操作,但是不保证一个线程的写入的值对于另一个线程是完全可见的(值得一提的是,究竟什么样的原子变量必须进行同步,是需要看情况的)。因此,为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。没有同步,各个线程之间对象的变化是相互不感知的。

进入的各个线程都能看到,同一个锁保护的之前所有的修改信息。就是说不同步,别的线程改变对象值,本线程不一定何时能看到修改后的值。

Java语言规范保证读或写一个变量是原子的,除非这个变量的类型为long或者double。换句话说,读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中,即使多个线程在没有同步的情况下并发的修改这个变量也是如此。

实际上,如果读和写操作没有被同步,同步就不会起作用。

避免问题的最佳办法是不共享可变的数据。要么共享不可变的数据,要么压根不共享。换句话说,将可变数据限制在单个线程中。如果采用这一策略,对它建立文档就很重要,以便他可以随着程序的发展而得到维护。深刻理解正在使用的框架和类库也很重要,因为他们引入了你所不知道的线程。

让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,只同步共享对象引起的动作。然后其他线程没有进一步的同步也可以读取对象。只要它没有再被修改。这种对象被称作事实上不可变的。将这种对象引用从一个线程传递到其他的线程被称作安全发布。安全发布对象引用有许多种方法:可以将它保存在静态域中,作为类初始化的一部分;可以将它保存在volatile域、final域或者通过正常锁定访问的域中,或者可以将它放到并发的集合中。

总结:当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步,如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败和安全性失败。这样的失败时最难以调试的。他们可能是间歇性的,且与时间相关,程序的行为在不同的VM可能根本不同,如果只需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式,但要正确的使用它可能需要一些技巧。

67、避免过度同步
依据情况的不同,过度同步可能会导致性能降低、死锁,甚至不确定的行为。同步区域中调用外来方法会导致异常、死锁或者数据损坏。

Java类库提供了一个并发集合,称作CopyOnWriteArrayLIst,这是专门用来将外来方法的调用移出同步的代码块。

开放外调:在同步区域之外被调用的外来方法被称作。除了可以避免死锁之外,开放调用还可以极大地增加并发性。外来方法的运行时间可能会任意长。如果在同步区域内调用外来方法,其他线程对受保护资源的访问就会遭到不必要的拒绝。

应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个很耗时的动作,则应该设法把这个动作移到同步区域的外面。

过度同步失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,他会限制VM优化代码执行的能力。

如果你的内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁、分离锁和非阻塞并发控制。

如果方法修改了静态域,那么你也必须同步对这个域的访问,即使他往往只用于单个线程。客户要在这种方法上执行外部同步时不可能的,因为不可能保证其他不想关的客户也会执行外部同步。

总结:为了避免死锁和数据破坏。千万不要从同步区域内部调用外来方法。更为一般的讲,要尽量限制同步区域内部的工作量。当你在设计一个可变类的时候,要考虑一下他们是否应该自己完成同步操作。在现在这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中。

68、executor和task优先于线程
java.util.concurrent这个包中包含了Executor Framework,这是一个很灵活的基于接口的任务执行工具。它创建了一个在各方面都比工作队列更好,却只需要这一行代码:

ExecutorService executor = Executors.newSingleThreadExecutor();

执行提交一个runnable的方法:executor.execute(runnable);

executor如何优雅的终止:executor.shutdown()。

executor service

(1)可以等待完成一项特殊的任务(后台线程)

(2)可以等待一个任务中任何任务或者所有任务完成(利用invokeAny、invokeAll方法)

(3)可以等待executor service优雅的完成终止(利用awaitTermination方法)

(4)可以在任务完成时逐个地获取这些任务的结果(利用ExecutorCompletionService)

java.util.concurrent.Executors类包含了静态工厂,能为你提供所需的大多数executor。也可以直接使用ThreadPoolExecutor类。这个类允许控制线程池操作的几乎每个方面。

在大负载的产品服务器中,最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度的控制它,就直接使用ThreadPoolExecutor类。

尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。现在关键的抽象不再是Thread了,它以前可是既充当工作单元,又是执行机制。现在工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务(task)。任务有两种:Runnable及其近亲Callable(它与Runnable域类似,但它会返回值)。执行任务的通用机制是executor service。如果从任务的角度来看问题,并让一个executor service替你执行任务,在选择适当地执行策略方面就获得了极大地灵活性。从本质上讲Executor Famework所做的工作是执行,犹如Collections Framework所做的工作是聚集(aggregation)一样。

Executor Framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolExecutor。虽然timer使用起来更加容易,但是被调度的线程池executor更加灵活。timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果timer唯一的线程抛出未被捕获的异常,timer就会停止执行。被调度的线程池executor支持多个线程,并且优雅的从抛出未受检异常的任务中恢复。

69、并发工具优先于wait和notify
java.util.concurrent中更高级的工具分成三类:Executor Framework、并发集合

(Concurrent Collection)以及同步器(Synchronizer)。他们可以完成以前必须在wait和notify上手写代码来完成的各项工作。

ConcurrentHashMap除了提供卓越的并发性之外,速度也非常快。除非不得已,否则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable。只要用并发Map替换老式的同步Map,就可以极大地提升并发应用程序的性能。更一般的,应该优先使用并发集合,而不是使用外部同步的集合。

同步器是一些使线程能够等待另一个线程的对象,允许他们协调动作。最常用的同步器是CountDownLatch和Semaphore。较不常用的是CyclicBarrier和Exchanger。

倒计数锁存器(CountDownLatch)是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。CountDownLatch的唯一构造器带有一个int类型的参数,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。

wait方法被用来使线程等待某个条件,它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用wait方法的对象上。始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。

为了唤醒正在等待的线程,总是应该使用notifyAll。

从优化的角度来看,如果处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从这个条件中被唤醒,那么你应该选择调用notify,而不是notifyAll。

总结:直接使用wait和notify就像用“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的语言。没有理由在新代码中使用wait和notify,即使有、也是极少的。如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait。一般情况下,你应该优先使用notifyAll,而不是使用notify。如果使用notify,请一定要小心,以确保程序的活性。

70、线程安全性的文档化
因为在一个方法声明中出现synchronized修饰符,这是个实现细节,并不是导出的API的一部分。她并不一定表明这个方法是线程安全的。

而且,“出现了synchronized关键字就足以用文档说明线程安全性”的这种说法隐含了一个错误的观念,即认为线程安全性是一种“要么全有要么全无”的属性。实际上,线程安全性有多种级别。一个类为了可被多个线程安全的使用,必须在文档中清楚地说明他所支持的线程安全性级别。

不可变的(immutable)——这个类的实例是不变的。所以,不需要外部的同步。这样的例子包括String、Long和BigInteger。

无条件的线程安全(unconditionally thread-safe)——这个类的实例是可变的,但是这个类有足够的内部同步,所以,它的实例可以被并发使用,无需任何外部同步。其例子包括Random和ConcurrentHashMap。

有条件的线程安全(conditionally thread-safe)——除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。这样的例子包括Collections.synchronized包装返回的集合,他们的迭代器要求外部同步。

非线程安全(not thread-safe)——这个类的实例是可变的。为了并发的使用他们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。这样的例子包括通用的集合实现,例如ArrayList和HashMap。

线程对立的(thread-hostile)——这个类不能安全的被多个线程并发使用,即使所有方法调用都被外部同步包围。线程对立的根源通常在于,没有同步的修改静态数据。没有人会有意编写一个线程对立的类;这种类是因为没有考虑到并发性而产生的后果。幸运的是,在Java平台类库中,线程对立的类或方法非常少。System.runFinalizersOnExit方法是线程对立的,但已经被废除了。

总结: 每个类都应该利用字斟句酌的说明或者线程安全注解,清楚地在文档中说明他的线程安全属性。sychronized修饰符与这个文档毫无关系。有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候要获得哪把锁”。如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活的对并发控制采用更加复杂的方法。

71、慎用延迟初始化
延迟初始化:延迟到需要域的值时才将它初始化的这种行为。

(1)在大多数情况下,正常的初始化要优先于延迟初始化。

(2)如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法,因为它是最简单、最清楚地替代方法。

(3)如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。这种模式保证了类要被用到的时候才会被初始化。

(4)如果出于性能的考虑而需要对实例域使用延迟初始化,就是用双重检查模式。

总结: 大多数的域应该正常进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,就是用双重检查模式;对于静态域,则使用lazy initialization holder class。对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式。

72、不要依赖于线程调度器
线程调度器:当有多个线程可以运行时,决定哪些线程将会运行,以及运行多长时间。但是不同os所采用的策略却大相径庭。因此,程序不应该依赖于这种策略的细节任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。

(1)如果线程没有在做有意义的工作,就不应该运行。

(2)不要企图通过调用Thead.yield来“修正”该程序。

(3)线程优先级是Java平台上最不可移植的特征了。

总结: 不要让应用程序的正确性依赖于线程调度器。否则,结果得到的应用程序将既不健壮,也不具有可移植性。作为推论,不要依赖Thread.yield或者线程优先级。这些设施仅仅对调度器做些暗示。线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本并不能工作的程序。

73、避免使用线程组
线程组:作为一种隔离applet(小程序)的机制,但是安全性极差。他们允许你同时把Thread的某些基本功能应用到一组线程中。其中有一些基本功能已经被废弃了,剩下的也很少使用。

总结:线程组并没有提供太多有用的功能,而且他们提供的许多功能还是有缺陷的。我们最好把线程组看做是一个不成功的试验,你可以忽略掉他们。如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor。

十、序列化

74、谨慎地实现Serializable接口
总结:实现Serializable接口并不容易。除非一个类在用了一段时间之后就会被抛弃,否则,实现Serializable接口就是个很严肃的承诺,必须认真对待。如果一个类是为了继承而设计的,则更加需要加倍小心。对于这样的类而言,在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”两者之间的一个折中设计方案是,提供一个可访问的无参构造器。这种设计方案允许(但不要求)子类实现Serializable接口。

75、考虑使用自定义的序列化形式
总结:当你决定要将一个类做成可序列化的时候,请仔细考虑应该采用什么样的序列化形式。只有当默认的序列化形式能够合理的描述对象的逻辑状态时,才能使用默认的序列化形式;否则就要设计一个自定义的序列化形式,通过它合理的描述对象的状态。你应该分配足够多的时间来设计类的序列化形式,就好像分配足够多的时间来设计它的导出方法一样。正如你无法再将来的版本中去掉导出方法一样,你也不能去掉序列化形式中的域;他们必须被永久的保留下去,以确保序列化兼容性。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响。

76、保护性的编写readObject方法
每当编写readObject方法的时候,都要这么想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例。

readObject方法指导方针:

(1)对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。

(2)对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。

(3)如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。

(4)无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。

77、对于实例控制,枚举类型优先于readResolve
应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同步时又需要一个既可序列化又是实例控制的类,就必须提供一个readResolver方法,并确保该类的所有实例域都为基本类型,或者是transient的。

78、考虑用序列化代理代替序列化实例
当必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想稳健的将带有重要约束条件的对象序列化时,这种模式可能是最容易的办法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值