1.考虑用静态工厂方法代替构造函数
所谓静态工厂方法,实际上只是一个简单的静态方法,它返回的是类的一个实例。
静态方法的好处:
a.它与构造函数不同,它有名字,你可以把名字起的更易于阅读。注意:用静态工厂方法代替构造函数后,构造函数就成了private的了,当然如果你希望同时也提供公有的构造函数也是可以的。
b.它与构造函数不同,它每次被调用时,不要求非得创建一个新的对象。
c.它与构造函数不同,它可以返回类型的子类型对象。
缺点:
a.类如果不含有公有或者受保护的构造函数,就不能被继承。
b.静态工厂方法和其他静态方法一样,一般要在API文档中作出特别的说明。在没有强烈的需要下,你还是应该使用规范的构造函数。
常见的形式:
getInstance(),valueOf()
2.遇到多个构造函数参数时要考虑使用构建器
使用build模式,NutritionFacts cocaCola = new NutritionFacts.Build(240,8).calories(100).solium(35).
carbohydrate(27).build();//经典的一段程序
类似:
public class Person {
private int mAge;
private String mName;
private int mSex;
private Person(Builder builder) {
mAge = builder.mAge;
mName = builder.mName;
mSex = builder.mSex;
}
public static class Builder {
private int mAge;
private String mName;
private int mSex;
public Builder name(String name) {
mName = name;
return this;
}
public Builder age(int age) {
mAge = age;
return this;
}
public Builder sex(int sex) {
mSex = sex;
return this;
}
public Person build() {
return new Person(this);
}
}
}
3.用私有构造函数强化singleton属性
三种方法:
a.共有的静态成员变量;
b.私有静态变量,共有的静态工厂方法;
c.枚举,单元素的枚举类型已经成为实现Singleton的最佳方法;
public enum Elvis{
INSTANCE;
}
4.通过私有构造函数强化不可实例化的能力
工具类(util)就是一类不该被实例化类的代表。它们只是提供一些实用功能,实例化之是毫无意义的。
5.避免创建不必要的对象
a.String s1 = "Hello World !"; 而不是String s1 = new String("Hello World !");
b.对于提供了静态工厂方法和构造函数的非可变类,你通常可以利用静态工厂方法而不是构造函数,以避免重复创建对象。例如,Boolean.valueOf(String)几乎总是优先于构造函数Boolean(String)的。因为构造函数每次被调用都会创建一个新的对象,而静态工厂方法从来不要求这样做。
c.那些已知不会被修改的对象,它们一旦被计算出来就不再变化,可以随时使用,而不必每次使用前再次计算。例如,有一个SystemInfo类,里面有保存和取得系统信息的域和方法。为了简单我们认为只有一个用于获取和保持CPUID的方法和域。如果这个类写的不好的话,可能在每次调用 getCPUID()时,都要重新创建SystemInfo中的cpuId,重新获取,甚至重新创建一个SystemInfo类;而好的代码,是在第一次调用时获取,并保持起来,之后再次调用时只是返回就可以了。初始化后就不变的类String myCpu = SystemInfo.getInstance().getCPUID();
6.消除过期的对象引用
a.
public Object pop(){
if(size == 0){
throws new Exception();
}
Object result = elements[--size];
elements[size] = null;//清除过期引用
return result;
}
b.内存泄露的另一个常见的来源为缓存
c.内存泄露的第三个常见的来源为监听器和其它的回调
7.避免使用终结函数
终结函数(finalizer)通常是不可预测的,常常也是很危险的,一般情况下不是必要的。使用终结函数会导致不稳定的行为、更差的性能,以及带来移植性问题。不要把终结函数当做C++中的析构函数(destructors)的对应物。
原因:
a.若程序依赖方法被执行的时间点,那么这个行为在不同的JUM实现可能截然不同;
b.java语言规范不能保证终结方法会被执行;
c.被捕捉的异常在终结方法中被抛出,那么这种异常会被忽略;
d.使用终结方法具有非常严重的性能损失,使用终结方法来创建和销毁对象速度可能慢百倍还不止。
8.在改写equals的时候请遵守通用约定
Object是个即普通又特殊的类。说它普通是因为所有的类都是由它派生来的,说它特殊是因为它是所有其他类的父类必须先了解它才能写好你自己的类。
equals即“相等”之意,看起来改写很容易,但是实际中却容易犯错误。
equals的改写规范:
1)自反性:x.equals(x)一定为true
2)对称性:当且仅当x.dquals(y)为true;那么y.equals(x)也必须为true
3)传递性:如果x.equals(y)为true,y.equals(z);那么x.equals(x)也必须为true
4)一致性:对于任意引用值x和y,如果用于equals比较的对象信息没有被修改的话,那么多次调用x.dquals(y)返回的值是一致的
5)对于非空引用值x,x.equals(null)一定返回false
写好equals方法的“处方”:
1)使用==操作符检查“实参是否为指向对象的一个引用”
if (o==this)
return true;
2)使用instanceof操作符检查“实参是否为正确的类型”
if (!(o instanceof MyClass))
return false;
3)把实参转为正确的类型
MyClass mo = (MyClass)o;
4)对于该类中每一个“关键(significant)”域,检查实参中的域与当前对象中对应的域值是否匹配
if (!(this.field == null ? o.field == null : this.equals(o.field)))
//或者写成 if(!(this.field == o.field || (this.field != null && this.field.equals(o.field)))) 对于this.field和o.field通常是相同的对象引用,会更快一些。
return false;
//比较下一个field
//都比较完了
return true;
另外:
改写equals的同时,总是(必须)要改写hashCode方法,这是极容易被忽略的,有极为重要的
9.改写equals时总是要改写hashCode
一个很常见的错误根源在于没有改写hashCode方法。在每一个改写了equals的方法的类中,你必须也要改写hashCode方法。如果不这么做的话,就会违反Object.hashCode的通用约定:相等的对象必须具有相等的散列码,从而导致该类无法与所有基于散列值(hash)的集合类结合在一起正常运行,这样的集合类包括HashMap、HashSet、Hashtable。
现在知道了为什么一定要改写hashCode,该将如何改写了。书上又给出了一个“处方”:
1)把某个非零的常量值,比如17,保存为int result = 17;
2)对每个关键域(我觉得就是那些影响equals的域)如下处理:
2.1)int c; 并根据该域的类型:
2.1.1)如果该域f是boolean型,c = (f ? 0 : 1);
2.1.2)如果该域f是byte、char、short、int型,c = (int)f;
2.1.3)如果该域f是long型,c = (f ^ (f >>> 32));
2.1.4)如果该域f是float型,c = Float.floatToIntBits(f);
2.1.5)如果该域f是double型,c = (int)Double.doubleToLongBits(f);
2.1.6)如果该域f是一个对象,c = f.hashCode();
2.1.7)如果该域f是一个数组,遍历数组的每个元素,并按2.2)中的做法吧这些散列值组合起来;
2.2)result = 37 * result + c
3)return result
10.总是要改写toString
将你所关心的、感兴趣的部分toString了就可以了
11.谨慎地改写clone
克隆——是一个很让人“感兴趣”而又“颇有争议”的话题,无论是在生物界还是在代码的世界中。
于是clone方法的改写模板可以是这样的:
public class MyClass{
........
public Object clone() {
MyClass v = null;
try{
v = (MyClass) super.clone();
// 逐域克隆MyClass的非原生类型域
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError();
}
return v;
}
}
正如很多人不喜欢克隆,或者说clone有着很多争议那样,“如果你的代码中经常要使用clone,那么可以错略地说,你的设计是有问题的”。
最后再来说一下实践中比较实用的clone的替代品:
1) 如果一个类是可序列化的(实现系列化),那么可以将其先序列化,然后再反序列化已得到其副本。如果对象很大,甚至可以序列化到磁盘上。
2) 更普遍的,可以将一个对象转为字符流,然后在进而转到副本对象中。不依赖于类必须可系列化,所以更通用。
3) 如果是JavaBean(就像我的情况),可以使用org.apache.commons.beanutils.BeanUtils.cloneBean静态方法
12.考虑实现Comparable接口
compareTo方法在Object中并没有被声明,它是java.lang.Comparable接口中唯一的方法。实现这一接口,也可以说类具有了“内在排序能力”。
compareTo的用处除了我们可以直接使用外,很多API已经加以利用了,你只要提供具有内在排序能力(实现了Comparable接口)的对象就可以了,比如上
例子中的TreeSet。再例如,一个数组,如果放入数组的元素都是某个实现了Comparable接口的类的实例的话,那么给这个数组排序就可以简单到一句话:
Arrays.sort(myArray); // 使用Arrays的sort静态方法,其内部会使用实参的compareTo方法
13.使类和成员的可访问能力最小化
要想区别一个设计良好的模块与一个设计不好的模块,最重要的因素是,这个模块对于外部的其它模块而言,是否隐藏了内部的数据和其他的实现细节。换句话说,就是模块的设计者是否对其进行了良好的封装。
你应该总是尽量降低可访问性,否则你就有义务对你开放出去的东西负责到底。
对于顶层的(非嵌套的)类和接口,它们只有2种访问级别:包级私有(package-private)和公有(public)。如果选择了包级私有,那么它只是这个包的实现的一部分,而不是该包对外提供服务的API的一部分。在以后的版本中,你可以对它进行修改、替换甚至删除,而无需担心会伤害到现有的使用者。而如果选择的公有的,你就有义务永远支持它,以保持兼容性。
对于成员(域和方法),访问级别共为4种:
私有的(private)——只有该成员的顶层类中才能访问
包级私有(package-private)——本包内任何类都可访问
保护的(protected)——本包内的任何类和所在类的子类都可以访问
公有的(public)——任何地方都可以访问
14.支持非可变性
a.非可变类,这个名词可能并不是所有人都知道。所谓非可变类,就是具有如下特征的类:每个实例中包含的所有信息都必须在该实例被创建的时候就提供出来;并且在此对象的整个生命周期内固定不变。
b.Java平台中其实有很多非可变类,比如原语类型的包装类(Integer、Long、Short等)、String、BigDecimal、BigInteger等。
c.使用非可变类的理由是:比起可变类,它们更加易于设计、实现和使用,更不容易出错,更加安全。
d.对于非可变类,add方法必须要返回一个新的实例,而不再是在原来的基础上修改.
e.好处:简单、线程安全和节省系统内存开销(名字和生日)
f.缺点:对于每一个不同的值都要求一个单独的对象。创建这样的类可能会代价很高,特别是对于大型对象的情况。
g.和适用场景:综合这一点和前面的优点,我总结一下,就是在那些容易出现重复值,而且并经常会改变值的应用中,适合使用非可变类。
h.结论:所有的值类都应该考虑使用非可变类,除非有很好的理由要让一个类成为可变类,否则就应该是非可变的”,而其,“即便一个类不能被做成非可变的,你仍然应该尽可能地限制它的可变性。
15.复合优先于继承
a.继承(指的是子类扩展超类,并不包含接口)是实现代码重用的有力手段,但它并不总是完成这项工作的最佳工具。不适当地使用继承会导致脆弱的软件。
b.所谓复合就是在原来的“子类”中,不再继承“超类”,而是声明一个私有的域,并使用“超类”类型。
16.要么专门为继承而设计,并给出文档说明,要么禁止继承
a.在为了继承而设计类的时候,Cloneable和Serializable接口表现出了特殊的困难。如果一个类是为了继承而设计的,那么无论实现其中哪个接口都不是一个好主意,因为它们把一些实质性的负担转嫁到了扩展这个类的程序员的身上。
17.接口优于抽象类
a.接口和抽象类这二者比较起来,从特征来说,最明显的区别在于抽象类允许包含某些方法的实现,但是接口是不允许的。但是在使用方面还有一个更为突出的区别,就是抽象类是通过被子类继承来使用的;而接口是通过被实现来使用的。而Java是一种单继承机制的语言,所以就限制了抽象类的使用。(一个类只有一个超类或叫父类)当一个类已经继承了一个超类时,现在你再想因为一些原因让它再继承一个抽象类是不可能的(当然也有变通办法,就是先让这个抽象类继承那个超类,再把子类的超类改为这个抽象类。即原超类和子类的关系从父子关系变为了爷孙关系,中间插了这个抽象类)。这时候就能体现出接口的优势了——一个类可以实现多个接口(其实这也是Java之所以大胆地放弃多继承的原因)。
b.其它优点:已有的类可以很容易被更新,以实现新的接口,但如果是使用抽象类就麻烦了;接口是定义混合类型的理想选择;接口使得我们可以构造出非层次结构的类型框架。
18.接口只是被用于定义类型
当一个类实现了一个接口的时候,这个接口被用做一个类型(type),通过此类型可以引用这个类的实例。除此之外,其他目的而定义接口是不合适的。
19.优先考虑静态成员类
其实这一条的题目应该再多几个字“在四中嵌套类中,优先考虑静态成员类”,这样才更不容易产生歧义。
所谓嵌套类(nested class)是指定义在另一个类内部的类。嵌套类存在的目的应该只是为了它的外围类提供服务。共分为四种:
1)静态成员类(static member class)
2)非静态成员类(nonstatic member class)
3)匿名类(anonymous class)
4)局部类(local class)
其中后三者又被统称为内部类(inner class)
本条就是说在以上这四中,应该优先使用静态成员类。它是一种最简单的嵌套类。最好把它看成是一个普通的类,只是恰好被声明在了另一个类的内部而已,它可以访问外围类的所有成员(包括private)。
1)静态成员类的一种通常用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。
2)非静态成员类从语法的角度仅与静态成员类有一子之差——修饰符中不含static。尽管它们的语法非常相似,但用处有很大不同。非静态成员类得每一个实例都隐含着与外围类的一个外围实例紧密关联在一起,在其内部调用外围类的方法是可能的,也可以使用this引用。
3)匿名类不同于Java语言中的任何其他语法单元,它是没有名字的。它不是外围类的一个成员。它并不与其他成员一起被声明,而是在被使用的点上,同时被声明和实例化。因此匿名类只能被用在代码中它将被实例化的那个点上。还有,因为它没有名字,所以在它被实例化后就不能再对它进行引用。匿名类出现在表达式中间,所以它们应该非常简短,太长的话影响可读性。
4)局部类是四种嵌套类中最少使用的类。在任何“可以声明局部变量”的地方,都可以声明局部类,它也遵循着同样的作用域规则。
总之,四种嵌套类,各有各的用处。如果一个嵌套类需要在一个以上的地方使用,或者它太长了,不适合放在一个方法(注意:不是类)内部,那么应该使用成员类。如果成员类的每个实例都需要一个指向外围实例的应用(需要使用this),则他成员类做成非静态的;否则就应该做成静态的。