《Effective Java》这本书介绍了Java编程中78条极具使用价值的经验规划,包括创建和销毁对象,类和接口,泛型枚举,异常,并发,序列化等等内容,可以针对性地有选择阅读。本书主要针对Java,具体到android中要自己考虑实际应用。
第二章 创建和销毁对象
第5条:避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象
反例一:
String string =newString("stringette”);
传递给String构造器的参数"stringent”本身就是一个String实例
String string ="stringent";
只用了一个String实例,而不是每次都创建一个新的实例
在同一个java虚拟机中的代码,只要它们包含相同的字符串常量,该对象就会被重用
除了重用不可变的对象外,也可以重用那些已知不会被修改的可变对象
反例二:
现在有一个类是Person,并有一个isBabyBoomer方法,用来检验这个类的每个对象是否为一个“baby boomer”(生育高峰期出生的小孩),换句话说,就是检验这个人是否出生于1946年至1964年期间。
public classPerson {
private finalDatebirthDate;
private booleanisBabyBoomer() {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY,1,0,0,0);
Date boomEnd = gmtCal.getTime();
returnbirthDate.compareTo(boomStart) >=0&&
birthDate.compareTo(boomEnd) <0;
}
}
这里涉及可变的Date对象,它的值一旦计算出来之后就不再变化。isBabyBoomer每次被调用的时候,都会新建一个Calendar,一个TimeZone和和两个Date实例,这是不必要的。
private finalDatebirthDate;
private static finalDateBOOM_START;
private static finalDateBOOM_END;
static{
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
BOOM_START= gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY,1,0,0,0);
BOOM_END= gmtCal.getTime();
}
private booleanisBabyBoomer() {
returnbirthDate.compareTo(BOOM_START) >=0&&
birthDate.compareTo(BOOM_END) <0;
}
改进后只在初始化的时候创建Calendar,TimeZone和Date实例各一次,而不是在每次调用isBabyBoomer时候都创建这样的实例。如果isBabyBoomer被频繁调用,这种方法将显著提升性能。
反例三:
自动装箱方法
Long sum =0L;
for(longi =0; i
sum += i;
}
System.out.println(sum);
这段程序答案是正确的,但是比实际情况更慢一点,因为打错了一个字符。
应该优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
当你应该重用现有对象的时候,请不要创建新的对象
第三章 对于所有对象都通用的方法
第8条:覆盖equals时请遵守通用约定
第9条:覆盖equals时总要覆盖hashCode
第10条:始终要覆盖toString
第11条:谨慎地覆盖clone
尽管Object是一个具体类,但是设计它主要是为了扩展。它所有的非final方法(equals, hashCode, toString, clone和finalize)都有明确的通用约定,因为它们被设计成是要被覆盖的。任何一个类,它在被覆盖的时候,都有责任遵守这些通用约定;如果不能做到这一点,其它依赖这些约定的类(HashMap和HashSet)就无法一起正常工作。
public static classContactInfo {
publicStringname="";
publicStringnumber="";
publicStringavatarUriStr="";
publicObjectcustomInfo= Boolean.FALSE;
private volatile inthash code;
}
实现高质量equals方法的诀窍:
1.使用==操作符检查参数是否为这个对象的引用
2.使用instanceof操作符检查参数是否为正确的类型
3.把参数转换成正确的类型
4.对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配
@Override
public booleanequals(Object obj) {
if(obj ==this) {
return true;
}
if(!(objinstanceofContactInfo)) {
return false;
}
ContactInfo contactInfoObj = (ContactInfo)obj;
returnTextUtils.equals(name, contactInfoObj.name)
&& TextUtils.equals(number, contactInfoObj.number);
}
原则:
自反性:对象必须等于自身,x.equals(x) = true
对称性:任何两个对象对于”它们是否相等“必须保持一致 y.equals(x) = x.equals(y)
传递性:A对象等于B对象,B对象等于C对象,那么A对象一定等于C对象 x.equals(y) = true y.equals(z) = true, so x.equals(z) = true
一致性:任何两个相等的对象在没有被修改的情况下必须始终相等
非空性:所有的对象必须不等于null
每个覆盖了equals方法的类中,也必须覆盖hashCode方法,不然违反Object.hashCode通用约定,HashMap, HashSet等出bug
@Overridepublic inthashCode() {
intresult =hashcode;
if(result == 0) {
result = 17;
result = 31 * result +name.hashCode();
result = 31 * result +number.hashCode();
hashcode= result;
}
returnresult;
}
高质量散列函数的解决方法:
1.把某个非0常数值,例如17(奇素数),保存在一个名为result的int类型的变量中
2.对于对象中每个equals方法涉及的关键域f:
a.为该域计算int类型的散列码c:
boolean类型的域(如名叫f) 计算(f?1:0)
byte,char,short,int类型的域 计算 (int)f
long类型的域 计算 (int)(f^(f>>>32))
float类型的域 计算 Float.floatToIntBits(f)
double类型的域 计算 Double.doubleToLongBits(f)
引用类型的域 使用 引用类型的hashCode方法得到的散列值
数组类型的域 把数组中的每个元素当成一个关键域,计算出散列值。
b.对于每一个关键域计算出来的散列值 (如名叫c)
result = result * 31 + c;
3.最后返回这个result整数
原则:为不相等的对象产生不相等的散列码
@Override
publicString toString() {
returnname+number;
}
clone方法:
x.clone().getClass() == x.getClass()在父类子类的情况会出现歧义,子类的getClass和父类的getClass应该相等吗?
不建议覆盖,对于一个专门为继承而设计的类,如果你未能提供行为良好的受保护的clone方法,它的子类就不可能实现Cloneable接口。
第四章 类和接口
第14条:在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就提供访问方法;
如果公有类暴露了它的数据域,在将来想改变将会很困难,因为访问它的代码已经遍布各处了。
如果类是包级私有的,或者是私有的嵌套类,直接暴露它的 数据域并没有本质的错误。
这些代码被限定在类的内部,不必改变包之外的代码就可以修改
反例:
Java类库中的java.awt包中Point和Dimension类,特别是Dimension类,内部数据的暴露造成了严重的性能问题,而且这个问题至今仍然存在。
第16条:复合优先于继承
不当地使用继承会让代码变得很脆弱,因为继承打破了封装性。子类是依赖于父类的特定功能而实现的,但父类有可能随着发行版本而变化,如果真的发生了变化,子类就有可能遭到破坏。
解决方案:复合,不用扩展现有类,而是把现有类作为新类的一个实例。
第18条:接口优于抽象类
接口和抽象类最明显的区别:
抽象类允许包含某些方法的实现,接口不允许。
更加重要的区别:
为了实现抽象定义的类型,类必须成为抽象类的一个子类。
任何一个类,只要它定义了所有必要的方法,并且遵守通用约定,它就被允许实现一个接口,不管这个类是处于类层次中的哪一个位置。
因为Java只允许单继承,所以,抽象类作为类型定义受到了极大的限制。而实现新接口则没有限制。
但是公有接口的设计必须非常谨慎,接口一旦被公开发行并被广泛实现,再想改变这个接口几乎是不可能的。
第五章 泛型
第25条:列表优先于数组
数组是协变的(covariant),泛型是不可变的(invariant)
//编译可通过,运行crash
Object[] objectArray =newLong[1];
objectArray[0] ="String”;
//编译报错
List list =newArrayList();
list.add("string”);
泛型是编译时类型安全的,数组是运行时类型安全的,数组有缺陷
第六章 枚举和注解
无
第七章 方法
第39条:必要时进行保护性拷贝
JAVA相比C和C++而言是一门安全的语言,它对于缓冲区溢出、数组越界、非法指针以及其他内部破坏的错误都自动免疫。在一门安全的语言中,在设计类的时候,可以确切地知道,无论系统的其他部分发生什么事情,这些类的约束都可以保持为真。对于那些把所有内存当成一个巨大数组来看待的语言来说,这是不可能的。但即使在安全的语言中,仍然需要适当的保护。
public class Period{
private final Date startTime;
private finale Date endTime;
public Period(Date startTime , Date endTime){
if(startTime.compareTo(endTime) > 0){
throw new IllegalArgumentException(“startTime after endTime !”);
}
this.startTime = startTime;
this.endTime = endTime;
}
pubilc Date start(){
return this.startTime ;
}
public Date end(){
return this.endTime ;
}
}
这个类貌似是一个不可变类 ,因为startTime和endTime域都是final的,还加了保护开始时间必须在结束时间之前,但是它并不是一个严格的不可变类,因为Date类并不是一个不可变类。
这样就会出问题:
攻击一:
Date startTime = new Date();
Date endTime = new Date();
Period per = new Period(startTime , endTime );
endTime.setYear(78);
为了保护内部信息避免受到攻击,对于构造器内的每个可变参数进行保护性拷贝是必要的,并且使用备份对象作为Period实例的组件,而不使用原始的对象:
防御一:
public Period(Date startTime , Date endTime ){
this.startTime =new Date (startTime.getTime());
this.endTime =new Date(endTime.getTime());
if(this.startTime.compareTo(this.endTime) > 0){
throw new IllegalArgumentException(“startTime after endTime !”);
}
}
保护性拷贝是在检查参数的有效性之前进行的,这是十分必要的
攻击二:
Date startTime = new Date();
Date endTime = new Date();
Period per = new Period(startTime , endTime );
per.end().setYear(78);
还需要修改对成员变量的访问,增加保护性拷贝:
防御二:
public Date start(){
return new Date(startTime.getTime());
}
public Date end(){
return new Date(endTime.getTime());
}
在采用了新的访问器和新的访问方法之后,Period真正不可变了。
第43条:返回零长度的数组或者集合,而不是null
private final List cheesesInStock = …;
public Cheese[] getCheeses() {
if (cheesesInStock.size() == 0) {
return null;
}
}
把没有奶酪可买的情况当做是一种特例,这是不合常理的。这样做客户端必须有专门的逻辑去处理null值。很容易出错。
有观点认为返回null比零长度数组更好,因为它避免了分配数组所需要的开销,但是这个级别上的性能担心是多余的。
第八章 通用程序设计
第45条:将局部变量的作用域最小化
将局部变量的作用域最小化可以增加代码的可读性和可维护性,并降低出错的可能性
第48条:如果需要精确的答案,请避免使用float和double
float和double是二进制浮点运算,并没有提供完全精确的结果,不应该被用于需要精确结果,尤其是货币运算的情况。
例如,假设你的口袋里有$1,看到货架上有一排糖果,标价分别为$0.1, $0.2, $0.3等等,一直到$1, 你打算从标价为$0.1的糖果开始买起,每种买一颗,一直到不能支持货架上下一钟糖果的价格为止,那么你可以买多少可糖果呢?
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for(double price = .10; funds >= price; price += .10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
这段程序的运行结果是只能买3颗糖果,并且还剩下$0.39999999999
解决问题的正确办法是使用BigDecimal、int或者long进行货币计算。
第49条:基本类型优先于装箱基本类型
Java有一个类型系统由两部分组成,包含基本类型,int、double和boolean,和引用类型,如String和List。每个基本类型都有一个对应的引用类型,称作装箱基本类型,例如Interger、Double和Boolean。
基本类型和装箱类型的区别:
1.基本类型只有值,装箱类型有与值不同的同一性(==)。
两个装箱基本类型可以有相同的值和不同的同一性。
2.基本类型只有功能完备的值,装箱类型还有null
3.基本类型比装箱类型更节省时间和空间
问题1:
Comparator naturalOrder =newComparator() {
publicintcompare(Integer first, Integer second) {
returnfirst < second ? -1 : (first == second ? 0 : 1);
}
};
如果打印naturalOrder.compare(new Integer(42), new Integer(42))的值,结果不是期望的0,而是1,表明第一个Integer值大于第二个。
问题出现在执行first == second,它在两个对象引用上执行同一性比较,如果first和second引用表示同一个int值的不同Integer实例,这个比较操作会返回false,比较器会错误地返回1,对装箱基本类型运用==操作符几乎总是错误的。
修正:
Comparator naturalOrder = new Comparator() {
public int compare(Integer first, Integer second) {
int f = first;
int s = second;
return f < s ? -1 : (f == s ? 0 : 1);
}
};
问题2:
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if(42 == i)
System.out.println("Unbelievable");
}
}
它不是打印出Unbelievable,而是抛出NullPointException异常,问题在于i是个Integer,不是int,它的初始值是null而不是0,当计算(i == 42)时,将Integer和int进行比较,装箱基本类型自动拆箱,如果null对象被自动拆箱,就只能得到Nu'llPointException了。
修正的方法把i声明为int。
总结:
装箱基本类型适用于集合中的键值对以及泛型中,在其他可以选择的情况下,基本类型要优于装箱基本类型。
第53条:接口优于反射机制
反射机制允许一个类使用另一个类,即使当前者被编译的时候后者还根本不存在,然而,这种能力也要付出代价:
1.丧失了编译时类型检查的好处
2.执行反射访问所需要的代码非常笨拙和冗长
3.性能损失 2~50倍
如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。
最好的解决方式是以反射的方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。如果适当的构造器不带参数,甚至根本不需要使用java.lang.reflect包;Class.newInstance方法就已经提供了所需的功能。
下面的程序创建一个Set实例,它的类是由第一个命令行参数指定的。该程序把其余的命令行参数插入到这个集合中,然后打印该集合。如果是HashSet以随机的方式打印出来,如果是TreeSet按照字母顺序打印出来的程序:
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName(args[0]);
} catch(ClassNotFoundException e) {
System.out.println("Class not found");
System.exit(1);
}
Set s = null;
try {
s = (Set) c.newInstance();
} catch(IllegalAccessException e) {
System.out.println("Class not accessible");
System.exit(1);
} catch(InstantiationException e) {
System.out.println("Class not instantiable");
System.exit(1);
}
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
缺点:
1.需要加三个cache保护
2.代码冗长
第54条:谨慎地使用本地方法
Java Native Interface(JNI)允许java应用程序调用本地方法(native method),指用本地程序设计语言(C或C++)来编写的特殊方法。
使用本地方法来提高性能的做法不值得提倡。现在JVM越来越快,对于大多数任务,即使不使用本地方法也可以获得与之相当的性能。
使用本地方法有一些严重的缺点:
1.因为本地方法不是安全的,使用它不能避免受内存毁坏错误的影响。
2.本地方法与平台相关,使用本地方法不再可以自由移植。
3.更难调试
4.在进入和退出本地代码时会产生固定开销
5.如果本地代码只做少量工作,反而会降低性能
6.某些需要胶合代码的本地方法编起来难以阅读
总而言之,使用本地方法前务必三思。
第55条:谨慎地进行优化
优化的三条格言:
1.很多计算上的过失都归咎于效率(没有必要达到的效率),而不是任何其他的原因,包括盲目地做傻事.
2.不要去计较效率上的小小得失,在97%的情况下,不成熟的优化才是一切问题的根源
3.优化方面我们遵守两条原则:
a.不要进行优化
b.针对于专家,还是不要进行优化,也就是说,在你还没有绝对清晰的优化方案之前,请不要优化.
它们讲述了关于优化的深刻真理:优化的弊大于利。
不要为了性能而牺牲合理的结构,要努力写好的程序而不是快的程序。
如果好的程序不够快,它的结构将使它可以得到优化。但是遍布全局并且限制性能的结构缺陷几乎是不可能被改正的,除非重写。
反例:
java.awt.Component类中的getSize方法,Dimension实例是直接暴露的,由于Dimension是可变的,迫使与它有关的任何实现都必须分配一个新的实例,过多的分配会导致性能问题。
解决方案:
1.Dimension设置为不可变
2.用两个方法来替换getSize方法,分别返回Dimension对象的单个基本组件
但是之前设计失误的性能影响一直存在
“不要进行优化”:
试图优化通常对性能没有明显影响,有时甚至更差。主要原因是要猜出程序哪里花时间并不容易,要多多使用剖析工具。优化的第一个步骤是检查所选择的算法,再多的底层优化也无法弥补算法选择的不当。必要时重复这个过程,每次改变完之后都要测试性能,直到满意为止。
总而言之,不要费力去编写快速的程序,应该努力编写好的程序,速度自然会随之而来。