创建和销毁对象
第1条 考虑用静态工厂方法代替构造器
- 静态工厂方法有名称,能确切地描述正被返回的对象。
- 不必每次调用都创建一个新的对象。
- 可以返回原返回类型的任何子类对象。
- 创建参数化类型实例时更加简洁,比如调用构造 HashMap 时,使用
Map < String,List < String > m = HashMap.newInstance()
,与Map < String,List < String > m > = new HashMap < String,List < String > >()
;
实践
JDK
源码提供了静态方法:Collections.emptyList();
(注意返回的List不是java.util.ArrayList而是java.util.Collections.EmptyList内部类)google
的guava
包中提供了Maps.newHashMap()
和Lists.newArrayList()
静态方法创建对象(返回的是java.util.ArrayList#ArrayList()和java.util.HashMap#HashMap())
第2条 遇到多个构造器参数时要考虑用构建器
简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是不错的选择,特别是当大多数参数都是可选的时候。
优势:
-
与传统的重叠构造器模式(即多个参数的构造器)相比,使用Builder模式的客户端代码理易于编写与阅读,也比JavaBeans模式(即使用构造器创建对象,提供setter方法设置参数)更加线程安全。
-
等待所有的参数验证通过才会build()对象。
-
与构造器相比,Builder 的微略优势在,builder可以有多个可变参数。构造器像方法一样,只有一个可变参数。因为builder利用单独的方法来设置每个参数,你想要多少个可变参数,他们就可以有多少个,直到每个setter方法都有一个可变参数。
-
Builder 模式非常灵活,可以使用单个Builder 构建多个对象。Builder 的参数可以在创建对象时进行调整。
-
设置了参数的Builder 生成一个很好的抽象工厂(Abstract Factory),也就是客户端可以将这样一个Builder 传给方法,使该方法能为客户端创建一个或者多个对象。
-
Builder 的建造者模式:使用必须的参数调用构造器,得到一个 Builder 对象,再在 builder 对象上调用类似 setter 的方法设置各个可选参数,最后调用无参的 build 方法生成不可变对象,new Instance.Builder(必须参数).setter(可选参数).build()。
-
Builder 模式让类的创建和表示分离,使得相同的创建过程可以创建不同的表示。
不足:
-
Builder 也有自己的不足,就是创建对象就必须创建它的构建器。虽然创建构建器的开销在实践中可能不是很明显,但是在某些十分注重性能的情况下,这个就是问题了。
-
Builder 模式还比重叠构造器模式更加的冗长,因此它会在参数多的时候使用。但是我们要知道,我们可能会在设计之后还要添加参数,所以已开始就用构建器还是比较好的。
-
静态工厂和构造器不能很好地扩展到大量的可选参数。
实践
[遇到多个构造器参数时要考虑用构建器][2]
[java学习-建造者模式(静态内部类)][3]
[静态内部类的使用][4]
[Java静态内部类、匿名内部类、成员式内部类和局部内部类][6]
public class Person {
private final String name;
private final int age;
private final String address;
private final String phone;
public static class Builder{
private final String name;
private final int age;
private String address = null;
private String phone = null;
public Builder(String name,int age){
this.name = name;
this.age = age;
}
public Builder address(String val){
address = val;
return this;
}
public Builder phone(String val){
phone = val;
return this;
}
public Person builder(){
return new Person(this);
}
}
private Person(Builder builder){
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
}
@Override
public String toString() {
return "name:"+name+" age:"+age+" address:"+address+" phone:"+phone;
}
}
public class PersonTest {
public static void main(String[] args) {
Person p = new Person.Builder("tom", 18).address("深圳").phone("110").builder();
System.out.println(p.toString());
}
}
第3条 用私有构造器或者枚举类型强化Singleton属性
单元素的枚举类型已经成为实现Singleton(单例模式)的最佳方法。
这种方法在功能上与公有域方法相近,但是它更简洁,无偿提供了序列化机制,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候。
第4条 通过私有构造器强化不可实例的能力
有些工具类,例如 Arrays 等对它们进行实例化并没有意义,所以应该在它们的构造方法上应该使用private修饰。
实践
由于只有当类不包括显式的构造器时,编译器才会生成缺省的构造器,因此我们只要让这个类包含私有构造器,它就不能被实例化了。
AssertionError不是必须的,但是可以避免不小心在类的内部调用构造器。
public class UtilityClass{
private UtilityClass(){
throw new AssertionError();
}
}
第5条 避免创建不必要的对象
书中提到 当你应该重用现有对象的时候,请不要创建新的对象,最能体现这一点的莫过于 String 对象的创建了。
实践
String name = "MarkLogZhu";
String name = new String("MarkLogZhu");
第一种 String 字符串的创建是在方法区(JDK7后改到了堆内存)中的常量池中创建一个”hello”常量,将来若再有一个字符串变量为“hello”时将直接指向常量池中的“hello”变量而不用重新创建;第二种则是会在堆变量中创建一个新的 String 实例,将来若再以此方式创建一个字符串变量为“hello”时还是会重新创建一个 String 实例。显然第二种方式创建了一个“不必要”的实例,相比较而言第一种方式更加高效。
除了这种外还需要关注程序中是否有些局部变量可以提升成类变量,以避免重复创建对象。例如:
public boolean isBabyBoomer(Date birthDate){
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1949, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
}
boomStart 和 boomEnd 对象在每次方法调用的时候都会重新生成,实际上是可以将他提升成类变量,以避免重复创建对象:
private static final Date BOOMSTART;
private static final Date BOOMEND;
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1949, Calendar.JANUARY, 1, 0, 0, 0);
BOOMSTART = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOMEND= gmtCal.getTime();
}
public boolean isBabyBoomer(Date birthDate){
return birthDate.compareTo(BOOMSTART) >= 0 && birthDate.compareTo(BOOMEND) < 0;
}
第6条 消除过期的对象引用
之所以要消除过期的对象引用其目的就在于尽量避免内存泄露的问题。
实践
书中举了一个“栈”的例子,其中在弹出一个元素时代码如下:
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
可以看到弹出元素时仅仅是将元素弹出后在将数组的索引-1,实际上数组维护的那个元素引用还在,也就是说那个被弹出的元素并不会被 GC,如此一来最后就很有可能导致内存泄露的问题。解决此类的办法就是,当不再使用这个元素时,将其置为 null。
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
需要注意的是消除对象引用并不是在任意条件下都成立,你应该仔细思考此时的引用是否为过期引用,“清空对象引用应该是一种例外,而不是一种规范行为”。
简单来说如果对象内部自己管理内存,如有 list 、数组等属性时,就需要警惕内存泄漏的问题,除此之外该干嘛干嘛去。
第7条 避免使用终结方法
这里的终结方法指的是 finalize()方法。在 C++ 中有一个“析构函数”,它代表在这个对象垃圾回收前所做的一些动作例如资源的关闭等。但是对于 Java 来说垃圾回收是自动的,或者称之为不可预知或不可控,尽管 finalize 方法所代表的也是在垃圾回收前所做的一些动作,但对于 GC 的时间你不能掌握,也就是说不能保证 finalize 方法会被及时执行,这是很危险的,一般情况下这个方法不会被用到。
对所有对象都通用的方法
第8条 覆盖equals时请遵守通用约定
对象的 equals() 方法可以被覆盖,在覆盖时需要遵守通用的约定,否则程序将会出现未知的错误,通用的约定包含:
- 自反性
- 对称性
- 传递性
- 一致性
- 非空性
自反性
对象需要等于它自身,实在不知道怎么违法这个约定。
@Override
public boolean equals(Object obj) {
if(this != obj){
return false;
}
return true;
}
对称性
如果 A == B,那么 B == A 也必须是成立的。
传递性
如果 A == B, B == C,那么 A == C 也必须是成立的。
但是在子类的时候常常会出现违反这个约定的情况,这个时候可以考虑下是否该使用组合,而不是继承。
一致性
如果两个对象相等,除非它们有被修改过,否则将一直相等。
非空性
所有的对象必须不等于 null。
实践
最佳实现方式
@Override
public boolean equals(Object obj) {
// 1.判断对象是否是该类的引用
if(this != obj){
return false;
}
// 2.判断对象的类型是否正确
if(!(obj instanceof Student)){
return false;
}
// 3.将对象转为正确的类型
Student student = (Student) obj;
// 4.对类中的关键字段进行检查,确认是否相等
if(!this.name.equals(student.name) || !this.sex.equals(student.sex)){
return false;
}
return true;
}
第9条 覆盖equals时总要覆盖hashCode
在覆盖 equals 方法的同时也必须要覆盖 hashCode 方法,Object 规范中规定 相等的对象必须具有相等的散列码(hashCode)。
怎么编写hashCode
1、申明一个int 类型的变量 result ,设置一个非零的默认值。
2、对于对象中的关键字段完成以下步骤
为该字段计算 int 类型的散列码 c
字段类型 | 公式 |
---|---|
boolean | c = (f ? 1 : 0); |
byte,char,short,int | c = (init) f; |
long | c = (int)(f ^ (f >> 32)); |
float | c = Float.floatToIntBits(f); |
double | t = Double.doubleToLongBits(f); c = (int) ( t ^ (t >> 32)); |
对象引用 | c = 对象.hashCode(); |
数组 | 循环数组,根据上面的公式计算散列码 |
按照下面的公式,把步骤 2.a 中计算得到的散列码 c 累加到变量 result。
result = 31 * result + c;
3、返回 result
第10条 始终要覆盖toString
为了方便之后的日志输出,这一条约定很显然是行之有效的方法。
第11条 谨慎地覆盖clone
clone 默认只能实现 浅拷贝 ,而不能实现 深拷贝。
浅拷贝和深拷贝的区别
浅拷贝:能复制变量,如果对象内还有对象,则只能复制对象的地址
深拷贝:能复制变量,也能复制当前对象的内部对象
第12条 考虑实现 Comparable 接口
当你的类需要进行排序时,可以考虑实现 Comparable
接口,Comparable
接口中只有一个方法 compareTo
。相比于 equals
方法只能返回 True
和 false
,compareTo
可以代表更多的含义:
public class Student implements Comparable<Student> {
@Override
public int compareTo(Student student) {
return this.age - student.getAge();
}
}
compareTo
约定第一个对象若“大于”第二个对象则返回整数,“等于”则返回0,“小于”则返回负数,compareTo 能约定更为复杂的“比较”。
有一个强烈的建议就是 compareTo
应该返回和 equals
方法相同的结果,当然建议的意思就是你也可以不采纳,但最好有个注释以防止后来者踩坑。
类和接口
第13条 使类和成员的可访问性最小化
这条建议实际上就是讲解面向对象的三大特性之一 “封装”。
封装的好处有:
- 只提供方法调用而隐藏内部的实现,调用者不需要关心内部实现
- 可以灵活修改类内部实现而不用担心影响调用者的使用
- 内部属性不会被其他类所使用,防止对象间的不良交互,提高代码的模块化和安全性
实现“封装”的方式其实就是通过修饰符权限来控制:
修饰符 | 权限范围 |
---|---|
private | 只有在类内部才可以访问 |
package-private | 默认的访问权限(接口除外,接口默认为 public),只有在同一个包下的类才能访问,就算是它的子类但不是在同一包下也不能访问 |
protected | 只有在同一个包下,或者是它的子类才能访问 |
public | 任何类都可以访问 |
子类不能提供比父类更大范围的权限,如父类方法是以 protected
修饰,子类继承后不能实现为 public
的修饰方法。当然比父类访问范围小也是不可以的。
第14条 在公有类中使用访问方法而非公有域
这条其实是从上一条延伸过来,不应该直接暴露字段给外部调用,而是应该通过 get/set 方法的形式提供调用。使用方法的形式调用,可以更好的保留更改类内部表示的灵活性。
第15条 使可变性最小化
最好的类就是不变的类,如果做不到不变,那么就让它的可变性变的更小的吧。通过使用 final 关键字来申明不可变。
final 用法
- 和
static
关键字配合,以常量的形式代替硬编码:
private static final int ZERO = 0;
- 修饰类使其不能被继承,如 String
- 修饰成员变量,使得该变量变为不可变的对象引用,此时应该给它赋初值,之后它不能被重新赋值,准确来讲是它的引用不可改变。注意是引用不可改变,不代表被引用的对象内部不能改变。
第16条 复合优先于继承
继承是实现代码复用的有效方式,但是有时候它也会破坏类的封装性。一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。
复合(has-a)和继承(is-a)两者在意义上有明显的区别:
1.继承明确了基类和子类的职责关系,基类(A)要实现的功能,子类(B)也要实现,但是实现方式可以有区别。
2.复合则属于互相平行关系,以A和B来说,A实现的功能,B不必去实现,但是B在实现所属业务的过程中可以调用A的方法。
3.根据业务需要,使用不同的方式才是最合适的方式。业务上明确A和B是继承关系的,则使用继承。否则,这个时候就需要去考虑下,是否还应该使用继承,是否使用复合更合适。
第17条 要么为继承而设计,并提供文档说明,要么就禁止继承
除非真的是非继承不可的,尽量不要使用继承。因为继承的成本太高了,需要关注它的父类的生命周期。如果你不这样做,子类可能会依赖于父类的实现细节,并且如果父类的实现发生改变,子类可能会损坏。 为了允许其他人编写高效的子类,可能还需要导出一个或多个受保护的方法。 除非你知道有一个真正的子类需要,否则你可能最好是通过声明你的类为final禁止继承,或者确保没有可访问的构造方法。
第18条 接口优于抽象类
接口和抽象类的区别
1.接口不能被实例化,不能有构造器,抽象类也不能被实例化但可以有构造器;
2.接口中不能有实现方法(JDK8在接口中可以有实现方法,称“默认方法”),抽象类中可以有实现方法,也可以没有;
3.接口方法的默认修饰符就是public,不可以用其他修饰符,抽象类可以有public、protected、default;
4.一个类只支持单继承,但可以实现多接口;
第四点就已经能够很好的体现接口优于抽象类了,不同的行为可以提交多个接口,但是如果使用抽象类的话你就必须实现一些不相干的方法。
第19条 接口只用于定义类型
这个建议是让接口不要只用于定义常量,使之变成常量接口,如果一个类只有常量应该使用枚举类型或者不可实例化的工具类。JDK中的反例就是 java.io.ObjectStreamConstant。
如果大量利用工具类导出的常量,可以通过利用静态导入(JDK 1.5+)机制,避免用类名来修饰常量名。
第20条 类层次优于标签类
标签类是指在类中定义了一个变量,使用该变量的值来控制该做什么动作。例如:
定义一个Figure类,使用shape变量,可以传入“长方形”或者“圆形”,根据传入的类型不同调用共同的方法。
这就是一个标签类,如果新增一个“三角形”的话,就得修改这个标签类的代码。而更好的方法就是利用继承,合理利用继承能更好的体现面向对象的多态性。
第21条 用函数对象表示策略
什么是函数对象?
在 JDK8 之前 Java 还没有支持 lamda 表达式,方法参数不能传递一个方法只能通过传递对象的方式“曲线救国”,例如Arrays.sort(T[] a, Comparator<? super T> c)方法,第一个参数传递数组,根据传入第二个自定义的比较类中的比较方法进行排序。如果能传入函数指针、Lambda表达式等,那就自然不用传递一个类。
第22条 优先考虑静态成员类
什么是嵌套类?
嵌套类(nested class
)是在另一个类中定义的类。
有四种嵌套类,分别是:静态成员类,非静态成员类,匿名类和局部类。 除了第一种以外,剩下的三种都被称为内部类(inner class
)。
静态成员类:
public class Main{
public static class NestClass{}
}
// 在外面使用时候形式如下,在 Main 中使用则不需要加上外部类限定
Main.NestClass nestClass = new Main.NestClass();
非静态成员类:
public class Main{
public class NestClass{}
}
匿名类:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是一个匿名类");
}
});
thread.start();
局部类:
public class Test {
{
class AA{}//块内局部类
}
public Test(){
class AA{}//构造器内局部类
}
public static void main(String[] args){
}
public void test(){
class AA{}//方法内局部类
}
}
//注意到了吧,可以同名,编译后,形成诸如:外部类名称+$+同名顺序+局部类名称
//Test$1AA.class/Test$2AA.class/Test$3AA.class
为什么优先考虑静态成员类?
静态成员类 相比较于 非静态成员类 就是多了一个 static 关键字修饰类,另外一个更重要的区别在于非静态成员类的每个实例都包含一个额外的指向外围对象的引用,保存这份引用要耗费时间和空间。
那么什么时候使用静态什么时候使用非静态呢?
如果声明成员类不要求访问外围实例,就要始终把 static 修饰符放在它的声明中。也就是说如果成员类和外围实例类有交互,那这个类就应该是非静态的,如果没有交互而是作为外围类的一个组件存在在应使用静态的。
局部类的使用场景
只要是在任何“可以声明局部变量的地方”,都可以声明局部类,用得最少,如果要使用那也必须非常简短。
泛型
第23条 请不要在新代码中使用原生态类型
Java 1.5中增加了泛型(Generic)。在没有泛型之前,从集合中读取到的每一个对象都是必须进行转换的。如果不小心插入了类型错误的对象,在运行时的转换处理就会出错。有了泛型以后,可以告诉编译器每个集合中接收哪些对象类型。编译器就会自动地为你的插入进行转化,并在编译时告知是否插入了类型错误的对象。这样可以是程序既更加安全,又更加清楚。
每个泛型都会定义一个原生态类型(Raw Type),即不带任何实际类型参数的泛型名称。例如,与List
在没有泛型之前,定义一个保存int类型的对象如下:
List nameList = new ArrayList();
nameList.add(1);
但如果添加一行nameList.add("java");
也是能编译成功的。
切记:错误要尽早发现,最好是编译时返现。
但是在本例中,直到运行起来,你从nameList
中获取int item0 = Integer.parsetInt(nameList.get(i));
当你运行到“java”
时就会出现ClassCastException
错误。
有了泛型之后,就可以利用改进后的类型声明来替代集合中的这种情况,告诉编译器集合里只能存放什么类型参数。
List<Integer> nameList = new ArrayList<Integer>();
再添加nameList.add("java");
编译器就会给出错误警告。而且,获取集合中的元素时不需要再手动转换了(Integer.parsetInt()
),直接可以int item0 = nameList.get(i);
简单来说,使用泛型会相对“安全”点,从一开始就能限定数据类型,防止之后不小心插入了错误的类型,而对于原生态类型则不会检查插入的类型,有可能在以后插入了其他类型而只有在运行时才抛出异常,所以鼓励使用泛型。
第24条 消除非受检警告
Set<Student> exaltation = new HashSet();
会出现如下警告:
Unchecked assignment: 'java.util.HashSet' to 'java.util.Set<com.marklogzhu.platform.test1.Student>' less... (Ctrl+F1)
Signals places where an unchecked warning is issued by the compiler, for example:
void f(HashMap map) {
map.put("key", "value");
}
Hint: Pass -Xlint:unchecked to javac to get more details.
然后可以进行指示修正,让警告消失。 请注意,实际上并不需要指定类型参数,只是为了表明它与Java 7中引入的钻石运算符("<>")一同出现。然后编译器会推断出正确的实际类型参数(在本例中为Student):
Set<Student> exaltation = new HashSet<>();
但有时候一些警告难以消除,但你确认没有问题时,可以使用 @SuppressWarnings(“unchecked”)
注解来抑制警告。请注意一定要最小范围的使用它,如可以在方法上使用的就不要在类上面申明,可以在变量中申明的就不要在方法上申明。
每个未经检查的警告代表在运行时出现 ClassCastException
异常的可能性。 尽你所能消除这些警告。 如果无法消除未经检查的警告,并且可以证明引发该警告的代码是安全类型的,则可以在尽可能小的范围内使用 @SuppressWarnings(“unchecked”)
注解来禁止警告,并在注释中记录你决定抑制此警告的理由。
第25条 列表优先于数组
列表和数组最大的区别在于,数组是协变的,这里的“变”指的是数据类型,而不是说数组的长度,数组的长度当然从一开始就确定不可改变,但对于以下代码在编译期是合法的:
public static void main(String[] args) throws Exception{
Object[] objects = new Long[1];
objects[0] = "hello world";
System.out.println(objects[0]);
}
但是在运行时就会抛出错误:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
at com.marklogzhu.platform.test1.Student.main(Student.java:14)
而在使用列表的时候,将会在编译期就会报错。
总而言之,数组和泛型有着非常不同的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。
第26条 优先考虑泛型
当你不知道需要传递什么参数的时候请优先考虑泛型,例如:
public interface List<E> extends Collection<E> {
......
}
使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成泛型。如果你现在有任何类型应该是通用的但却不是通用的,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端。
第27条 优先考虑泛型方法
正如类可以是泛型的,方法也可以是泛型的。 对参数化类型进行操作的静态工具方法通常都是泛型的,例如:
@Component("applicationContextHelper")
public class ApplicationContextHelper implements ApplicationContextAware {
/**
* Spring 应用上下文环境
*/
private static ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = context;
}
public static <T> T popBean(Class<T> clazz) {
if (applicationContext == null) {
return null;
}
return applicationContext.getBean(clazz);
}
public static <T> T popBean(String name, Class<T> clazz) {
if (applicationContext == null) {
return null;
}
return applicationContext.getBean(name, clazz);
}
}
泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来的更加安全,也更加容易。就像类型一样,你应该确保新的方法可以不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端。
第28条 利用有限制通配符来提升 API 的灵活性
什么是通配符
List<?> list = new ArrayList<>();
像 <?>
这种就叫做通配符,它通常用于定义一个引用变量,而不必如下面这样定义:
List<String> list = new ArrayList<>();
List<Integer> list = new ArrayList<>();
<?>
和 <T>
的区别
<?>
定义在引用变量上,而 <T>
是用在类上或方法上。
什么是有限制通配符?
有范围限制的通配符,如:
<? extends E>
:表示可接受 E
类型的子类型;
<? super E>
:表示可接受 E
类型的父类型。
为什么要使用有限制通配符?
避免出现类型转换错误,所以尽量让方法的使用是一个类型,例如:
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);
它会抛出如下错误:
StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
numberStack.pushAll(integers);
第29条 优先考虑类型安全的异构容器
异构”的英文 heterogeneous
意为多种多样的,书中所举的例子如下:
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null) {
throw new NullPointerException();
}
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
Integer favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s", favoriteString, favoriteInteger, favoriteClass.getName());
}
Favorite
类使用起来有点Map的感觉,putFavorite
方法就类似Map.put
,或者说用Map
不就能实现吗?例如:
public static void main(String[] args) {
Map<Class<?>, Object> map = new HashMap<Class<?>, Object >();
map.put(String.class, "Java");
map.put(Integer.class, 122);
System.out.println(map.get(String.class));
System.out.println(map.get(Integer.class));
}
能运行和上面结果一致,但问题就在于以下代码:
public static void main(String[] args) {
Map<Class<?>, Object> map = new HashMap<Class<?>, Object >();
map.put(String.class, "Java");
map.put(Integer.class, 122);
Object str = map.get(String.class);
//Integer str = (Integer) map.get(String.class);
Object in = map.get(Integer.class);
}
根据键取出来的值是 Object,也就是说这是很危险的一件事情。如果代码写成上面注释那样的话,在编译时是无法判断的,只有在运行时才会抛出异常。记住,能在编译时检查就在编译时检查,而不要等到真正运行起来才做检查,这也就是上面 Favorite 所带来的好处,它是类型安全的,同时它也是异构的,这个例子值得细细品味。
枚举和注解
第30条 用 enum 代替 int 常量
在代码中你可能使用过如下的代码,用来减少代码中的硬编码:
// 星期一
private static final int MONDAY = 1;
// 星期二
private static final int TUESDAY = 2;
// 星期三
private static final int WEDNESDAY = 3;
// 星期四
private static final int THURSDAY = 4;
// 星期五
private static final int FRIDAY = 5;
// 星期六
private static final int SATURDAY = 6;
// 星期日
private static final int SUNDAY = 7;
使用这种方式会存在如下问题:
表达内容少
名称相同的两个常量,需要不同的前缀避免冲突导致命名过长
......
这个时候通常建议使用枚举类,实际上枚举类型对于强化项目代码的结构和规整也很有帮助:
public enum PayrollDayEnum {
MONDAY(1, "星期一"),
TUESDAY(2, "星期二"),
WEDNESDAY(3, "星期三"),
THURSDAY(4, "星期四"),
FRIDAY(5, "星期五"),
SATURDAY(6, "星期六"),
SUNDAY(7, "星期日");
PayrollDayEnum(int day, String desc) {
this.day = day;
this.desc = desc;
}
private int day;
private String desc;
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
public static void main(String[] args) {
System.out.println(PayrollDayEnum.MONDAY.getDay()+":"+PayrollDayEnum.MONDAY.getDesc());
}
这可能是我们使用枚举比较常见的一种用法。实际上枚举还有其它一些比较“高级”的用法,我们不妨从书中举例来一一说明。首先用枚举来实现加减乘除四种操作:
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
double apply(double x, double y) {
switch (this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Unknow op:" + this);
}
}
public static void main(String[] args) {
double x = 1.1;
double y = 2.2;
double result = Operation.PLUS.apply(x, y);
System.out.println(result);
}
如果这时候需要新增另外一种操作的时候却忘了新增 case
怎么办?编译时编译器并不会给出任何提示,同样的功能考虑以下实现能很好的避免这种遗忘新增 case 的情况:
public enum Operation {
PLUS {
double apply(double x, double y) {
return x + y;
}
},
MIUS {
double apply(double x, double y) {
return x - y;
}
},
TIMES {
double apply(double x, double y) {
return x * y;
}
},
DEVIDE {
double apply(double x, double y) {
return x / y;
}
};
abstract double apply(double x, double y);
}
现在你可以尝试新增一个操作,可以发现编译器会提示你必须实现 apply
方法,否则编译不通过。
总而言之,与 int
常量相比,枚举类型的优势是不言而喻的。枚举要易读得多,也更加安全,功能更加强大。许多枚举都不需要显式的构造器或者成员,但许多其他枚举则受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”。只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。如果多个枚举常量同时共享相同的行为,则考虑策略枚举。
第31条 用实例域代替序数
枚举类型中有一个 ordinal
方法,它返回每个枚举常量类型的数值位置。它的使用方式如下:
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
虽然这个枚举类也可以正常工作,但是如果对这些常量进行重新排序,那么程序很可能就会出现逻辑错误。所以不建议使用这个方法,因为这不能很好地对枚举进行维护,正确应该是利用实例域,例如:
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
在枚举规范中申明了 ordinal
方法的使用场景:“大多数程序员对这种方法没有用处。 它被设计用于基于枚举的通用数据结构,如 EnumSet
和 EnumMap
。“除非你在编写这样数据结构的代码,否则最好避免使用 ordinal
方法。
第32条 用 EnumSet 代替位域
EnumSet
在目前为止我都没有使用过,这里先来介绍下 EnumSet
是什么类型,它存在的意义是什么。
我们都知道 HashSet
不包含重复元素,同样EnumSet
也和 HashSet
一样实现自 AbstractSet
,它也不包含重复元素,可以说它就是为 Enum
枚举类型而生,在《Thinking in Java》
中这样描述 “Java SE5
引入EnumSet
,是为了通过enum
创建一种替代品,以替代传统的基于int
的“位标识””。本书中也是提到用 EnumSet
来代替位域。
关于 EnumSet
中的元素必须来自同一个 Enum
,并且构造一个 EnumSet
实例是通过静态工厂方法—— noneOf
,用法如下:
public enum Operation {
PLUS, MINUS, TIMES, DEVIDE;
}
public static void main(String[] args) throws InterruptedException {
EnumSet<Operation> enumSet = EnumSet.noneOf(Operation.class);
enumSet.add(Operation.DEVIDE);
System.out.println(enumSet);
enumSet.remove(Operation.DEVIDE);
System.out.println(enumSet);
}
在书中提到的位域,实际上就是 OR
位运算,换句话说就是“并集”也就是 Set
所代表的就是并集,在使用 int
型枚举模式的时候可能会用到类似 “1 || 2
”,这个时候不如用 Enum
枚举加上 EnumSet
来实现。
第33条 用 EnumMap 代替序数索引
有了上一条 EnumSet
的经验,实际上 EnumMap
和 HashMap
也类同,不同的是它是为 Enum
为生的,同样它的键也只允许来自同一个 Enum
。本条目所说的不要使用序数,实际上就是利用Map
而不是是使用数组这个意思。我们来看下《Thinking in Java》
中的例子(命令设计模式):
public enum AlamPoints {
KITCHEN, BATHROOM;
}
public interface Command {
void action();
}
public static void main(String[] args) throws InterruptedException {
EnumMap<AlamPoints, Command> em = new EnumMap<AlamPoints, Command>(AlamPoints.class);
em.put(AlamPoints.KITCHEN, new Command() {
@Override
public void action() {
System.out.println("Kitchen fire");
}
});
em.put(AlamPoints.BATHROOM, new Command(){
@Override
public void action() {
System.out.println("Bathroom alert!");
}
});
for (Map.Entry<AlamPoints, Command> e : em.entrySet()) {
System.out.print(e.getKey() + ":");
e.getValue().action();
}
}
这个例子说明了 EnumMap
的基本用法,和 HashMap
除了在构造方法上的不同外,基本无异。
第34条 用接口模拟可伸缩的枚举
在第30条的时候我们提过这样一个实例:
public enum Operation {
PLUS {
double apply(double x, double y) {
return x + y;
}
},
MIUS {
double apply(double x, double y) {
return x - y;
}
},
TIMES {
double apply(double x, double y) {
return x * y;
}
},
DEVIDE {
double apply(double x, double y) {
return x / y;
}
};
abstract double apply(double x, double y);
}
避免新增一个枚举操作时忘记实现 apply
方法,所以定义了一个 abstract
方法,强制必须覆盖
apply
方法。但是从软件开发可扩展性来讲,这并不是一个好的解决方案。软件可扩展性并不是在原有代码上做修改,如果这段代码是在jar
中的呢?这个时候就需要接口出场了,我们修改上述例子:
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation{
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
},
MIUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES ("*") {
public double apply(double x, double y) {
return x * y;
}
},
DEVIDE ("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
}
当我们需要扩展操作符枚举的时候只需要重新实现Operation
接口即可:
public enum ExtendedOperation implements Operation {
EXP ("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER ("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
}
这样就达到代码的可扩展性,这样做的有一个小小的不足就是无法从一个枚举类型继承到另外一个枚举类型。
第35条 注解优先于命名模式
命名模式的缺点:
拼写错误,不会编译错误。例如:
public void tets(){...}
无法确保它们仅用于适当的程序元素
没有提供将参数值与程序元素相关联的好的方法
而注解可以解决以上的问题,如 @Test
注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
注:Test
注解类型的声明本身使用 Retention
和 Target
注解进行标记。 注解类型声明上的这种注解称为元注解。
@Test
public void testFindByOrderId();
这种使用方式被称为标记注解,因为它没有参数,只是“标记”注解元素。 如果程序员错拼 Test
或将Test
注解应用于程序元素而不是方法声明,则该程序将无法编译。
注解永远不会改变被注解代码的语义,它们只负责提供信息供相关的程序使用。
对于注解运行原理,以及如何正确使用自定义的注解在这里不做过多讲解,此条的目的在于对待“特定程序员”,注解是他们编写“工具类”、“框架类”的利器。
第36条 坚持使用Override注解
如果要重写父类的方法,一定要使用 Override
注解。它会提供编译期的检查,将避免产生大量的恶意 bug
。
第37条 用标记接口定义类型
标记接口是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口,例如实现了 Serializable
接口表示它可被实例化。在有的情况下使用标记注解比标记接口可能更好,但书中提到了几点标记接口胜过标记注解:
1)标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
2)可以更精确地定位目标
标记注解优于标记接口的主要优点是它们是较大的注解工具的一部分。因此,标记注解允许在基于注解的框架中保持一致性。
什么时候应该使用标记注解,什么时候应该使用标记接口?
如果标记适用于除类或接口以外的任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。
如果标记仅适用于类和接口,那么问自己问题:“可能我想编写一个或多个只接受具有此标记的对象的方法呢?”如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译时类型检查的好处。
如果你能说服自己,永远不会想写一个只接受带有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。
方法
第38条 检查参数的有效性
参数的有效性检查,最常见的莫过于检查参数是否为 null
。
有时出现调用方未检查传入的参数是否为空,同时被调用方也没有检查参数是否为空,结果这就导致两边都没检查以至于出现 null
的值程序出错,通常情况下会规定调用方或者被调用方来检查参数的合法性,或者干脆规定都必须检查。null
值的检查相当有必要,很多情况下没有检查值是否为空,结果导致抛出 NullPointerException
异常。
第39条 必要时进行保护性拷贝
public class Order {
private String orderId;
private Date createTime;
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
这是一个简略版的一个订单实体类,这里面存在着一个问题。
我们知道 Date
是一个引用类型的数据。如果将这个订单复制给其他对象时,其他对象对创建时间进行修改操作,原来的对象也会被修改。为了避免这个问题,我们需要做一个拷贝操作:
public class Order implements Cloneable{
private String orderId;
private Date createTime;
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
protected Order clone() throws CloneNotSupportedException {
Order order = this.clone();
order.createTime = (Date) this.createTime.clone();
return order;
}
}
这样建议跟 第11条:谨慎地覆盖clone 某种意义上说冲突的,需要看使用场景。
第40条 谨慎设计方法签名
方法签名不仅仅是指方法命名,还包括方法所包含的参数。
方法命名要遵循一定的规则和规律,可参考JDK
的命名;
方法所包含的参数最好不应超过4个,如果超过4个则应考虑拆分成多个方法或者创建辅助类用来保存参数的分组。
第41条 慎用重载
与重载类似的是重写,但是它们有本质区别:
重写是子类的方法重新实现父类的方法,包括方法名和参数都要相同;
重载则不用要求是要继承,只要求拥有相同的方法名,参数类型不同个数不同都可以称之为重载。
重载会带来什么问题?
我们来演示一个实例:
public class CollectionClassifier {
public static String classify(Set<?> set) {
return "Set";
}
public static String classify(List<?> list) {
return "List";
}
public static String classify(Collection<?> collection) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = {new HashSet<String>(), new ArrayList<String>(), new HashMap<String, String>().values()};
for (Collection<?> c : collections) {
System.out.println(classify(c));
}
}
}
运行结果如下:
Unknown Collection
Unknown Collection
Unknown Collection
可以看到跟我们预期的结果不一致。在来看一个实例:
public static void main(String[] args) {
Set<Integer> set = new TreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
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 + " " + list);
}
运行结果:
[-3, -2, -1] [-2, 0, 2]
原因就在于对于 List
的 remove
方法有两个,这两个在这里产生了歧义,其中一个是删除下标索引元素,另一个是删除集合中的元素。如果将第21行
list.remove(i);
修改为
list.remove((Integer)i);
则避免得到了我们想要的结果。
这就是重载带来的“危害”,稍不留神就出现了致命问题。比较好的解决办法学习 ObjectOutputStream
类中做法:writeBoolean(boolean),writeInt(int),writeLong(long)
。如果两个重载方法的参数类型很相似,那一定得考虑这样做是否容易造成“程序的误解”。
第42条 慎用可变参数
可变参数的作用
我们在编写方法的过程中,可能会遇见一个方法有不确定参数个数的情况。一般我们会用方法重载来解决问题:
public void method();
public void method(int i);
public void method(int i, int j);
public void method(int i, int j, int k);
但是当参数多的时候就会显得很繁琐,同时每次扩展都会很麻烦。于是我们可以使用数组作为参数:
int[] a={1, 2, 3, 4};
public void method(int[] args);
```
这样还是有个准备参数的过程(需要构造一个数组,麻烦啊)。于是我们可以使用不定项参数(可变参数)的方式:
public void method(int...args);
是的,你没有看错就是省略号,格式就是这样,不是我省略了什么。
为什么要慎用可变参数
1) 在没有传入参数的时候,程序没有如果没有做任何保护会导致程序错误。
2) 它会带来一定的性能问题,因为可变参数方法的每次调用都会导致进行一次数组分配和初始化。
总之,“在定义参数数目不定的方法时,可变参数是一种很方便的方式,但是它们不应该被过度滥用。如果使用不当,会产生混乱的结果”。
### 第43条 返回零长度的数组或者集合,而不是null
像下面的代码应该在程序中可能是随处可见的:
List
if(studentList !=null && studentList.size()>=0){
......
}
对于零长度的数组或者集合,如果返回 `null` 的话,调用方又没有做 `null` 判断,就会很容易报 `java.lang.NullPointerException` 异常。所以为了避免这个问题,应该返回零长度的数组或者集合,而不是`null`。
### 第44条 为所有导出的API元素编写文档注释
// 写这段代码的时候,只有上帝和我知道它是干嘛的 。现在,只有上帝知道了。
为了避免上面这个问题的出现,请为所有的方法或你认为比较重要的地方写上注释。为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。
在文档注释内部出现任何HTML标签都是允许的,但是HTML元字符必须经过转义。
## 通用程序设计
### 第45条 将局部变量的作用域最小化
人的记忆和注意力是有限的,所以尽可能的将局部变量的作用域最小化是代码可读性提升的方式之一,关于这块可以花一天时间阅读下《编写可读代码的艺术》你可能会有更深的体会。
并且作用域最小化,出现错误的概率也会相对变小(毕竟一屏幕就能看到,设置错了也能马上发现)。
### 第46条 for-each 循环优先于传统的for循环
`for-each` 语法格式:
for (Element e : elements) {
doSomething(e);
}
当对集合、数组中的元素只做遍历时应首选 `for-each`,而不是通过 `for` 循环手动移动数组下标。
总之,for-each循环在简洁性和预防Bug方面有着传统的for循环无法比拟的优势,并且没有性能损失。应该尽可能的使用for-each循环。遗憾的是,有三种常见的情况无法使用for-each循环:
- **过滤**——如果需要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用他的remove方法。
- **转换**——如果需要遍历列表或者数组,并取代他部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
- **平行迭代**——如果需要并行的遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
在以上任何一种情况下,就要使用普通的for循环,要警惕提到的陷阱,并且要确保做到最好。
### 第47条 了解和使用类库
在项目中不要重复造轮子,利用现有的已成熟的技术能避免很多 bug 和其他问题。当然自己学习的时候请疯狂造吧,不然你怎么会进步呢?
### 第48条 如果需要精确的答案,请避免使用float和double
`float` 和 `double` 表示浮点类型数据,对于要精确到小数点的数值运算,通常会选择 `float`或者 `double` 类型,但实际上这两种类型对于精确的计算是存在一定隐患的。
对于精确的数值计算,应该要优先使用 `BigDecimal`,或者可以使用`int`、`long`型将单位缩小不再有小数点。
`double` 实例如下:
public static void main(String[] args) {
double funds = 1.0;
double price = .10;
for (int i = 0; i < 5; i++) {
funds -= price;
}
System.out.println(funds);
}
运行结果:
0.5000000000000001
`BigDecimal` 实例如下:
public static void main(String[] args) {
BigDecimal funds = new BigDecimal(1.0);
BigDecimal price = new BigDecimal(.10);
for (int i = 0; i < 5; i++) {
funds = funds.subtract(price);
}
System.out.println(funds.doubleValue());
}
运行结果:
0.5
### 第49条 基本类型优先于装箱基本类型
`Java` 数据类型分为基本类型和引用类型,对于 `int` 基本类型的值我们直接用 `==` 比较是否相等,那么它的装箱类型 `Integer` 又是怎样呢?
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
Integer c = 1000;
Integer d = 1000;
System.out.println(a == b);
System.out.println(c == d);
}
运行结果:
true
false
这是由于在 `Integer` 中会缓存`-128~127`的小数值,在自动装箱的时候对这些小数值能直接比较。再来看下面例子:
public static void main(String[] args) {
Integer a = new Integer(127);
Integer b = new Integer(127);
Integer c = new Integer(1000);
Integer d = new Integer(1000);
System.out.println(a == b);
System.out.println(c == d);
}
运行结果:
false
false
这个时候就都是引用类型,需要通过 `equals` 方法来比较了。
基本类型在某些场景下优先于装箱基本类型,并且装箱基本类型还会带来性能问题。所以某些场景下要优先考虑基本类型。
### 第50条 如果其他类型更合适,则尽量避免使用字符串
对变量申明合适的类型,而不是用字符串通用。比如只有 `true` 和 `false` 的值用布尔类型是更合适的;数值计算用数值类型比用字符串更好。
### 第51条 当心字符串连接的性能
`String` 字符串是不可变的,每次对一个字符串变量的赋值实际上都在内存中开辟了新的空间。如果要经常对字符串做修改应该使用 `StringBuilder`(线程不安全)或者 `StringgBuffer`(线程安全),其中 `StringBuilder`由于不考虑线程安全,它的速度更快。
### 考虑程序代码的灵活性应该优先使用接口而不是类来引用对象,例如:
List
这样带来的好处就是可以更换 `List` 的具体实现只需一行代码,这样就能实现程序的灵活性。
### 第53条 接口优先于反射机制
反射可以在编译时不知道对象,运行时访问对象,但是但它也有以下负面影响:
- 丧失了编译时类型检查
- 执行反射访问所需要的代码非常笨拙和冗长(这需要一定的编码能力)
- 性能损失
在使用反射时利用接口指的是,在编译时无法获取相关的类,但在编译时有合适的接口就可以引用这个类,当在运行时以反射方式创建实例后,就可以通过接口以正常的方式访问这些实例。
### 第54条 谨慎地使用本地方法
所谓的本地方法就是在 JDK 源码中你所看到在有的方法中会有 “native” 关键字的方法,这种方法表示用 C 或者 C++ 等本地程序设计语言编写的特殊方法。之所以会存在本地方法的原因主要有:访问特定平台的接口、提高性能。
实际上估计很少很少在代码中使用本地方法,就算是在设计比较底层的库时也不会使用到,除非要访问很底层的资源。当使用到本地方法时唯一的要求就是全面再全面地测试,以确保万无一失。
### 第55条 谨慎地进行优化
不要盲目的进行优化,相比于性能,写出结构优美、设计良好的代码更重要。
性能的问题应该有数据做支撑,也就是有性能测试软件对程序测试来评判出性能问题出现在哪个地方,从而做针对性的修改。
逻辑的问题需要有单元测试做最后的保证。
### 第56条 遵守普遍接受的命名惯例
代码是给人看的,所以让人看懂的代码才是好代码的基础。
## 异常
### 第57条 只针对异常的情况才使用异常
这条建议是不要滥用异常,比如可以通过 if 判断避免的错误就不要使用通过捕获异常来解决,例如:
public static void main(String[] args) {
List
try{
for(String str : list){
System.out.println(str);
}
}catch (Exception e){
e.printStackTrace();
}
}
上面这种写法就是很典型的错误,通过 `try/catch` 捕获异常实现逻辑对性能影响很大。
异常是为了在异常情况下使用而设计的。不要将它们用于普通的控制流,也不要编写迫使它们这么做的API。
### 第58条 对可恢复的情况使用受检异常,对程序错误使用运行时异常
什么时候使用受检查的异常(`throws Exception`),什么时候使用不受检查的异常(`throws RuntimeException`),本书中给出原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常。对于程序错误,则使用运行时异常。例如在数据库的事务上通常就会对异常做处理,防止出现数据不一致等情况的发生。
### 第59条 避免不必要地使用受检的异常
对于异常本身初衷是使程序具有更高的可靠性,特别是受检查的异常。但滥用受检查的异常可能就会使得程序变得复杂。
两种情况同时成立的情况下就可以使用受检查的异常:
1、正确地使用 API 并不能阻止这种异常条件的产生;
2、如果一旦产生异常,使用 API 的程序员可以立即采取有用的动作。
以上两种情况成立时,就可以使用受检查的异常,否则可能就是徒增烦恼。
另外在一个方法抛出受检查异常时,也需要仔细考量,因为对于调用者来讲就必须处理做相应处理,或捕获或继续向上抛出。如果是一个方法只抛出一个异常那么实际上可以将抛出异常的方法重构为 `boolean` 返回值来代替。
### 第60条:优先使用标准的异常
抛出能更明显反馈错误信息的异常,而不是抛出` Exception` 异常这么笼统的异常。例如参数值不合法就抛出 `llegalArgumentException` 等。
常见异常
|异常|使用场景|
|:-|:-|
|IllegalArgumentException|不合法的参数异常
|IllegalStateException| 对象状态不正确
|NullPointerException|参数值为null
|Indexoutofboundsexception|数值下标越界
|ConcurrentModificationException|当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。
|UnsupportedOperationException| 对象不支持用户请求方法
### 第61条 抛出与抽象相对应的异常
简单来说就是将异常转换,将错误信息更具体的返回给调用者,例如:
public static void main(String[] args) {
List
try{
for(String str : list){
System.out.println(str);
}
}catch (Exception e){
throw MyException("对象 list 空指针异常");
}
}
将 `Exception` 转为自定义的异常,将错误信息更友好的提供给调用者。
总之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转换,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。
### 第62条 每个方法抛出的异常都要有文档
跟方法一样,异常也应该有注释,当调用方能明确知道异常产生的原因,他们的处理将更合理。
### 第63条 在细节信息中包含能捕获失败的信息
捕获异常的时候也要做好日志输出功能,很多时候我们需要通过日志来复现异常原因。
### 第64条 努力使失败保持原子性
失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性。
失败过后,我们不希望这个对象不可用。在数据库事务操作中抛出异常时通常都会在异常做回滚或者恢复处理,要实现对象在抛出异常过后照样能处在一种定义良好的可用状态之中,有以下两个办法:
1) 设计一个不可变的对象。不可变的对象被创建之后它就处于一致的状态之中,以后也不会发生变化。
2) 在执行操作之前检查参数的有效性。例如对栈进行出栈操作时提前检查栈中的是否还有元素。
3) 在失败过后编写一段恢复代码,使对象回滚到操作开始前的状态。
在对象的临时拷贝上执行操作,操作完成过后再用临时拷贝中的结果代替对象的内容,如果操作失败也并不影响原来的对象。
### 第65条 不要忽略异常
忽略就是指写出以下代码:
try {
doSomething();
} catch (Exception e) {
}
这样的代码就是把异常吃掉了,当出现异常的时候,你都不知道是哪里出现问题。
## 并发
### 第66条 同步访问共享的可变数据
当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步,以确保线程安全,程序正确运行。使用 `synchronized` 和 `volatile` 这两个关键字来保证。
### 第67条 避免过度同步
上一条谈到要使用同步,这一条告诉我们不要过度使用。对于在同步区域的代码,千万不要擅自调用其他方法,特别是会被重写的方法,因为这会导致你无法控制这个方法会做什么,严重则有可能导致死锁和异常。通常,应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。
### 第68条 executor和task优先于线程
之所以推荐 `executor` 和 `task` 原因就在于这样便于管理。
### 第69条 并发工具优先于wait和notify
从`JDK5` 新增加的 `java.util.concurret` 并发包中提供了三个方面的并发工具:`Executor Framework`(这在上一条中有提到)、并发集合(`Concurrent Collection`)以及同步器(`Synchronizer`)。
随着 `JDK` 的发展,基于原始的同步操作 `wait` 和 `notify` 已不再提倡使用,因为基础,所以很多东西需要自己去保证,越来越多并发工具类的出现使得我们应该学习如何使用这些更为高效和易用的并发工具。
### 第70条 线程安全性的文档化
书中提到了很有意思的情景,有人会下意识的去查看 `API` 文档此方法是否包含 `synchronized` 关键字,如不包含则认为不是线程安全,如包含则认为是线程安全。实际上线程安全不能“要么全有要么全无”,它有多种级别:
- 不可变的——也就是有 final 修饰的类,例如 String、Long,它们就不用外部同步。
- 无条件的线程安全——这个类没有 final 修饰,但其内部已经保证了线程安全,例如并发包中的并发集合类,同样它们无需外部同步。
- 有条件的线程安全——这个有的方法需要外部同步,而有的方法则和“无条件的线程安全”一样无需外部同步。
- 非线程安全——这就是最“普通”的类了,内部的任何方法想要保证安全性就必须要外部同步。
- 线程对立的——这种类就可以忽略不计了,这个类本身不是线程安全,并且就算外部同样同样也不是线程安全的,`JDK` 中很少很少,几乎不计,自身也不会写出这样的类,或者也不要写出这种类。
可见队员线程是否安全不能仅仅做安全与不安全这种笼统的概念,更不能根据 `synchronized` 关键字来判断是否线程安全。你应该在文档注释中注明是以上哪种级别的线程安全,如果是有条件的线程安全不仅需要注明哪些方法需要外部同步,同时还需要注明需要获取什么对象锁。
### 第71条 慎用延迟初始化
延迟初始化又称懒加载或者懒汉式,这在单例模式中很常见。但是在并发环境下使用会出现线程安全错误,所以一定要利用 synchronized 进行同步:
public class Singleton {
private volatile Singleton singleton;
private Singleton() {
}
public Singleton getInstance() {
Singleton result = singleton;
if (result == null) {
synchronized (this) {
result = singleton;
if (result == null) {
singleton = result = new Singleton();
}
}
}
return result;
}
}
简言之,大多数域应该正常地进行初始化,而不是延迟初始化。
### 第72条 不要依赖于线程调度器
第69条说的是 `executor`和`task`优先于线程 ,此处又指不要依赖,实际上这里的不要依赖指的是不要将正确性依赖于线程调度器。例如:调整线程优先级,线程的优先级是依赖于操作系统的并不可取;调用 `Thread.yield` 使得线程获得 `CPU `执行机会,这也不可取。所以不要将程序的正确性依赖于线程调度器。
### 第73条 避免使用线程组
线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的,因此最好把线程组当成一个不成功的实验,当它们不存在。
## 序列化
### 第74条 谨慎地实现Serializable接口
**什么是序列化和反序列化?**
将一个对象编码成一个字节流的过程叫做 对象序列化(`serializing`);相反的处理过程就叫做 反序列化(`deserializing`)。
**序列化的作用?**
一旦对象被序列化后,他就可以从一台正在运行的虚拟机上传递到另一台虚拟机上,或者被存储到磁盘上。
**如何实现对象序列化?**
在 `Java` 中的类只需要实现 `Serializable` 接口即可被序列化。
**存在什么问题?**
它会带来以下几个代价:
1) 实现 `Serializable` 接口后就基本等同于将这个对象如同 `API` 一样暴露发布出去,这意味着你不可随意更改这个类,也就是大大降低了“改变这个类的实现”的灵活性。
2) 增加了出现 `Bug` 和安全漏洞的可能性,一个类的构造器往往是用来构建一个类约束关系。序列化机制是一种语言之外的对象创建机制,反序列化可以看作是一个“隐藏的构造器”,这也就是说如果按照默认的反序列化机制很容易不按照约定的构造器建立约束关系,以及很容易使对象的约束关系遭到破坏,以及遭受到非法访问。
3) 随着类的版本的改变,测试的负担增加。因为类的改变需要不断检查测试新版本与旧版本之间的“序列化-反序列化”是否兼容。
上面这三点代价,在有的条件下是值得的,例如很常见的如果一个类将加入到某个框架中,并且该框架依赖序列化来实现对象的传输和持久化这个时候就需要这个类实现 `Serializable` 接口。
另外书中举了`JDK`中为了继承而设计的实现了 `Serializable` 接口的类,`Throwable` 类实现了 `Serializable` 接口,所以 `RMI` 的异常可以从服务器端传到客户端。`HttpServlet` 实现了 `Serializable` 接口,因此会话状态可以被缓存。
尽管一个类要实现序列化很简单,但实现前一定要想好以及设计好这个类是否需要序列化,是否值得付出上面三个代价。
### 第75条 考虑使用自定义的序列化形式
目前没有用到,这里做下记录。
### 第76条 保护性的编写readObject方法
目前没有用到,这里做下记录。
### 第77条 对于实例控制,枚举类型优先于readResolve
目前没有用到,这里做下记录。
###第78条 考虑用序列化代理代理序列化实例
目前没有用到,这里做下记录。
>**参考:**
>[《Effective Java》——读后总结][1]
>[《Effective Java 2》——总结][5]
[1]:https://blog.csdn.net/sd09044901guic/article/details/80132651
[2]:https://www.cnblogs.com/ybbzbb/p/5519258.html
[3]:https://blog.csdn.net/zhanghua1068/article/details/82697174
[4]:http://c.biancheng.net/view/1026.html
[5]:https://www.cnblogs.com/markLogZhu/p/11398255.html
[6]:https://www.jianshu.com/p/ed3f8b0f0471