Java高效编程2

对象的通用方法
+ - 重载equals时要遵守通用约定
+ - 如果满足下列条件,就不要重载equals
每个类实例本质上是唯一的.
不关心类是否提供了"逻辑意义的等同"(logical equality)测试.例如java.util.Random本来可以重载equals方法,用以检查两个Random实例是否会产生相同的随机数序列,但设计者不认为客户会需要或想要这个功能.这种情况下,使用从Object继承的equals实现就够了.
超类已经重载了equals,而从超类继承的行为适合该类.
类是私有的或包内私有(package-private)的,而且可以确定它的equals方法永远不会被调用.
当类有逻辑上的等同意义而不仅仅是对象意义上的等同,而且超类没有重载equals方法以实现期望的行为,这时才需要重载.
+ - equals方法实现了相等关系(equivalence relation)
自反性(reflective):对于任意的引用值x,x.equals(x)总是返回true.
对称性(symmetric):对于任意的引用值x、y,如果y.equals(x)返回true,x.equals(y)总返回true.
传递性(transitive):对于任意的引用值x、y、z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)总是返回true.
一致性(consistent):对于任意的引用值x、y,如果对象中用于equals比较的信息没有修改,那么对x.equals(y)的多个调用,要么一致为true,要么一致为false.
对于任何非空引用值x,x.equals(null)总是返回false.
+ - 为实现高质量的equals方法,下面提供一些方法
用"=="操作符检查是否参数是对该对象的引用.
用instanceof操作符检查是否参数是正确的类型.
public boolean equals(Object o) {
if(!(o instanceof SomeClass))
return false;
...
}
把参数映射到正确的类型.
对类中每一个"主要的"(significant)域,检查是否参数中的域与对象中的相应的域匹配.
完成equals方法时,问自己3个问题:它是否是对称的、传递的、一致的.
+ - 实现equals方法应该注意的地方
在重载equal方法时要重载hashCode方法.
不要使自己聪明过头.把任何的同义形式考虑在比较的范围内一般是糟糕的想法,例如File类不应该与指向同一文件的符号链接进行比较,实际上File类也没有这样做.
不要设计依赖于不可靠资源的equals方法.
不要将equals声明中的Object替换为其他类型.程序员编写出形如下面所示的equals方法并不少见,它会让人摸不清头脑:所设计方法为什么不能正确工作:
public boolean equals(Myclass o) {
...
}
问题出在这个方法没有重载(override)参数为Object类型的Object.equals方法.而是过载(overload)了它.这在正常的equals方法中,又提供了一个"强类型"的equals方法.
+ - 重载equals时永远要重载hashCode
一定要在每一个重载了equals的类中重载hashCode方法.不这样做会违背Object.hashCode的一般约定,并导致你的类与所有基于散列的集合一起作用时不能正常工作,这些集合包括HashMap、HashSet和Hashtable.
不重载hashCode方法违背了java.lang.Object的规范:相等的对象必须有相等的散列码.两个截然不同的实例根据类的equals方法也许逻辑上是等同的,但对于Object类的hashCode方法,它们就是两个对象,仅此而已.因而对象的hashCode方法返回两个看上去是随机的数值,而不是约定中要求的相等的值.
好的hash函数倾向于为不相等的对象生成不相等的hash码.理想的情况下,hash函数应该把所有不相等的实例的合理集合均一地分布到所有可能的hash值上去.达到理想状态很难,但是下面有一种相对合适的方法
1.保存某个非0常数如17,到名为result的int类型变量中
2.对对象中每个"主要域"f,(每个域由equals方法负责),做下面的工作
a.为域计算int型的hash码c
i.如果域是boolean型,计算(f?0:1).
ii.如果域是byte型、char型、short型或int型,计算(int)f.
iii.如果域是long型,计算(int)(f^(f>>>32)).
iv.如果域是float型,计算Float.floattoIntBits(f).
v.如果域是double型,计算Double.doubleToLongBits(f),然后如2.a.iii所示,对long型结果进一步处理.
vi.如果域是对象引用,而且这个类的equals方法又递归地调用了equals方法对域进行比较,那么对这个域递归地调用hashCode方法.如果需要一种更复杂的比较方式,那么先为这个域计算出"范式表示",然后在该"范式表示"上调用hashCode方法.如果域为null,则返回0.
vii.如果域是数组,则把每个元素作为分离的域对待.即递归地使用这些规则,为每个"主要元素"计算hash码.然后用2.b所示方法复合这些值.
b.把步骤a中计算出的hash码c按如下方式与result复合: result = 37*result + c;
3.返回result.
4.完成hashCode方法后,测试是否相同的实例会有相同的hash码,如果不是,找到原因,修正问题.
+ - 永远要重载toString
为类提供一个好的toString实现可以使类使用起来更加赏心悦目.
实际使用中,toString方法应该返回包含在对象中的所有令人感兴趣的信息.
无论是否指明格式,都要把你的意图清楚地文档化出来.
+ - 谨慎的重载clone
为了实现Cloneable接口,会产生一种古怪的机制:不通过调用构造函数却创建了一个对象.
实现对像拷贝的精巧的方法是提供一个拷贝构造函数(copy constructor).拷贝构造函数及它的静态工厂变形与Cloneable/clone方法相比有很多好处
它们不依赖于那种有风险的蹩脚的对象创建机制;
不需要遵守由糟糕的文档规范的规约;
不会与final域的正常使用产生冲突;
不要求客户不必要地捕获被检查的异常;
给客户提供了一种类型化的对象.
方法
+ - 检查参数的有效性
如果方法没有对参数做检查,会出现几种情形.
方法可能在执行中间失败退出并给出含糊的异常.
更差的是,方法能正常返回,并计算了错误结果.
对那些不被方法使用但会被保存以供使用的参数,检查有效性尤为重要.
一种重要的例外是有效性检查开销高,或者不切实际,而且这种有效性检查在计算的过程中会被隐式地处理的情形.
总的来说,每次在设计方法或设计构造函数时,要考虑它们的参数有什么限制.要在文档中注释出这些限制,并在方法体的开头通过显示的检查,对它们进行强化.养成这样的习惯是重要的,有效性检查所需要的不多的工作会从它的好处中得到补偿.
+ - 使用保护性拷贝
必须在客户会使用一切手段破坏类的约束的前提下,保护性地设计程序.
下面的类的目的是表示非可变的时间周期:
//Broken "immutable" time period class
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(start.compareTo(end) >0 )
throw new IllegalArgumentException(start+" after "+end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
...
}
乍看上去,这个类是非可变的,并通过执行周期的起始点不会落在周期终止点之后的判断,增强了类的约束.然而,如果Date是可变的,这种约束很容易被违背:
// Attack the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start,end);
end.setYear(78); //Modifies internals of p!
为了保护Period实例的内部细节免于这种攻击,对构造函数的每一个可变参数使用保护性拷贝是必要的.
使用副本代替初始值作为Period实例的组件:
//Repaired constructor - make defensice copies of parameters
public Period(Date start, Date end){
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if(this.start.compareTo(this.end)>0)
throw new IllegalArgumentException(start+" after "+end):
}
保护性拷贝要在参数的有效性检查的前面,并且有效性检测要在副本而不是初值上执行.
尽管替代构造函数成功防止了前面的攻击,但改变一个Period实例仍然是可能的,因为访问器对它的可变的内部细节提供了访问能力.
//Second attack on the internals of a Period instance
Date start = new Date();
Date end = new Date();
Period p = new Period(start,end);
p.end().setYear(78); //Modifies internals of p!
为了防止第二种攻击,简单地修改访问器,返回可变内部域的保护性拷贝即可:
//Repair accessors - make defensive copies of internal fields
public Date start() {
return (Date) start.clone();
}
public Date end() {
return (Date) end.clone();
}
只要可能,就应该使用非可变对象作为对象的组件,以便不再关心保护性拷贝问题.
+ - 认真设计方法签名
认真地给方法选名字.名字要永远遵守标准的命名惯例.
不要过于追求提供便利的方法.对接口这一点是千真万确的,接口中方法太多会使实现程序和用户使用时变得复杂.
避免长参数列表.三个参数的使用就该作为最大值.类型相同的长参数序列尤其有害.
有两种技术可以大幅缩短常参数列表.一种技术是将一个方法分成多个方法实现,其中每个方法仅需要参数类表的一个子集.
第二种技术是创建助手类,用来保持参数集合.
在参数类型的使用上,接口优于类.
谨慎地使用函数对象.
+ - 谨慎地使用过载
下面是一个意图良好的尝试,按照是否是集、列表还是其他种类的集合把集合分类:
//Broken - incorrect user of overloading!
public class CollectionClassifier {
public static String classify(Set s) {
return "Set";
}
public static String classify(List l) {
return "List";
}
public static String classify(Collection c) {
return "Unkown Collection";
}
public static void main(String[] args) {
Collection[] tests = new Collection[] {
new HashSet(),
new ArrayList(),
new HaspMap().values()
};
for(int i=0;i<tests.length;i++)
System.out.println(classify(tests[i]));
}
}
也许认为这个程序会依次打出Set、List和Unkown Collection,但实际是打印3次Unkown Collection.这是因为classify方法被过载了,选择使用哪个调用的决定是在编译期做出的.
改正的方法是用一个显式instanceof测试实例的方法代替classify中的三个过载方法:
public static String classify(Collection c) {
return (c instanceof Set? "Set" :(c instanceof List ? "List" : "Unkown Collection"));
}
一种安全、保守的策略是永远不要导出两个具有相同数目参数的过载方法.
+ - 返回0长度的数组而不是null
下面的方法比较常见:
private List CheesesInStock = ...;
public Cheese[] getCheeses() {
if(cheesesInStock.size() == 0)
return null;
...
}
没有理由需要对无奶酪可买这种情况做特殊的处理.着需要客户方提供额外代码处理null的返回值,例如:
Cheese[] cheeses = shop.getCheeses();
if (cheeses != null && Array.asList(shop.getCheeses()).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
而不是:
if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
几乎每次返回null而不是返回0长度数组的方法时,都需要这种多余地处理.返回null是易出错的,因为写代码的程序员可能会忘记设计处理返回null的特殊情形的代码.
正确的做法是:
private List cheeseInStock = ...;
private final static Cheese[] NULL_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return (Cheese[]) cheesesInStock.toArray(NULL_CHEESE_ARRAY);
}
+ - 通用编程
+ - 最小化局部变量作用域
通过最小化局部变量的作用域,可以增加代码的可读性和可维护性,减少出错可能.
最小化局部变量的最有效的方式是在它第一次被使用时声明.
几乎每一个局部变量声明都应该包含一个初始化器(initializer).
最小化局部变量的最后一个技术是使方法小而集中.
需要确切答案时,不要使用float或double类型
float和double类型特别不适合货币计算.
假设手中有$1.03,花掉0.42后还剩多少钱呢?
System.out.println(1.03 - .42);
不幸的是,它会打印0.6100000000000001.
解决这个问题的正确方法是使用BigDecimal、int和long类型进行货币计算.
使用BigDecimal有两个缺点.
它不如使用算术类型方便.
速度慢.
+ - 尽量避免使用串
串是值类型的糟糕的替代物.
串是可枚举类型的糟糕的替代物.
串是集合类型的糟糕的替代物
串是capabilities(能力表)的糟糕的替代物
+ - 了解串并置的性能
为连接n个串重复地使用串合并操作符需要n的二次方时间.
为了获得可接受的性能,可以使用StringBuffer代替String.
通过接口访问对象
如果存在合适的接口类型,那么参数、返回值、变量和域应该用接口类型声明.
应该养成下面这样的程序习惯:
//Good - uses interface as type
List subscribers = new Vector();
而不要这样做:
//Bad - uses class as type!
Vector subscribers = new Vector();
如果养成了使用接口作为类型的习惯,程序就会有更好的扩展性.当希望转换实现时,需要做的全部工作就是改变构造函数中类的名字.
谨慎地做优化
人们通常都把计算机的罪归咎于效率问题(甚至是不必要的获得的效率),而不去怀疑任何其他的原因mm甚至包括盲目地做傻事.mmWilliam A.Wulf
不要计较微小效率的得失,在97%的情况下,不成熟的优化是一切罪恶的根源. mmDonald E.Knuth
做优化时,要遵循两条原则:
原则1 不要做优化
原则2(仅对专家) 还是不要做优化mm也可以这么说:在绝对清楚的、未经优化的方案之前,不要做优化.
mmM.A.Jackson
异常
串行化
线程

转载于:https://www.cnblogs.com/521taobao/archive/2012/03/17/2402515.html

猜你喜欢

转载自blog.csdn.net/weixin_34336526/article/details/93355918