读effection java
1.考虑用静态工厂方法代替构造器
public static Boolean valueOf(boolean b){
return b?Boolean.TRUE:Boolean.FALSE;
}
静态工厂方法与构造器不同的第一大优势在于,它们有名称,有名称可以更好地构建清晰的对象.
静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象,实例受控的类.
静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象,API可以返回对象,同时又不会使对象的类变成公有的.比如Java Collections Framework的集合接口.
通过接口来引用被返回的对象,而不是通过实现类.
灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework),例如jdbc,服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来.
其有三个重要的组件:服务接口(Service Interface),提供者实现的;提供者注册API(Provider Registration API),这是系统用来额实现,让客户端访问它们的;服务访问API(Service Access API),是客户端用来获取服务的实例的.服务提供者接口,jdbc通过反射方式进行实例化,connection就是它的服务接口.DriverManager.registerDriver是提供注册者API,getConnection是服务访问API,Driver是服务提供者接口.
静态工厂方法的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁,newInstance.
静态工厂方法的缺点有二,1类如果不含公有的或者受保护的构造器,就不能被子类化.2它们于其他的静态方法实际上没有任何区别.
valueOf,getInstance,newInstance,getType
2.遇到多个构造器参数时要考虑用构建器
超过三个构造器参数时,请使用Builder模式.
在类里面加一个Builder类,每个方法都返回Builder类型,最后一个总的build方法.
这个方法在大类中为private,取的实例来自builder.
不足,必须先创建它的 ,成本比较大.
3.用私有构造器或者枚举类型强化Singleton属性
Singleton指仅仅被实例化一次的类.
第一种方法,开个public static final域,在这个域中new 对象,注意构造函数为private.
第二种方法,公有的成员是个静态工厂方法,public static Elvis getINstance(return INSTANCE)private static final 域
其实这两种方法没有什么区别,第二种方法加了个方法.
公有域方法的主要好处在于,组成类的成员的声明很清楚地表面了这个类是一个Singleton;公有的静态域是final的,所以该域总是包含相同的对象引用.现代的Jvm实现几乎总是能将静态工厂方法的调用内联化.
enum类型,public enum Elvis{INSTANCE;}这就是一个单例,并且无偿提供了序列化机制,这是最佳方法.
4.通过私有构造器强化不可实例化的能力
企图通过将类做成抽象类来强制该类不可被实例化,是行不通的.因为可以子类化.
private Utils(){throw new AssertionError()};
5.避免创建不必要的对象
对于同一台虚拟机中运行的代码,只要包含相同的字符串字面常量,该对象就会被重用.
String s = "str";
延迟初始化(lazy initializing),把对这些域的初始化延迟到第一次被调用的时候进行,就会消除这些不必要的初始化工作.
小对象的创建代价是非常廉价的,特别是在现代的jvm中更是如此.
通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法.除非数据库?
注意创建新对象的保护性拷贝
6.消除过期的对象引用
考虑一个简单的堆栈实现例子.
public class Stack{
private Object[] elements;
private int size=0;
private static final int DEFAULT_INITIAL_CAPACITY=16;
public Stack(){
elements=new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapactity();
elements[size++]=e;
}
public Object pop(){
if(size=0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity(){
if(elements.length==size)
elements=Arrays.copyOf(elements,2*size+1);
}
}
这里有一个内存泄露,对于size上面的没使用的部分,因为stack一直被引用着,那么之后的非活性部分不会被垃圾回收期回收,过期引用.
准确的就是加上elements[size]=null;
注意:清空对象引用应该是一种例外,而不是一种规范行为.
主要是stack类自己管理内存.只要类自己管理内存,我们就应该警惕内存泄露问题.
另一个常见来源是缓存.可以用weakHashMap来代表缓存.
第三个常见来源是监听器和其他回调.在这个API中注册回调,却没有显示地取消注册,那么就会几句.回调应该保存它们的弱引用?
7.避免使用终结方法
finalize(),finallizer,通常是不可预测的,也是很危险的,一般清空下是不必要的,不要把终结方法当成是C++中的析构器.不能保证及时的执行.
使用终结方法有性能损失.
显示终结方法的例子是inputstream和connection的close方法,温和地终止自己.
好处:安全网?本地对等体(native peer)?终止非关键的本地资源.
8.覆盖equals时请遵守通用的约定
不覆盖equals时,需满足以下条件,类的每个实例本质上都是唯一的,如代表活动实体的Thread;不关系类是否提供了逻辑相等的测试功能,如Random;超类已经覆盖了equals,
从超类继承过来的行为对于子类也是合适的,如List实现从AbstractList继承equals实现;类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用.
如果类具有自己特色的逻辑相等概念,而且超类没有实现equals,那么我们就必须自己覆盖.
equals方法实现了等价关系,如自反性,对于任何非null的引用值x,x.equals(x)必须返回true;对称性,xy,yx;传递性;一致性,多次调用不会改变;对于任何非null的值,必须返回false.
里斯替换?
高质量equals方法,==操作符来检查参数是否为这个对象的引用;使用instanceof操作符检查参数是否为正确的类型;把参数转换成正确的类型;对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配.
问自己3个问题,是否是对称的\传递的\一致的.
不要将equals声明中的object对象替换为其他的类型.
9.覆盖equals时总要覆盖hashCode
在包含HashMap\HashSet\Hashtable里面,可能就有问题.
没有覆盖hashCode,那么相等的对象可能没有相等的散列码.
什么是一个好的散列函数,对于对象中每个域进行计算,偶去散列值.
不要试图从散列码中排除掉一个对象的关键部分来提高性能.
10.始终要覆盖toString
提供好的toString实现可以使类用起来更加舒适.
toString方法应该返回对象中包含的所有值得关注的信息.
11.谨慎地覆盖clone
Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆,Object的clone方法是受保护的.无调用器就可以创建对象.
永远不要让客户去做任何类库可以替客户完成的事情.clone方法就是另一个构造器,必须确保它不会伤害到原始的对象.
拷贝构造器?拷贝工厂?
12.考虑实现Comparable
compareTo方法并没有在Object中声明.一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作.
不过要注意符合逻辑.自反性,对称性,传递性.
13.使类和成员的可访问性最小化
一个模块不需要知道其他模块的内部情况.这个概念被称为信息隐藏或者封装,这是软件设计的基本原则之一.
访问控制机制决定了类\接口\成员的可访问性.尽可能地使每个类或者成员不被外界访问,对于类和接口-要么包级私有要么公有的,对于成员-私有的-包级私有的-受保护的-
公有的,实例域决不能是公有的;类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的.如果是一个数组,可以返回一个公有的不可变列表,偶这返回私有
数组的一个备份.?
14.在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就提供访问方法.如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误.
公有类永远都不应该暴露可变的域.
15.使可变性最小化
不可变类只是其实例不能被修改的类.每个实例中包含的所有信息都必须在创建该实例的时候就提供.
为使类不可变,需遵循下面5条原则.不要提供任何会修改对象状态的方法;保证类不会被扩展;使所有的域都是final的;使所有的域都成为私有的;确保对于任何可变组件的互斥访问.
考虑函数的做法以及过程的做法.
不可变对象本质上是线程安全的,它们不要求同步.不仅可以共享不可变对象,甚至也可以共享它们的内部信息.不可变对象为其他对象提供了大量的构建;不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象如int.
公有静态工厂.?
外部可见.延迟初始化?
16.复合优先于继承
与方法调用不同的是,继承打破了封装性.子类依赖于其超类中特定的功能的实现细节.超类的实现有可能会随着发型版本而变化,子类可能会遭到破坏.
增加比覆盖略微安全.复合或许更好,引用别的类的实例.转发方法.委托.delegation.
包装类不适合拥在回调框架中.?只有当子类真正是超类的子类型时,才适合用继承.
17.要么为继承而设计,并提供文档说明,要么就禁止继承
可覆盖,方法自用性.类必须提供某种形式的钩子(hook),以便能够进入到它的内部工作流程中,这种形式是可以精心选择的受保护的.
为了继承而设计的类,唯一的测试方法就是编写子类.
为了继承而设计类,对这个类会有一些实质性的限制.
最好的解决方法,就是禁止子类化.
18.接口优先于抽象类
现有的类可以很容易被更新,以实现新的接口.只要implements就可以了.
接口是定义mixin(混合类型)的理想选择,类除了实现它的基本类型之外,还可以提供某些行为.
接口允许我们构造非层次结构的类型框架.比如一个人可以扮演多个角色.
对于包装类,接口使得安全地增强类的功能成为可能.
通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来.
如果设计得当,骨架可以使程序员很容易提供它们自己的接口实现.可以使用内部私有类扩展骨架实现类,这种方法被称为模拟多重继承.
抽象类的演变比接口的演变压迫容易地多
19.接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型
有一种接口被称为常量接口。常量接口是对接口的不良使用。
应该使用不可实例化的工具类来导出这些常量。也可以使用静态导入机制来避免用类名来修饰常量名。
20.类层次优于标签类
带有两种甚至多种风格的实例的类,并包含表示实例风格的标签域,
标签类正是类层次的一种简单的仿效。
为标签中的每个方法都定义一个抽象类的抽象方法,再由子类去实现。
类层次可以翻译类型之间本质上的层次关系。有助于增强灵活性。并进行更好的编译时类型检查。
21.用函数对象表示策略
例如C语言中的qsort函数要求提供的comparator函数指针其实是一种策略模式的实现。java没有提供函数指针,但是可以用对象引用来实现同样的功能。调用对象上的方法通常是执行该对象上的某项操作。它的方法执行其他对象上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。作为具体策略类,其是无状态的。所以可以用Singleton。可以定义一个接口,策略接口来做,接口是泛型的。具体的策略类往往使用匿名类声明。
如果策略接口被用作所有具体策略实例的类型,所以可以做成私有嵌套类。节省对象开销。用公有的final域导出。
class Host{
private static class StrLenCmp implements Comparator
public int compare(String s1,String s2){
return s1.length()-s2.length();
}
}
public static final Comparator
...//Bulk of class omitted
}
22.优先考虑静态成员类
嵌套类是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为它的外围类提供服务。嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类。除了静态成员类,其他都被称为内部类。
静态成员类是一种普通的类,可以访问外围类的所有成员;作为公有的辅助类。非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。匿名类的使用有许多限制,比如不能使用Instanceof;主要可以创建函数对象(Comparator实例)、创建过程对象(Runnable,Thread),用在静态工厂方法的内部,如intArrayAsList。r如果不能用匿名类,那就用局部类。
23.请不要在新代码中使用原生态类型
有了泛型就要上,类型检查的好处,还有原生态是为了兼容。对于某些东西可以使用通配符的泛型。
24.消除非受检警告
泛型的警告要去消除,尽量不使用SuppressWarnings。
25.列表优先于数组
数组是协变的(covariant),就是有子类型的;泛型则是不可变的,对于两个不同的类型,没有子类型一说。数组是具体化的,只有在运行时才能知道并检查它们的元素类型约束。数组和泛型不能很好的使用。创建泛型数组是非法的,除非是无限制通配类型的数组是合法的。列表List有时候会有并发问题,考虑用一个synchronized来锁住,但是因为是调用外来的方法,那么需要先list.toArray(会在内部锁定列表);也可以锁定一个列表,然后synchronized(list){snapshot=new ArrayList
26.优先考虑泛型
一般来说,编写自己的泛型相对困难些。适当地强化某个类来利用泛型。E是一个不可具体化的类型,编译器无法在运行时检验转换。不能创建不可具体化的类型的数组,如E。可以使用SupressWarnings来消除警告。有两种方法来做,具体见书。
27.优先考虑泛型方法
静态工具方法尤其适合于泛型化。泛型静态工厂,来消除引用和对象都有的冗余。
泛型单例工厂,创建不可变但又适用于许多不同类型的对象。
递归类型限制?通过某个包含该类型参数本身的表达式来限制类型参数是允许的。
最普遍的用途与Comparable接口有关。
28.利用有限制通配符来提升API的灵活性
参数化类型是不可变的。泛型的问题会出现。不过可以使用有限制的通配符类型。
bounded wildcard ,? Extends E,代表这个类型是某个类型的子类型。
为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。
? super T,也是可以的代表这个类型是某个类型的父类型。
使用通配符类型虽然需要一点技巧,但是使API变的灵活许多。
producer-extends,consumer-super(PECS?),生产者继承,消费者父类。
所有的comparable和comparator都是消费者。
29.优先考虑类型安全的异构容器.
可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用Class对象作为键。以这种方式使用的Class对象称作类型令牌。也可以使用定制的键类型。例如,用一个DatabaseRow类型表示一个数据库行(容器),用泛型Column
不像普通容器,它的所有键都是不同类型的。因此,我们将XX称作类型安全的异构容器。
如ThreadLocal和AtomicReference。?
30.用enum代替int常量
int枚举模式是非常不好的,同样的还有String枚举模式.java的枚举本质上是int值,其通过公有的静态final域为每个枚举常量导出实例的类.因为没有可以访问的构造器,枚举类型是真正的final.枚举类型是实例受控的,他们是单例的泛型化.如果枚举具有普遍适用性,就应该成为一个顶层类,如果它只是被用在一个特定的顶层类中,它就应该成为该顶层类的一个类.
特定于常量的类主体,枚举中加方法块.
策略枚举?私有的枚举,private enum PayType
通常,许多枚举都不提供显式的构造器或成员,如果多个枚举常量同时共享相同的行为,则考虑策略枚举.
31.用实例域代替系数
永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中.
如SOLO(1),DUET(2),TRIO(3)
32.用EnumSet代替位域
int枚举模式,将2的不同倍数赋予每个常量,将几个常量合并到一个集合中,称为位域
枚举类型用在集合中,所以没有理由用位域来表示它.?
33.用EnumMap代替序数索引
最好不要用序数来索引数组,而要使用EnumMap?
34.用接口模拟可伸缩的枚举
定义一个枚举类型,实现某个接口,达成可伸缩的枚举.
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟.
35.注解优先于命名模式
naming pattern,表明某些程序元素需要通过某种工具或者框架进行特殊处理.
有几个缺点,1.文字拼写错误会导致失败2.无法确保只是用于相应的程序元素上3.没有提供将参数值于程序元素关联起来的好方法.
@Retention
标记注解?
应该使用预定义的注解类型.
36.坚持使用Override注解
应该在你想要覆盖超类声明的每个方法声明中使用Override注解
37.用标记接口定义类型
标记接口是没有包含方法声明的接口,而只是指明了一个类实现了具有某种属性的接口.例如
SERIALIZABLE接口.
标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型.这个类型允许你在编译时捕捉在使用标记注解的情况下要到运行时才能捕捉到的错误.
Set接口就是这种有限制的标记接口
标记注解胜过标记接口的最大优点在于,它可以通过默许的方式添加一个或多个注解类型元素,给已经被使用的注解类型添加更多的信息.
标记注解是更大的注解机制的一部分.
如果编写的目标是ElementType.TYPE的类型,是否标记接口更合适.?
可用性\健壮性\灵活性
38条:检查参数的有效性
检测参数,如不能为null,并且传递出去适当的异常.
对于非公有的方法,可以使用assert来做参数检测,如果断言失败,会抛出AssertionError
除非有效性检查比较昂贵,如排序.
39条:必要时进行保护性拷贝
方法调用时,考虑使用保护性拷贝,改变原来的对象.
java是一门安全的语言,这意味着,它对于缓冲区溢出\数组越界\非法指针以及其他的内存破坏错误都自动免疫.
使用不可变的对象.
40条:谨慎设计方法签名
谨慎地选择方法的名称,不要过于追求提供便利的方法(如果不能确定,还是不提供快捷为好),
避免过长的参数列表(目标是四个参数,或者更少)(拆分过长的参数列表,1分解成多个方法,每个方法只是一个子集,提升正交性,如List接口,power-to-weight
2.创建辅助类bean3.从对象构建到方法调用都采用Builder模式)
对于参数类型,优先使用接口而不是类
对于boolean参数, 要优先使用两个元素的枚举类型(使代码更易于阅读和编写).
41条:慎用重载
重载(overload),一个方法名,根据参数类型和数量的不同,这些方法是不同的.
重写(override),子类重写父类的方法,覆盖.
重载方法的选择是静态的,选择的方法是编译时类型.
重写方法的选择是动态的,选择的方法是运行时类型.
覆盖机制是规范,重载机制是例外.
对于重载机制,安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法,如果使用可变参数,不要重载它.
同一组参数在重载方法的行为应该一致,比如经过类型转换就可以被传递为不同的重载方法就是不行的.
如List接口,对于remove方法,既有index也有object参数,如果传进来的是int参数,
那么可能会删除的是list中的第一个元素.
而Set接口,对于remove方法,只有object参数,那么会比较明确
42.慎用可变参数
就是...
只有确实存在数量不定的值上调用时才使用可变参数.
比较少的参数时请使用重载方法.
43.返回零长度的数组或者集合,而不是null
如Android中的适配器里面的list
44.为所有导出的API元素编写文档注释
doc?
45.将局部变量的作用域最小化
第一次使用它的地方声明,应该包含初始化表达式,尽量优先for循环.
46.for-each循环优先于传统的for循环
除了过滤-转换-平行迭代.
47.了解和使用类库
尽可能地使用标准类库,
怎么取随机整数,有Random.nextInt(int)
数论?2的求补算法?伪随机数生成器
熟悉java.lang和java.util以及java.io
48.如果需要精确的答案,请避免使用float和double
float和double类型主要是为了科学计算和工程计算而设计的.他们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的.然而,他们并没有提供完全精确的结果.float和double类型尤其不适合用于货币计算.
请使用BigDecimal\int(不超过9位数字)\long(不超过18位数字)进行货币计算.
49.基本类型优先于装箱基本类型.
基本类型只有值,装箱基本类型除了有值还有个非功能值null,对装箱基本类型运用==操作符基本上都是错的.如果混合使用基本类型和装箱基本类型,装箱基本类型会自动拆箱,如果null对象被自动拆箱就会得到一个null指针异常
使用装箱基本类型是因为有些地方必须要用到对象,比如集合中的元素\键和值,还有参数化类型.
50.如果其他类型更适合,则尽量避免使用字符串.
字符串不适合代替其他的值类型,比如int等
字符串不适合代替枚举类型.
字符串不适合代替聚集类型,如果一个实体有多个组件,用一个字符串来表示二哥实体通常是很不恰当的.更好的做法是编写一个类来描述这个数据集.
字符串也不适合代替能力表(capabilities),考虑线程局部变量,
public final class ThreadLocal{public ThreadLocal(){}public void set(Object value)public Object get();}这个API不是类型安全的,可以将其类泛型化就可以了.而不是用string来做键.
51.当心字符串连接的性能
+是把多个字符串合并为一个字符串的便利途径.但不适合用在大规模的场景中,因为连接的时间复杂度为n的平方级时间.字符串不可变,连接在一起时,内容都要被拷贝.考虑使用StringBuilder代替String,使用append方法.
52.通过接口引用对象
如果有合适的接口类型存在,那么对于参数\返回值\变量和域来说,就都应该使用接口类型进行声明.
使用接口作为类型,程序将会更为灵活.
如果没有合适的接口存在,完全可以用类而不是接口来引用对象;如果对象属于框架,那么使用基类也可.
53条:接口优先于反射机制
使用反射方法,
优点:解决编译问题,兼容版本,主要用在sdk中比较多.比如spring的ioc和di.
缺点:慢,代码冗长,没有编译时类型检查
54.谨慎地使用本地方法navive interface,
用途有三,1:访问特定于平台的机制的能力,如注册表和文件锁.
2:访问遗留代码库的能力
3:通过本地语言,编写应用程序汇总注重性能的部分,如一些高性能媒体库.目前很多用java进行优化了,可以不用这么做.
缺点:1:不安全,内存问题,比较难查找错误.2:平台相关,进入和退出均有固定开销,需要本地方法提高较多性能,否则不划算.3:代码编写比较乏味.
55.谨慎地进行优化
规则1:不要进行优化
规则2:如果还没有绝对清晰的未优化方案之前,请不要进行优化.
应该从架构层面就设计良好的程序.
努力编写好的程序而不是快的程序.
我犯了一个优化错误,让十个人左右周末加班,周六一直到晚上2点钟.
56.遵守普遍接受的命名惯例
比如驼峰.
关于类,包,接口,方法,变量等.
常量全大写.
57.只针对异常的情况才使用异常.
异常的代码是比较慢的,jvm没有优化过.
java对于错误,最好返回异常,而不是像C语言一样的状态码.
58.对于可恢复的情况使用受检(checked)异常,对编程错误使用运行时(RuntimeException)异常.
特别是自己写sdk的时候.
59.避免不必要地使用受检的异常.
正确地使用API并不能组织这种异常条件的产生,并且一旦产生异常,使用API的程序员就可以立即采取有用的动作,那么使用受检异常就是有用的.不要刻意.
石蕊测试,简单而具有决定性的测试.
60.优先使用标准的异常.
IllegalArgumentException,参数不合适,非法参数.
IllegalStateException,状态不合适,非法状态.
NullPointerException ,null指针异常
UnsupportedOperationException,不支持的异常.
IndexOutOfBoundsException,数组越界异常.
61.抛出于抽象相对应的异常.
高层捕获底层异常,同时可以抛出按照高层抽象进行解释的异常,这叫做异常转译.
构成异常链.
62.每个方法抛出的异常都要有文档.
使用throws标记?
63.在细节消息中包含能捕获失败的消息.
异常的细节信息应该包含所有对该异常有贡献的参数和域的值.
在异常的构造器中产生细节消息,而不是消息string中产生消息.
64.努力使失败保持原子性.
一般而言,失败的方法调用应该使对象保持在被调用之前的状态.
设计一个不可变的对象.
可变对象上面,
1.检查参数有效性2.调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生.3. 编写一段回复代码.4.在拷贝上进行操作,在操作完成后再替换原来的对象,如Collections.sort方法.
并发比较难做.
65.不要忽略异常.
不要使用空的catch块.
只有FileInputStream的异常才可以忽略.
66.同步访问共享的可变数据
互斥(当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态.当这个对象被一个线程访问的时候,就被锁定了,其他线程来访问会一直等到另一个线程退出该对象)
通信(线程的变化可以被其他线程看到,保证进入同步方法或者代码块的每个线程,都看到由前一个锁保护的之前所有的修改效果)
java语言规范保证读写一个变量是原子(atomic)的,除非这个变量的类型为long或double,这个原子可以理解为互斥,但是并不保证通信的效果.
同步包含了互斥和通信.
提升(hoisting),活性失败(liveness failure)
写方法和读方法都必须同步.
volatile保证了通信,即不执行互斥访问,但是可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值.
安全性失败(safety failure)
同步,synchronization
可以使用AtomicLong来更精确表示可变的数据.
将可变数据限制在单个线程中.
当多个线程共享可变数据的时候,每个读或写数据的线程都必须执行同步.
67.避免过度同步
不要在同步块中又做同步块.
为了避免活性失败和安全性失败,在一个被同步的方法或代码块中,永远不要放弃对客户端的控制.
可能造成异常ConcurrentModificationException或者死锁.
在同步区域中调用外来方法容易造成死锁.
调用外来方法时,同步区域所保护的资源处于一致的状态.
java程序语言中的锁是可重入的(reentrant),这种调用不会死锁.???
可重入的锁,就是一个线程获取了一个锁在执行某个任务,执行任务的时候再次获取该锁,
是成功的,这就叫可重入的锁.
并发集合concurrent collection,CopyOnWriteArrayList,专门定制的并发.
应该在同步区域上做尽可能少的工作.
获得锁,检查共享数据,根据需要转换数据,然后放掉锁.如果应该设法执行某个很耗时的动作,
应该把这个动作移到同步区域的外面.
过度同步,丢掉了并行的机会.
如果一个可变的类要并发使用,应该使这个类变成线程安全的,通过内部同步,可以获得明显比外部锁定整个对象更高的并发性.否则就不要使用内部同步.让客户在必要的时候从外部同步.
java平台中,StringBuffer就是内部同步,但是其总是执行在单个线程中,所以一般最好用StringBuilder来代替,当然StringBuilder不是线程安全的.
如果在内部同步了类,可以使用不同的方法来实现高并发性.如分拆锁,分离锁,和非阻塞锁.
为了避免死锁和数据破坏,最好不要从同步区域内部调用外来方法,更为一般地讲,要尽量限制同步区域内部的工作量.
68.executor和task优先于线程
ExecutorService executor=Executors.newSingleThreadExecutor();
executor.execute(runnable);
关键的抽象不再是Thread了,以前它既充当工作单元,又是执行机制.
现在工作单元和执行机制是分开的.
限制关键的抽象是工作单元,称作任务(task).有两种,Runnable和Callable.
执行任务的通用机制是executor service.
选择适当的执行策略上获得了极大的灵活性.
ExecutorFramework框架做的工作就是执行,如同Collections Framework所做的工作就是聚集一样.
还有一个调度线程ScheduledThreadPoolExecutor
69.并发工具优先于wait和notify
正确地使用wait和notify比较困难,应该使用更高级的并发工具来代替.
concurrent中更高级的工具分成三类:ExecutorFramework\并发集合(Concurrent Collection)
同步器(Synchronizer).
优先在并发操作中使用并发集合.如ConcurrentHashMap等.Blocking Operation,阻塞操作等.
同步器是一些使线程能够等待另一个线程的对象,允许他们协调动作.
最常用的同步器是CountDownLatch和Semaphore.
倒计数锁存器(Countdown Latch)是一次性的障碍,含有一个int参数,配合countDown()和await()方法使用.线程饥饿死锁?对于间歇式的定时,应该优先使用System.nanoTime,它不会受到系统的实时时钟影响.
wait和notify就像用并发汇编语言进行编程一样.
70.线程安全性的文档化
线程安全性有多种级别.
不可变的(immutable),实例不可变
无条件的线程安全(),实例可变,但是有着足够的内部同步
有条件的线程安全,如Collections.synchronized包装返回的集合
非线程安全,实例可变,如果需要并发,那么需要自己选择外部同步
线程对立的,一般很少
私有锁对象?=子类和基类
synchronized修饰符是没有线程安全概念的.
71.慎用延迟初始化
延迟初始化是延迟到需要域的值时才将它初始化的这种行为.
大多数的域应该正常地进行初始化,而不是延迟初始化.
如果为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法.
对于实例域,就使用双重检查模式(单例?)或单重检查模式;对于静态域,就使用lazy initialization holder class模式.?
//lazy holder class s
private static class FieldHolder{
static final FieldType field=computeFieldValue();
}
static FieldType getField(){return FieldHolder.field;}
//lazy holder class e
//double check s
private volatile FieldType field;
FieldType getField(){
FieldType result=field;
if(resultnull){
synchronized(this){
result=field;
if(resultnull)
field=result=computFieldValue();
}
return result;
}
//double check e
72.不要依赖于线程调度器
thread scheduler是和操作系统相关的,不可一直.
线程优先级是java平台上最不可一直的特征了.
Thread.yield的唯一用途是在测试的时候人为地增加程序的并发性,后来不一定可行.
现在可以使用Thread.sleep来测试.
73.避免使用线程组
thread group,这是一个基本没用的东西,应该可以使用executor
74.谨慎地实现Serializable接口
序列化API提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象.
将一个对象编码成一个字节流,叫作对象序列化.
相反的处理过程称作反序列化.
序列化后,其编码可以从一台虚拟机传递到另一台虚拟机上面.
实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了"改变"这个类的实现的灵活性.因为可能不兼容.最好提供uid(serial version uid),不同的uid会导致兼容性受到破坏.
实现Serializable的第二个代价,它增加了bug的可能性,这是第二种构建对象的方式,如果是默认构造器,那么可能违背之前的关系.
实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了.
实现Serializable不是一个轻松就可以做出的决定.
为了继承而设计的类,应该尽可能少地实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口.
如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化.?
对于受防护的无参构造器,在调用任何方法的时候,进行初始化检查,这是一个很好的防范机制.
也是一个很好的线程安全状态机的通用实现.
内部类(innet class)不应该实现Serializable,其使用编译器产生的合成域来保存指向外围实例的引用.
静态成员类可以实现Serializable接口.
75.考虑使用自定义的序列化形式
对于序列化,如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式.即使确定了默认的序列化是合适的,通常还必须提供一个readObject方法以保证约束关系和安全性.
当一个对象的物理表示法于它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
它使这个类的导出API 束缚在该类的内部表示法上.
会消耗过多的空间.
会消耗过多的时间,如果不了解拓扑关系,必须要经过一个昂贵的图遍历过程.
transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉.
writeObjet(s.defaultWriteObject)
readObject(s.defaultReadObject)
在决定将一个域做成非transient的之前,一定要确信它的值将是该对象逻辑状态的一部分.
如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步.
不管选择了哪种序列化形式,都要为自己编写的每个可序列的类声明一个显示的序列版本UID
76.保护性地编写readObject方法
比较难?
77.对于实例控制,枚举类型优先于readResolve
比较难?
78.考虑用序列化代理代替序列化实例
类似Android的parcel?