一
创建和销毁对象
1
.考虑用静态工厂方法代替构造器
静态工厂方法的优势:
1
.有名称,可以见名知义了解获取对象的特点
2
.不必每次调用时都创建一个对象
3
.可以返回原类型的任何子类型对象
4
.创建参数化类型实例时,可以使代码更简洁(右边无需再写一遍)
5
.不可变对象可以进行缓存,以提升性能
2
.遇到多个构造器参数时要考虑用构建器
构建器优势:
1
.重叠构造器代码冗余
2
.JavaBean
模式阻止了将类做成不可变的可能
3
.builder
可以进行参数检查
4
.无论创建多少个对象,builder只需要一个,且可以在创建不同对象时进行调整,或者自动填充某些域 。
不足:创建对象前需要先创建builder
3
.用私有构造器或者枚举类型强化Singleton属性
不使用枚举时:
1
.构造器私有
2
.构造器抛出异常 if(INSTANCE!=null) throw
3
.序列化时必须声明所有实例域都是瞬时(transient
)的,并提供一个readResolve
方法返回现有的单例
4
.通过私有构造器强化不可实例化的能力
并在构造器中抛出异常
5
.避免创建不必要的对象
1
.如果对象是不可变的,将始终可以重用
2
.对于给定对象的适配器,不需要创建多个实例,例如:对于同一Map对象的keySet方法,每次返回的都是同一个对象
3
.优先使用基本类型,而不是装箱类型
4
.通过维护对象池来避免创建对象并不是好主意,除非对象非常昂贵(数据库连接池).
6
.消除过期的对象引用
内存泄露常见来源:
1
.无意识的对象保持(只要是类自己管理内存,程序员就应该警惕内存泄露问题)
2
.被遗忘在缓存中的对象引用(考虑用WeakHashMap
实现缓存)
3
.监听器和其他回调没有显式地取消注册
4种引用:
1
.强引用 Object obj = new Object();
: 只要强引用还存在,GC永远不会回收
2
.软引用 SoftReference
:用来描述一些有用但不必需的对象,在系统将要发生内存溢出之前,GC会对软引用对象进行回收,若回收后仍没有足够内存,才会抛出异常。
3
.弱引用 WeakReference
:也是用来描述非必需对象的,但是强度比软引用更弱,弱引用对象只能生存到下一次GC发生之前,当GC工作时,无论内存是否足够,都会回收弱引用对象
4
.虚引用 PhantomReference
:也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个实例。 为对象设置一个虚引用的唯一目的就是能在这个对象被GC回收时收到一个系统通知。
7
.避免使用终结方法
1
.终结方法不能保证会被及时执行,也不能保证会被执行,所以不应该依赖终结方法来更新重要的持久状态
2
.终结方法中发生的异常不会使线程终止,也不会打印栈轨迹,对象将处于破坏状态
3
.终结方法有非常严重的性能损失
4
.如果需要终止资源,应该显式地提供终止方法(io中的close方法),通常与try-finally结合使用,以确保及时终止
注意:
1
.除非作为安全网或者终止非关键资源,否则不要使用终结方法,最好就当没这个方法
2
.如果使用了终结方法,要记住super.finalize
3
.如果终结方法作为安全网,要记录终结方法的非法用法
4
.如果终结方法所属的类不是final的,请考虑使用终结方法守卫者
二
通用方法
8
.覆盖equals时请遵守通用约定
必须遵守自反性
、对称性
、传递性
、一致性
、非空性
可以用&&
连接所有对field的比较结果使代码简洁
诀窍:
1
.先使用==
操作符比较以优化性能
2
.使用instanceof
检查类型是否正确可以同时搞定null判断
3
.只比较关键域,可以通过关键域计算出来的冗余域不需要比较
4
.优先比较最可能不同、开销最低的域
5
.对float
,double
用compare
特殊处理,对数组
用Arrays.equals
处理
6
.覆盖equals时总要覆盖hashCode
7
.不要企图让equals太智能,不需要考虑参数可能存在的等价关系
8
.不要修改equals方法的参数类型(修改了就不是覆盖了),@Override可以避免这一点
9
.里氏替换原则
10
.优先用组合+公有视图的方式扩展值组件,而不是用继承
9
.覆盖equals时总要覆盖hashCode
1
.相同的对象必须具有相同的hashCode(在hash表中,hashCode用来定位hash bucket,如果相等的对象有不同的hashCode,那么hash表将从错误的hash bucket中寻找对象,最后会判定为不存在)
2
.用result = 31 * result + c
的方式获取hashCode,result初值设为17,c为每个域的散列值
3
.能够通过关键域计算出来的冗余域可以排除
4
.如果计算hashCode开销很大,可以对hashCode进行缓存,并在hashCode方法第一次被访问时才进行计算
5
.不要试图排除关键域(equals用到的所有域)来提高性能,这样虽然会提高hashCode计算速度,但是会提高hashCode重复率,反而降低散列性能。
10
.始终要覆盖toString
1
.优点:易于阅读,易于调试,易与其他应用交换数据
2
.提供一个静态工厂用于将toString返回值转换为本身的类型
3
.在文档中明确表示是否规定了toString的格式,如果规定了格式就要严格遵守
4
.对toString返回值中包含的关键内容提供访问方法
11
.谨慎地覆盖clone
1
.实现Cloneable接口(空接口)需要提供一个public clone
(Object中声明为proteced
)方法
2
.从jdk1.5开始,引入了协变返回类型
,clone方法被覆盖后可以返回Object的子类型(通则:永远不要让客户去做任何类库能完成的事情)
3
.clone方法就是另一个构造器;必须确保它不会伤害到原始对象,并确保正确地创建被克隆对象中的约束条件。
4
.clone架构与引用可变对象的final
域的正常用法是不相兼容的,为了使类可clone,有时需要去掉一些final
5
.使用深拷贝
,递归地调用每个域中包含的域,一直深入到不可变类为止
6
.拷贝”长”对象(数组,链表等)时,如果使用递归会创建很多指针消耗栈空间,可以考虑换成迭代
7
.另一种方法:先调用super.clone(),将所有域设为0值,然后为其赋值新对象。这种方法简单合理,但是没有”直接操作对象及其域中对象的clone方法“快。
8
.clone过程中不应该调用任何非final方法(如果是private方法则不需要final),因为子类有机会改变该方法的行为。
9
.重写的public clone
方法应该忽略CloneNotSupportedException
,但如果
该类是用来被继承的,则应该模拟Object.clone:声明为protected,抛出异常,不实现Cloneable。
10
.对于不可变类,提供clone没有太大意义
11
.用拷贝构造器
(即参数类型为类本身的构造器)或者拷贝工厂
代替clone,可以增加一个参数实现克隆对象的转型(例如接受一个Collection参数与一个Class参数)
12
.考虑实现Comparable接口
1
.可以方便地使用各种泛型算法与Comparable的集合实现
2
.强烈建议(x.compareTo(y) == 0) == (x.equals(y))
,但非必须,若违反了这个条件,应该予以说明。
3
.不满足上一条约定的对象放入有序集合
(使用compareTo,如TreeSet)中后,将无法遵守集合接口通用(使用equals)约定(如BigDecimal,浮点数)
4
.优先用组合+公有视图
的方式扩展值组件,而不是用继承(同equals)
5
.Comparable只规定了符号,没有规定数值大小,可以直接返回两个数值相减的结果,但是要小心溢出
三
类和接口
13
.使类和成员的可访问性最小化
1
.实例域决不能是公有的
2
.静态公共域只能为final修饰的基本类型或者final修饰的不可变类
3
.用Collections.unmodifiableList(arr)
将数组变成不可变列表后再提供访问;或者在访问方法中返回数组的clone()
14
.在公有类中使用访问方法而非公有域
暴露域将失去内部表示法的灵活性,也将失去对数据访问进行限制的能力
15
.使可变性最小化
1
.创建不可变类的5条规则
(1)
.不要提供任何会修改对象状态的方法
(2)
.保证类不会被扩展(final或者构造器私有)
(3)
.使所有域都是final的
(4)
.使所有域成为私有的
(5)
.确保对于任何可变组件的互斥访问
2
.用公有的静态域或静态工厂为不可变类提供缓存
3
不可变对象本质上是线程安全的,它们不要求同步,可以被自由地共享,永远不需要保护性拷贝,不需要拷贝构造器
4
.不可变类可以共享内部信息
5
.不可变对象为其他对象提供了大量的构件
6
.不可变类的缺点:每一个不同的值都需要一个单独的对象,存在性能问题,可以提供1个配套类来进行过渡操作(如StringBuilder)
7
.因历史问题没有设置成final的不可变类(如BigInteger
),在使用时需要检查对象是否被扩展了,如果被扩展了就可能需要保护性拷贝
8
.如果实现Serializable
,必须显式地提供readObject
和readResolve
方法或者使用ObjectOutputStream.writeUnshared
和ObjectInputStream.readUnshared
方法
9
.除非有必须的理由让类可变,否则类应该是不可变的;如果类无法做成不可变的,也应该尽力限制其可变性。因此域也应该是final的。
16
.复合优先于继承
1
.继承破坏了封装性,子类依赖于父类的实现细节,若父类内部发生变动,将可能损坏子类。
2
.假设子类覆盖父类所有的方法,增加了对参数的类型检查;后来父类增加了新方法,而子类没有同步更新,非法的参数将有可能通过类型检查,这将会产生安全漏洞
3
.假设子类增加了新方法,后来父类也新增了1个签名相同但返回类型不同的方法,编译将不通过;如果方法签名与返回类型都相同,将形成覆盖,又回到了上一种情况
4
.复合/转发
的类称为组件,并实现相同接口的类为包装类
,每个接口只需要1个转发类
5
.包装类不适用于回调框架,回调函数传递是1个指向自身的引用,避开了外面的包装类
6
.继承会将api中所有缺陷传播到子类中去,而复合却可以设计新的api隐藏缺陷
7
.只有真正存在从属关系时,才应该使用继承
17
.要么为继承而设计,并提供文档说明,要么就禁止继承
1
.决不能在构造器中调用可以被覆盖的方法,否则这样的方法将会在子类构造器之前运行,若是该方法中存在对域的访问,将会造成空指针异常
2
.类必须提供适当的hook,以便能够进入子类的内部工作流程
3
.如果实现了Cloneable或Serializable接口,那么clone
与readObject
都不能调用可被覆盖的方法
4
.如果实现了Serializable接口,必须使readObject与writeReplace成为受保护的方法,否则它们将会被子类忽略掉
5
.如果必须让类可被继承,务必保证这个类永远不会调用可覆盖方法,可以机械地消除可覆盖方法的自用性
:将每个可覆盖方法的方法体移到一个私有的“辅助方法
中”,然后让可覆盖方法调用辅助方法实现功能,这样可以确保可覆盖方法永远不会干涉到父类的运行。
18
.接口优于抽象类
1
.现有的类易被更新,以实现新的接口
2
.接口是定义mixin(混合类型)理想选择
3
.接口允许我们构造非层次接口的类型框架
4
.在包装类模式下,接口使得安全地增强类的功能成为可能
5
.通过对导出的每个接口都提供一个骨架实现
(skeletal implementation)类,把接口和抽象类的优点结合起来
6
.实现了接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类,这种方法被称为“多重模拟继承
”。
7
.抽象类比接口更易于扩展,抽象类可以直接增加方法,接口却不可以。
8
.接口一旦被公开发行,并且已被广泛实现,再想改变接口是不可能的。
9
.接口通常是定义允许多个实现的类型的最佳途径,但是当演变的容易性比灵活性和功能性更重要时例外。
19
.接口只用于定义类型
1
.常量接口模式是对接口的不良使用
2
.如果这些常量最好被看作枚举类型的成员,就应该是枚举类型
,否则应该使用不可实例化的工具类来导出这些常量。
3
.如果大量利用工具类来导出常量,可以通过利用静态导入
机制,避免用类名来修饰常量名。(1.5版本后)
20
.类层次优于标签类
1
.标签类过于冗长,容易出错,并且效率低下
2
.编译器确保每个类的构造器都初始化它的数据域,对于根类中声明的每个抽象方法,都确保有一个实现。
3
.类层次的另一种好处在于,它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查。
21
.用函数对象表示策略
1
.如果一个类仅仅导出一个方法,它的实例实际上就等同于一个指向该方法的指针。
2
.设计具体的策略类时,还需要定义一个策略接口
3
.如果它被重复执行,考虑将函数对象存储到一个私有的静态final域里,并重用它。
4
.宿主类可以导出公有的静态域(或静态工厂方法),其类型为策略接口,具体的策略类可以是宿主类的私有嵌套类。
22
.优先考虑静态成员类
1
.嵌套类存在的目的应该只是为它的外围类提供服务。
2
.嵌套类有四种:静态成员类
,非静态成员类
,匿名类
,局部类
3
.非静态成员类每个实例都隐含着与外围类的一个实例相关联。
4
.如果嵌套类的实例可以在它的外围类的实例之外独立存在,这个嵌套类就必须是静态成员类;在没有外围实例的情况下,想创建非静态成员类的实例是不可能的。
5
.非静态成员类的一种常见用法是定义一个Adapter
,它允许外部类的实例被看作是另一个不相关的类的实例。(如集合视图 keySet iterator)
6
.如果声明成员类不要求访问外围实例,那成员类就应该是静态的。非静态成员类的实例必须要有一个外围的实例
7
.私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件。
8
.匿名类的常见用法是①动态地创建函数对象(第21条),②创建对象过程(Runabel,Thead,TimerTask),③在静态工厂方法的内部
9
.在任何可以声明局部变量的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。
四
泛型
23
.不要在新代码中使用原生态类型
1
.错误应该尽早发现,最好在编译期就发现。
2
.如果使用原生类型,就失去了泛型在安全性和表述性方面的所有优势
3
.在不在乎集合中的元素类型时,使用无限制的通配符类型(<?>
)
4
.在类文字中必须使用原生态类型;因为泛型会被擦除,所以instanceof
操作符使用原生类型就可以了。
24
.消除非受检警告
1
.泛型相关的编译警告:
非受检强制转化警告(unchecked cast warnnings)
非受检方法调用警告
非受检普通数组创建警告
非受检转换警告(unchecked conversion warnings)
2
.要尽可能地消除每一个非受检警告
3
.如果无法消除警告,可以同时证明引起警告的代码是类型安全的,只有在这种情况下才可以用一个@SuppressWarnings("unchecked")
注解来禁止这条警告。
4
.SuppressWarnings注解可以用在任何粒度的级别中,应该始终在尽可能小的范围中使用它,永远不要在整个类中使用它
5
.如果在长度不止一行(即一行代码中存在多个动作)的代码中使用了SuppressWarnings注解,可以将它移到一个局部变量声明中,以减小SuppressWarnings注解的影响范围。
6
.每当使用@SuppressWarnings("unchecked")
时,都要添加一条注释,说明为什么这么做是安全的。
25
.列表优先于数组
1
.数组与泛型的区别
数组是
协变
的,如果sub
为super
的子类型,那么sub[]
也是super[]
的子类型;相反,泛型则是不可变
的,对于两个类型Type1
与Type2
,List<Type1>
与List<Type2>
没有任何关系。数组是
具体化
的,因此数组在运行时才知道并检查它的元素类型约束;而泛型则是通过擦除(erasure)来实现的,泛型只在编译时强化类型信息,并在运行时擦除元素类型信息,擦除可以使泛型与早期无泛型的代码兼容。
2
.泛型与数组不能混合使用,因为泛型数组不是类型安全的。(因为数组是协变的,所以数组可以自动向上转型,我们可以往向上转型后的数组中插入与转型前不同的类型的数据,而借转型前数组名义取出的对象将会产生CalssCastException)
List<String>[] stringLists = new List<String>[];
List<Integer> intList = Arrays.asList(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
3
.每当调用可变参数方法时,就会创建一个数组来存放参数,如果这个数组的元素类型不是可具体化的,就会得到一条警告,除了禁止这个警告,别无他法。
4
.数组提供了运行时的类型安全,但是没有提供编译时的类型安全;泛型提供了编译时的类型安全,但是没有提供运行时的类型安全。
5
.不可具体化的类型数组转换只能在特殊情况下使用。
26
.优先考虑泛型
1
.禁止数组类型的未受检转换比禁止标量类型的更加危险
2
.使用泛型比使用需要在客户端代码中进行转换的类型更加安全,也更加容易,在设计新类型的时候,要确保不需要这种转换就能使用。
27
.优先考虑泛型方法
1
.如果泛型方法返回的对象是无状态的,可以考虑将该对象做成泛型单例
2
.调用泛型方法时,编译器会对返回值进行类型推到,以适应接收者的类型
28
.利用有限通配符来提升API的灵活性
1
.泛型中使用extends
时,每个类型都是自身的子类型;使用super
时,每个类型都是自身的超类型。
2
.为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。PECS(producer-extends;consumer-super)
3
.不要用通配符类型作为返回类型
4
.如果编译器不能推断你希望它拥有的类型,可以通过显式的类型参数来指定。
5
.如果类型参数在方法中声明中只出现一次,可以用通配符取代它。
6
.不能把null之外的任何值放进List<?>中
29
.优先考虑类型安全的异构容器
1
.cast
方法是java的cast操作符的动态模拟,它只检验它的参数是否为Class对象所表示的类型的实例,如果是,就返回参数,否则抛出ClassCastException
2
.类型安全的异构容器的缺陷
恶意的客户端可以很轻松的破坏类型安全,只要它以原生态类型使用Class对象,在容器的添加方法第一行加入参数的类型检查(
type.cast(obj)
)可以避免这种问题。不可存放不可具体化的类型数据(如
List<String>
,因为List<String>
的类型为List.class,而List<String>.class
是语法错误),只能保存String或String[]等可具体化的对象。
3
.利用有限制类型参数或者有限制通配符来限制可以表示的类型
4
.asSubclass(List.class)
方法将Class类的类型令牌安全且动态地转换成参数所表示的类(List)的一个子类的通配符表示法(? extends List),参数必须为调用者的父类型,如果成功,返回它的参数,否则抛出ClassCastExcpetion
五
枚举和注解
30
.用enum代替int常量
1
.因为没有可以访问的构造器,枚举类型是真正的final
。
2
.枚举类型允许添加任意的方法和域,并实现任意的接口。枚举提供了所有Object方法的高级实现,实现了Comparable和Serializable接口,并针对枚举类型的可任意改变性设计了序列化方式。
3
.枚举有一个静态的values
方法,按照声明顺序返回它的值数组,toString
方法返回每个枚举值的声明名称。
4
.在同一个枚举类型中,将特定的行为与每个具体的枚举常量关联起来:在枚举类型中声明一个抽象的方法,并在特定于常量的类主体中,覆盖该方法。
5
.每一个枚举常量可以关联特定的数据(在构造器中绑定),因此特定于枚举常量的方法可以与特定于枚举常量的数据结合起来。
6
.枚举类型有一个自动产生的valueOf(string)方法,它将常量的名字转变成常量本身。如果枚举类型中覆盖toString,要考虑编写一个fromString
方法,将定制的字符串表示法变回相应的枚举。
7
.如果多个枚举常量同时共享多个行为,要考虑策略枚举
。
31
.用实例域代替序数
1
.所有枚举都有一个ordinal
方法,它返回每个枚举常量在类型中的数字位置
2
.永远不要根据枚举的序数导出与他关联的值,而是要将值保存在一个实例域中。
32
.用EnumSet代替位域
1
.or
位运算将几个常量合并到一个集合中,称为位域
。(形如111→rwx)
2
.EnumSet可以表示从单个枚举类型中提取的多个值的集合,EnumSet.of
3
.截止jdk1.6,无法创建不可变的EnumSet
33
.用EnumMap代替序数索引
1
.EnumMap
构造器采用键类型的Class对象:这是一个有限制的类型令牌,它提供了运行时的泛型信息。实例化需要指定Class类型,可以保存同一个类型的对象与值的映射关系
2
.最好不要用序数来索引数组,而要用EnumMap,如果需要表示的关系是多维的,就用嵌套的EnumMap
34
.用接口模拟可伸缩的枚举
1
.用接口模拟可伸缩的枚举有个小小的不足,即无法实现从一个枚举类型继承到另一个枚举类型。
2
.虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,模拟可伸缩的枚举。<T extends Enum<T> & interfaceType>
35
.注解优先于命名模式
1
.注解的声明类型就是自身通过Retention
和Target
注解进行注解,@Retention(RetentionPolicy.RUNTIME)
元注解表明,应该在运行时保留;@Target(ElementType.METHOD)
元注解表明,只有在方法声明中才是合法的。
2
.InvocationTargetException
封装了目标方法在运行时产生的异常,出现该异常表明目标方法确实被运行了,使用getCause()
可以获得方法运行时产生的具体异常。
3
.用花括号包裹用逗号分隔的参数将自动变成参数数组
4
.如果编写的工具需要在源文件上添加信息,就要定义一组适当的注解类型。
36
.坚持使用Override注解
1
.Override
会对标记的方法进行代码检查,确保该方法成功地被重写了。
2
.现代IDE在检测到Override注解时会提醒用户小心无意识的覆盖。
3
.在抽象类或接口中使用Override可以确保不会添加新的方法。
37
.用标记接口定义类型
1
.标记接口是没有包含方法声明的接口,只是指明了实现类应该具有的属性。
2
.标记接口有两点胜过注解
标记接口可以在编译时进行类型检查,保证参数的有效性
标记接口类型更容易指定实现类的约束条件,如Set就是一个标记接口,他限制了实现类必须具有Collection特性。
3
.注解有两点胜过标记接口
注解可以在内部丰富信息而不影响客户端使用
标记注解在支持注解的框架中具有一致性
4
.如果标记需要应用到任何程序元素而不仅仅是类或接口上,就应该使用注解;否则应该使用标记接口;如果该标记只用于特定的接口,最好将标记定义成该接口的子接口(Set与Collection)
5
.如果一个标记永远不会扩展,应该定义成标记接口;如果未来可能需要在标记上添加更多信息,则应该定义成标记注解。
六
方法
38
.检查参数的有效性
1
.应该在发生错误后,能够尽快检测出错误,不要让错误在过深的地方爆发出来。
2
.对于公有的方法,要用@throws
标签在文档中说明违反参数值限制时会抛出的异常。
3
.非公有的方法通常应该使用断言来检查参数, -ea
启用
4
.如果有效性检查非常昂贵,或者已经隐含在计算中完成则无需进行检查
5
.使用异常转译
技术,将计算过程中抛出的异常转换为正确的异常。
39
.必要时进行保护性拷贝
1
.对构造器的每个可变参数进行保护性拷贝
2
.保护性拷贝是在检查参数的有效性之前
进行的,并且有效性检查是针对拷贝之后
的对象,而不是针对原始对象,这样可以避免在拷贝阶段从另一个线程改变类的参数。
3
.对于参数类型可以被不信任方子类化的参数,不要使用clone
。
4
.如果需要访问类的内部域,返回内部域的保护性拷贝。
5
.如果不能容忍客户端传入的对象可变,使用保护性拷贝
6
.长度非0的数组总是可变的,使用保护性拷贝或者返回数组的不可变视图
7
.通常使用Date.getTime()返回的long类型作为内部的时间表示法。
8
.包装类一般客户端自己使用,无需使用保护性拷贝。
40
.谨慎设计方法签名
1
.谨慎地选择方法的名称
2
.不要过于追求提供便利的方法,只用某项操作被经常用到时,才提供快捷方式。
3
.避免过长的参数列表,相同类型的长参数序列格外有害。
缩短参数列表的三种方法
把方法分解成多个方法,但是会导致方法过多,但是提升方法的正交性,可以减少方法数目。
创建辅助类,用来保存参数的分组。
从对象构建到方法调用都采用
Builder
模式
4
.对于参数类型,优先使用接口而不是类。
5
.对于boolean参数,要优先使用连个元素的枚举类型。
41
.慎用重载
1
.要调用哪个重载方法是在编译
时做出决定的。
2
.对于重载方法的选择是静态
的,而对于被覆盖的方法的选择是动态
的。
3
.如果普通用户根本不知道”对于一组给定的参数,其中哪个重载方法将会调用”,那么这样的API很可能导致错误。
4
.安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法;如果方法使用可变参数,保守的策略是根本不重载它。
5
.一个类的多个构造器总是重载的,很多情况下可以用静态工厂代替构造器。
6
.对于一对重载方法,如果至少有一个对应的参数在两个方法中具有”根本不同(无法强转)”的类型,那就不会让用户感到混淆。
7
.将list.remove()
参数转换成Integer
,迫使选择正确的重载方法。java语言添加了自动装箱后,破坏了List接口。
8
.当传递的参数没有本质的不同时,必须确保所有的重载方法行为一致,做法是让更具体化的重载方法把调用转发给更一般化的重载方法。
42
.慎用可变参数
1
.参数数量检测无法在编译期完成
2
.可变参数方法会将多个参数收集成数组,但若参数本身就是数组,那么收集后的数组将无法得到预期的结果(Arrays.asList
)
3
.不必改造具有数组参数的每个方法;只当确实是在数量不定的值上执行调用时才使用可变参数
4
.重视性能时要小心,可变参数方法每次调用都会进行数组分配和初始化
5
.提供大多数使用情况下的不可变参数方法,再额外附加可变参数方法(EnumSet.of
)
43
.返回零长度的数组或者集合,而不是null
1
.如果返回null,每次调用该方法时都会需要进行空值判断
2
.长度为0的数组是不可变的,因此返回的空数组可以自由共享
(Collections.emptySet
;
Collections.emptyList
;
Collections.emptyMap
)
44
.为所有导出的API元素编写文档注释
1
.文档注释应该简洁地描述出它和客户端之间的约定
2
.应该列举出这个方法的所有前提条件,后置条件和副作用
3
.{@code}
会造成包裹的内容以代码片段形式呈现(即生成code标签)
4
.{@literal}
会自动转义包裹的内容
5
.同一个类或者接口中的两个成员或者构造器,不应该具有相同的概要描述
6
.不要忽视类是否是线程安全的,以及类是否是可序列化的。
7
.{@inheritDoc}
继承注释
七
通用程序设计
45
.将局部变量的作用域最小化
1
.最有力的方法就是在第一次使用它的地方声明
2
.几乎每个局部变量都应该包含一个初始化表达式,如果没有足够的信息来进行有意义的初始化,就推迟这个声明,try-catch除外。
3
.循环中提供了特殊的机会来将变量的作用域最小化,如果在循环终止后不再需要循环变量的内容,for
循环就优先于while
循环。
46
.for-each 循环优先于传统for循环
1
.嵌套的for-each循环,外层循环的对象可以在内层对象中保持,不会随内层循环移动指针。
2
.有3种情况无法使用for-each
需要遍历并删除元素时
需要遍历元素并取代某些值时
需要并行迭代多个集合时
47
.了解和使用类库
java.lang
;
java.util
;
java.io
;
java.util.concurrent
;
48
.如果需要精确的答案,请避免使用float和double
使用BigDecimal
,int
或者 long
进行货币计算
49
.基本类型优先于装箱基本类型
1
.基本类型与装箱基本类型的区别
基本类型只有值,而装箱基本类型的同一性不仅取决于值
装箱基本类型除了值还多了一个null
基本类型更节省空间
2
.对装箱基本类型使用==操作符几乎总是错误的。
3
.当一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型会自动拆箱。若被拆的对象为null,将抛出异常。
4
.在集合中只能使用装箱基本类型
50
.如果其他类型更适合,则尽量避免使用字符串
1
.字符串不适合代替其他的值类型
2
.字符串不适合代替枚举类型
3
.字符串不适合代替聚集类型(即将多个部分拼接成字符串用关键字分隔),最好用一个简单的类来代替。
4
.字符串不可替代控制共享资源访问权限的键
51
.当心字符串连接的性能
1
.为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间
2
.为了性能,用StringBuilder
替代String
52
.通过接口引用对象
1
.如果有合适的接口类型存在,那么对于参数,返回值,变量和域来说,就都应该使用接口类型进行声明
2
.如果对象属于基于类的框架,就应该使用基类引用这个对象,尽量用抽象类型引用对象
3
.如果程序依赖于类中额外的方法,那么这种类应该只被用来引用实例,而不应该作为参数类型。
53
.接口优先于反射机制
反射功能应该只有在设计时被用到,或者只是用来创建对象,程序在运行时不应该以反射方式访问对象
54
.谨慎地使用本地方法
1
.本地方法3个用途
提供访问特定于平台的机制的能力,如注册表和文件锁
提供了访问遗留代码库的能力
通过本地语言,编写程序中注重性能的部分
2
.使用本地方法不值得提倡,java.util.prefs
提供了访问注册表的能力,java.awt.SystemTray
提供了访问桌面系统托盘的能力
3
.本地语言是不安全
的,无法免受内存毁坏错误的影响。
55
.谨慎地进行优化
1
.好的程序体现了信息隐藏的原则,只要有可能,就要把设计决策(暂时理解为业务逻辑)集中在单个模块中,因此,可以改变单个决策,而不会影响到系统的其他部分。
2
.努力避免那些限制性能的设计决策,系统中最难更改的是指定模块交互关系的组件,API,线路层协议和永久数据格式
3
.要考虑API设计设计决策的性能后果
如果将公类类型成为可变的,就可能导致大量不必要的保护性拷贝
在适合使用复合模式的公有类中使用继承,会把这个类与它的超类永远束缚在一起
在API中使用实现类而不是接口,会将客户端束缚在具体的实现类上,如果将来出现更快的实现也无法使用。
56
.遵守普遍接受的命名习惯
1
.《The Java Language Specification
》
2
.对于缩写单词,也应该遵守首字母大写原则
3
.常量域是唯一推荐使用下划线的情形
4
.类型参数,T
任意类型,E
集合元素类型,KV
键值映射类型,X
异常,任何类型的序列可以是T,U,V或者T1,T2,t3
5
.转换对象类型,返回不同类型的独立对象的方法 toType
;
返回视图asType
;
返回一个与被调用对象相同值的基本类型的方法typeValue
;
静态工厂valueOf
,of
,getInstance
,
newInstance
,getType
,NewType
八
异常
57
.只针对不正常情况使用异常
1
.异常机制的设计初衷是为了不正常的情形,很少有JVM实现会对它进行优化;把代码放在try-catch中反而阻止了本来可以进行的优化;对数组进行遍历的标准模式并会不导致冗余的检查,有些现代的JVM实现会将它们优化掉。
2
.异常只应用于不正常情况,不应该用于正常的控制流。
3
.如果类具有状态,有两种方法可以避免异常,第一种是状态测试法
,如Iteable接口的hasNext。
4
.另一种方法是可识别返回值法
,并发环境下或状态检查影响性能时更适用,如果所有情况等同,优先使用状态检测法。
58
.对可恢复的情况使用受检异常,对编程错误使用运行时异常
1
.三种可抛出结构:受检异常
,运行时异常
,错误
2
.如果期望调用者能够适当地恢复,使用受检的异常
3
.用运行时异常表明编程错误,大多数运行时异常表示提前违例,即API用户没有遵守约定
4
.错误通常被JVM保留,用于表示资源不足,约束失败等,最好不要再实现任何新的Error子类
5
.如果不清楚是否有可能恢复,最好使用未受检的异常
6
.受检的异常往往需要表明可回复条件,提供一些便于查询信息的方法尤其重要
59
.避免不必要地使用受检的异常
1
.如果正确使用API不能阻止异常的产生,而一旦产生异常API使用者能够立即采取有效的动作,就值得使用受检的异常,除非两个条件都成立,否则更适合使用未受检异常。
2
.把受检的异常变成未受检异常的方法是把抛出异常的方法分成两个方法,其中一个方法返回boolean表示是否应该抛出异常(类似状态检测法)。这种方法在并发环境下或者异常检测很耗性能时则不适用。
60
.优先使用标准的异常
ConcurrentModificationException
;
UnsupportedOperationException
61
.抛出与抽象相对应的异常
1
.更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法称为异常转译
2
.一种特殊的异常转译形式称为异常链
,如果低层异常对于调试高层异常有帮助,就适合使用异常链,高层异常提供访问方法获得低层异常(Throwable.getCause
)
3
.大多数标准异常都有异常链构造器,没有异常链构造器的,可以使用Throwable的initCause
方法设置原因。
4
.异常转译不应滥用,更好的做法是及时进行参数检查,尽早中断异常链。
62
.每个方法抛出的异常都要有文档
1
.始终要单独声明受检的异常,而不是抛出多个异常的共同的超类,并且利用@throws
标记,准确记录下抛出每个异常的条件
2
.不要在方法声明中抛出任何RuntimException
3
.如果一个类中的多个方法出于同样的原因抛出同样的异常,可以将异常的信息写入到类的文档注释中。
63
.在细节消息中包含能捕捉失败的信息
1
.如果捕获失败,异常的细节信息应该包含所有对异常有贡献的参数和域的值
2
.在异常的构造器中引入细节信息
64
.努力使失败保持原子性
1
.失败的方法调用应该使对象保持在被调用之前的状态
2
.保持失败原子性的方法:
设计不可变类
检查参数有效性,提前失败,将所有可能产生失败的运算放到改变状态的操作之前
编写恢复代码
在对象的一份临时拷贝上操作,然后用拷贝中的结果代替对象的内容(如
Collections.sort
)
3
.在缺乏同步机制的情况下并发访问对象,难以保持失败原子性。
65
.不要忽略异常
有一种情形可以忽略异常,即关闭FileInputStream时,因为文件状态没有改变,不必终止当前操作,即使这种情况下,也应该将异常信息记录下来,以便排查。
九
并发
66
.同步访问共享的可变数据
1
.同步可以保证每个线程看到的对象处于一致 的状态中;保证每个线程都看到锁对象被修改的结果。
2
.java语言规范保证,读或者写一个变量是原子的,除非这个变量的类型为long
或double
3
.读写原子数据的时候也应该使用同步,不仅为了互斥访问
,也为了在线程间进行可靠的通信
。详见java内存模型。
4
.不要用Thead.sotp
。要中断一个线程,建议做法是轮询boolean域,通过另一个线程修改这个boolean域;具体方法是添加对这个boolean域的读写方法,并对读写方法添加同步
。
5
.volatile
修饰符虽然不执行互斥访问,但是可以保证每个线程在读取该域的时候都能看到最近刚被写入的值。
6
.使用AtomicLong
进行原子化long操作
7
.最佳做法是要么不共享可变数据,要么就共享不可变数据,就是将可变数据限制在单个线程内部。
8
.活性失败:线程永远无法读取到变量的变化
9
.安全性失败:一个线程修改非原子变量时,另一个线程过来读取,此时只能读取到过渡状态的数据。
67
.避免过度同步
1
.为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。尽量不要在同步代码块中调用可被覆盖的方法或者客户端提供的方法(原则上永远不要信任客户端)
2
.当客户端造成服务端变化后能够预定通知,即观察者(Observer
)模式
3
.线程B需要获得锁才能继续执行,但是线程B执行完当前的代码片段后,线程A才能让出锁,此时将会造成死锁
。死锁的根本原因是,多个线程同时需求同步锁。
4
.java中的锁是可重入
的,这样的锁简化了多线程面向对象的构造
5
.不要把锁对象传递给客户端,而是传递锁对象的快照,这样可以避免死锁
6
.java类库提供了并发集合CopyOnWriteArrayList
7
.同步区域内的工作越少越好
8
.如果一个可变的类需要并发访问,应该使这个类变成线程安全的,通过内部同步可以实现;如果不确定是否需要同步,那就不要同步,并建立文档,注明它不是线程安全的
9
.如果在内部同步了类,就可以进行分拆锁
,分离锁
和非阻塞
并发控制
10
.如果方法修改了静态域,那么就必须同步对这个域的访问,即使它往往只作用于单个线程,这是为了可靠的线程通信
68
.executor和task优先于线程
1.Executor Framework
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(runnable);
executor.shutdown();
invokeAny
invokeAll
awaitTermination
ExecutorCompletionService
2
.如果编写的是小程序,或者轻载服务器,使用Executors.newCachedThreadPool
3
.在大负载的产品服务器中,使用Executors.newFixedThreadPool
4
.尽量不要编写自己的工作队列,尽量不直接使用线程。现在的关键抽象不再是Thread了,而是task。task有两种:Runnable与Callable,执行任务的关键机制是ExecutorService。
5
.ScheduledThreadPool代替Timer
6
.《Java Concurrency in Practice
》
69
.并发工具优先于wait和notify
1
.concurrent包中三类工具:
Executor Framework
并发集合
同步器
2
.并发集合中不可能排除并发活动,将他锁定没什么作用,只会使程序速度变慢。
3
.应该优先使用ConcurrentHashMap
,而不是Collections.synchronizedMap
或者Hashtable
4
.阻塞队列用于工作队列或生产者-消费者队列,任意个生产者线程向工作队列中添加项目,并且当工作项目可用时,任意个消费者线程从工作队列中取出项目
5
.同步器是一些使线程能够等待另一个线程的对象(互斥),最常用的同步器是CountDownLatch
和Semaphore
,较不常用的是CyclicBarrier和Exchanger
6
.倒计数锁存器,当countDown数量足够时,将自动从await
状态唤醒,否则将继续等待。
7
.线程饥饿死锁
8
.对于间歇式的定时,始终应该优先使用System.nanoTime
,而不是System.currentTimeMills
9
.a.wait()
,使所有访问a对象的线程等待,该方法必须在同步区域内被调用;始终应该使用wait循环模式
调用wati方法,永远不要在循环之外调用wait方法。
10
.优先使用notifyAll
,因为被意外唤醒的线程会在循环中检查条件,如果条件不满足,会继续等待。
70
.线程安全性的文档化
1
.在一个方法声明中出现synchronized修饰符,这个是实现细节,不是导出API的一部分
2
.一个类为了可以被多个线程安全地使用,必须在文档中清楚说明它所支持的线程安全级别:
不可变的
无条件的线程安全:可变类,但是有足够的内部同步,可以被并发使用
有条件的线程安全:某些方法需要外部同步
非线程安全:必须外部同步
线程对立:即使所有调用都有外部同步,依旧不能安全地并发使用。通常因为没有同步地修改静态数据
3
.必须指明哪个调用序列需要外部同步,还要指明为了调用这个序列,必须获得哪一把锁。如果一个对象代表了另一个对象的一个视图,客户端通常就必须在后台对象上同步,以防止其他线程直接修改后台对象。
4
.除非从返回类型来看已非常明显,否则静态工厂必须在文档中说明被返回对象的线程安全性
5
.私有锁对象必须声明为final的,只能用在无条件线程安全类上,有条件线程安全类不能使用,因为外部调用时无法获得锁。
71
.慎用延迟初始化
1
.对静态域使用延迟初始化,用 lazy initialization holder class
模式(静态私有内部类)
2
.对实例域使用延迟初始化,用双重检查模式,第二次检查在同步中进行
3
.如果允许重复初始化,用单重检查模式
72
.不要依赖于线程调度器
1
.任何依赖于线程调度器来达到正确性或者性能要求的程序,很可能不可移植
2
.最好的办法是确保可运行的线程平均数量不明显多于处理器数量
3
.如果线程没有在做有意义的工作,就不应该运行
4
.不要企图通过Thread.yield
来修正程序,它唯一的作用是在测试期间人为增加程序的并发性
5
.线程优先级是Java平台上最不可移植的特征
73
.避免使用线程组
十
序列化
74
.谨慎地实现Serializable接口
1
.实现Serializable接口的代价是,
-
一旦一个类被发布,就大大降低了改变这个类实现的可能性
-
增加了出现BUG和安全漏洞的可能性,反序列化是语言之外的对象创建机制
-
随着类发行新的版本,相关的测试负担也增加了
2
.如果没有声明serialVersionUID
,系统将在运行时通过类名,接口,成员等自动产生
3
.为了继承而设计的类,应该尽少实现Serializable接口,用户接口也应该尽少继承Serializable接口
4
.如果类有一些约束条件,当类的实例域被初始化为默认值,就会违背这些约束条件,此时需要添加readObjectNoData
方法,并在其中抛出InvalidObjectException
5
.对于为继承而设计的可序列化的类,应该提供一个无参构造器,以及一个初始化方法,readObject与有参构造器共用这个初始化方法
6
.AtomicReference
操作原子引用,这是一个很好的线程安全状态机
7
.内部类不应该实现Serializable,内部类的默认序列化形式是定义不清楚的,然而静态成员类却可以实现Serializable
75
.考虑使用自定义的序列化形式
1
.如果没有先认真考虑默认的序列化形式是否合适,则不要贸然接受;理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的
2
.如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式
3
.即使确认了默认的序列化形式是合适的,通常还必须提供一个readObject
方法以保证约束关系和安全性
4
.当一个对象的物理表示法与逻辑内容有实质性的区别时,使用默认的序列化形式会有以下4个缺点
-
这个类的导出API永远束缚在该类的内部表示法上
-
消耗过多的空间,因为会反序列化所有的域
-
消耗过多的时间,因为会遍历对象图的拓扑关系
-
引起栈溢出,因为会递归遍历对象图
5
.defaultWriteObject
方法被调用的时,每个未被标记为transient
的域都会被序列化;defaultReadObject
将会反序列化这些域,在决定将一个域做成非transient之前,一定要确信它的值将是逻辑状态的一部人
6
.如果所有的域都是瞬时的,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这么做,可能会引起StreamCorruptedException
7
.如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步
8
.不管选择哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID
,这样可以避免因为自动生成UID造成不兼容
9
.如果想为一个类生成一个新的版本,使这个类与现有的类不兼容,那么只需修改UID即可,反序列化时将会发生InvalidClassException
76
.保护性地编写readObject方法
1
.《Java Object Serialization Specification
》
2
.当一个对象被反序列化的时候,如果哪个域将被客户端所引用,就必须要做保护性拷贝
3
.不要使用writeUnshared
和readUnshared
方法
writeUnshared:将“未共享”对象写入 ObjectOutputStream。此方法等同于 writeObject,不同点在于它总是将给定对象作为流中惟一的新对象进行写入
readUnshared:从流中读取一个不共享的对象,同readObject,但不会让后续的readObject和readUnshared调用引用这次调用反序列化得到的对象
4
.readObject不可以调用可被覆盖的方法,一旦调用,这个方法将在子类的域被反序列化之前运行,如果这个方法依赖子类的域,将导致程序失败
5
.对于任何约束条件,如果检查失败,则抛出InvalidObjectException
,这写检查应该在所有的保护性拷贝之后
6
.如果整个对象图在反序列化之后必须进行验证,则使用ObjectInputValidation
接口
77
.对于实例的控制,枚举类型优先于readResolve
1
.在反序列化之后,新建对象上的readResolve方法将被调用,然后该方法返回的对象将取代新建的对象
2
.如果依赖readObject进行实例控制,带有对象类型的所有实例域必须声明为transient的
3
.用readResolve进行实例控制并不过时,如果必须编写可序列化的实例受控的类,它的实例在编译时还不知道,你就无法将类表示成枚举
4
.如果把readResolve放在一个final类上,它就应该是私有的;如果readResolve方法不是私有的,并且子类没有覆盖它,对序列化过的子类进行反序列化,将会获得一个超类的实例,可能出现ClassCastException
78
.考虑用序列化代理代替序列化实例
1
.实际在流之间传输的是代理对象,反序列化之后,代理对象通过readResolve
返回对象,此时通过构造器创建外围对象,构造器中自带参数检查;序列化时,外围类通过writeReplace
返回序列化代理实例。
2
.writeReplace方法将在对象被写入流之前执行,然后用方法返回的实例替换自身被写入流中,与readResolve相对