首页 > 代码库 > effective java
effective java
effective java
1.考虑用静态工厂方法代替构造函数
对于一个类,为了让客户获得他的一个实例,最通常的方法是提供一个共有的构造函数。
实际上还有另外一种技术,尽管较少为人所知,但也应该成为每个程序员的工具箱中的一
部分,类可以提供一个公有的 静态工厂方法 。所谓静态工厂方法,实际上只是一个简单
的静态方法,他返回的是类的一个实例。
类可以提供一些静态工厂方法,来代替构造函数,或者同时也提供好一些构造函数。用静态
工厂来代替公有的构造函数,既有好处,也有不足之处。
静态工厂方法的一个好处是,与构造函数不同,静态工厂方法具有名字,返回值等,可以通过
名字就能知道代码的含义。
静态工厂方法的第二个好处是,与构造函数不同,他在被调用时,并不要求非得创建一个新的对象。
静态工厂方法的第三个好处是,与构造函数不同,他们可以返回一个原返回类型的子类型的对象。
静态工厂方法的主要缺点是,类如果不含公有的或者受保护的构造函数,就不能被子类化。
静态工厂方法的第二个缺点是,他们和其他的静态方法没有任何区别。
2.使用私有构造函数强化singleton属性
singleton是指这样的类,他只能实例化一次,singleton通常被用来代表那些本质上具有唯一性的
系统组件。
实现singleton有两种方法:这两种方法都要把构造函数保持私有的,并且提供一个静态成员,以便
允许客户能够访问该类的唯一实例,在第一种方法中,公有静态成员是一个final域:
public class Elvis{
public static final Elvis INSTANCE = new Elvis();
private Elvis(){
}
}
私有构造函数仅被调用一次,用来实例化公有的静态final域,由于缺少公有或者受保护的构造函数,所以保证了全局唯一性,一旦类被实例化之后,只有一个实例存在--不多也不少。
客户的任何行为都不会改变这一点。
第二种方法提供了一个公有的静态工厂方法,而不是共有的静态final域:
public class Elvis{
private static final Elvis INSTANCE = new Elvis();
private Elvis(){
}
public static Elvis getInstance(){
return INSTANCE;
}
}
所有对于静态方法的调用,杜辉返回同一个对象引用,所以,不会有别的实例被创建。
第一种方法的主要好处在于,组成类的成员的申明很清楚的表明了这个雷是一个singleton.
公有的静态域是final的,所以该域将总是包含相同的对象引用,第一种方法可能在性能上稍微
领先,但是在第二种方法中,一个优秀的jvm实现应该能够通过将静态工厂方法的调用内联化,来消除这种差别。
第二种方的主要好处在于,他提供了灵活性,在不改变API的前提下,允许我们改变想法,把该类做成singleton,
或者不做成singleton,singleton的静态工厂方法返回该类的唯一实例,但是他也容易被修改,比如说,为每一个
调用该方法的线程返回一个唯一的实例。
总而言之,如果你确信该类将永远是一个单例,那么使用第一种方法是有意义的,如果你希望留有余地,那么请使用
第二种方法。
为了使一个单例类变成可序列化,仅仅在申明中加上“ implements Serializable” 是不够的,为了维护单例性,你必须也要提供
一个readResolve方法,否则的话,一个序列化的实例在每次反序列化的时候,都会导致创建一个新的实例。
3.通过私有构造函数强化不可实例化的能力
企图通过将一个类做成抽象类来强化该类不能被实例化,这个是行不通的,该类可以被子类化,而子类可以被实例化。我们只要
让这个类包含单个显示的私有构造函数,那么他就不能被实例化了。
4.避免创建重复对象
5.消除过期的对象引用
6.避免使用终结函数
7.在改写equals的时候请遵守通用约定
一个类的每一个实例本质上都是唯一的。
不关心一个类是否提供了逻辑相等的测试功能。
超类已经改写了equals,从超类继承过来的行为对于子类也是合适的。
一个类时私有的,或者是包级私有的,并且可以确定他的equals方法永远也不会被调用。尽管如此,在这样的情况下,应该要改写equals方法,以免万一有一天他会被调用
public boolean equals(Object o){
throw new UnsupportedOperationException();
}
那么,什么时候应该改写equals呢?当一个类有他自己特有的逻辑相等概念,而且超类没有改写equals以实现期望的行为,这是我们需要改写,这通常适合于值类的情形。
比如Integer或者Date,程序员在利用equals方法来比较两个指向值对象的引用的时候,希望知道他们逻辑上是否相等。而不是他们是否指向同一个对象。
在改写equals方法的时候,你必须要遵守他的通用约定,下面是约定的内容,来至Object:
(1)equals方法实现了等价关系:
自反性。对于任意的引用至x,x.equals(x)一定为TRUE。
对称性。对于任意的引用值x和y,当且仅当x.equals(y)返回TRUE,那么y.equals(x)也一定返回TRUE。
传递性。对于任意的引用值x,y和z,如果x.equals(y)返回TRUE,并且y.equals(z)也返回true,那么x.equals(z)也一定返回true。
一致性。对于任意的引用值x和y,如果用于equals比较的对象信息没有被修改的话,那么多次调用要么一致返回TRUE,那么一致的返回FALSE。
对于任意的非空引用值x,x.equals(null)一定返回false.
下面是为实现高质量equals方法的一个处方:
1.使用==操作符检查“实参是否为指向对象的一个引用”。如果是的话,则返回TRUE。这只不过是一种性能的优化,如果比较操作有可能非常耗时的话,这样做的值得的。
2.使用instanceof操作符检查实参是否正确的类型。
3.把实参转换成正确的类型。因为前面已经有了instanceof测试,所以这个转换可以确保成功。
4.对于该类中每一个关键域,检查实参中的域与当前对象中对应的域值是否匹配
5.当你编写完成了equals方法后,应该问自己三个问题:他是否对称的,传递的,一致的,如果答案是否定的,那么请找到这些特性未能满足的原因,在修改代码。
当你改写equals的时候,总是要改写hashcode.
不要试图让equals方法过于聪明。
不要使equals方法依赖不可靠的资源。
不要将equals申明中的Object对象替换为其他的类型。
8.改写equals时总要改写hashCode
一个常见的错误根源在于没有改写hashcode方法,在每一个改写了equals方法的类中,你必须也要改写hashcode,如果不这样做的话,就会违反Object.hashcode的通用约定,从而导致该类无法与所有基于散列值的集合类结合在一起正常工作。
下面是hashcode约定的内容,来至java.lang.Object的规范:
(1)在一个应用程序执行期间,如果一个对象的equals方法比较所用到的信息没有被修改的话,那么对该对象调用hashcode方法多次,他必须始终如一的返回同一个整数。
(2)如果两个对象根据equals(Object)方法时相等的,那么调用这两个对象中任一个对象的hashcode方法必须产生相同的整数结果。
(3)如果两个对象根据equals方法时不相等的,那么调用这两个对象中的任一个对象的hashcode方法,不要求必须产生不同的整数结果。然而,程序员应该意识到这样的事实,对于不同的对象产生截然不同的整数结果,有可能提高散列表的性能。
不要试图从散列码计算中排除一个对象的关键部分以提高性能。
9.总是要改写toString
虽然不重要,但是一个好的toString实现,可以使一个类用起来更加愉快。
在实际应用中,toString方法应该返回对象中包含的所有令人感兴趣的信息。
不管你是否决定指定格式,都应该在文档中明确的表明你的意图。
10.谨慎改写clone
为了实现cloneable接口,使它对一个类确实产生效果,它和所有的超类都必须遵守一个相当复杂的,
不可实施的,并且基本上没有文档说明的协议,由此得到一种语言本身之外的机制:无需调用构造函数就可以
创建一个对象。
Clone方法的通用约定是非常弱的,下面是来至java.lang.Object规范中的约定内容:
创建和返回该对象的一个拷贝,这里的拷贝精确的含义取决于该对象的类,一般的含义是,对于任何对象x,表达式
x.clone() != x
将会是TRUE,并且,表达式
x.clone().getClass() == x.getClass()
将会是TRUE,但是这些都不是绝对的要求,虽然,通常情况下,表达式
x.clone().equals(x)
将会返回TRUE,但是这也不是一个绝对要求,拷贝一个对象旺旺会导致创建该类的一个新实例,但同时他也会要求拷贝内部的数据结构。
这个过程中没有调用构造函数。
实际上,clone方法是另一个构造函数,你必须确保他不会伤害到原始的对象,并且正确的建立起被克隆对象中的约束关系。
clone结构与指向可变对象的final域的正常用法是不兼容的。
11.考虑实现comparable接口
compareTo方法的通用约定与equals方法的通用约定具有相似的特征,下面是她的内容,摘自Comparable的规范:
将当前这个对象与制定的对象进行顺序比较,当该对象小于等于或者大于指定对象的时候,分别返回一个负数,零或者正整数。如果由于指定对象的
类型而使得无法进行比较,则抛出ClassCastExceptions异常。
在下面的说明中,记号sgn表示数学上的signum函数,它根据expression的值为负值,零和正值,分别返回-1,0或1.
实现者必须保证对于所有的x和y,满足sgn(x.compareTO(y))==sgn(y.compareTo(x))
实现者也必须保证这个比较关系是可传递的:x.compareTo(y)>0 && y.compareTo(z)>0,那么x.compareTo(z)>0;
最后实现者保证x.compareTo(y)==0暗示着:对于所有的z,sgn(x.compareTo(z))==sgn(y.compareTo(z));
强力建议(x.compareTo(y) ==0) == (x.equals(y)),但这不是严格要求的,一般而言,任何实现了Compareble接口的类,若违反了这个条件,应该明确的予以说明,推荐使用这样的说法,注意这个类
具有内在的排序功能,但是与equals不一致。
12.使类和成员的可访问能力最小化
经验表明,你应该尽可能的是每一个类或成员不被外界访问。
具有公有的静态final数组域几乎总是错误的,
13.支持费可变性
1.不要提供任何会修改对象的方法。
2.保证没有可被子类改写的方法。
3.使所有的域都是final的。
4.使所有的域都称为私有的。
5.保证对于任何可变组件的互斥访问。
总而言之,坚决不要为每一个get方法编写一个对应的set方法,除非有很好的理由要让一个类称为可变类,否则就应该是非可变的。
14.复合优先于继承
继承是实现代码重用的有利手段,但他并不总是完成这项工作的最佳工具。不适当的使用继承或导致脆弱的软件,在一个包的内部使用继承是非常安全的
然而,对普通的具体类进行跨越包边界的继承,则是非常危险的。
与方法调用不同的是,继承破坏了封装性。
15.要么专门为继承而设计,要么禁止继承。
16.接口优先于抽象类。
已有的类可以很容易被更新,以实现新的接口。
接口是定义mixin(混合类型)的理想选择
接口使得我们可以构造出非层次结构的类型框架
接口使得安全的增强一个类的功能成为可能。
使用抽象类来定义允许多个实现的类型,比使用接口有一个明显的优势:抽象类的演化比接口的演化比接口的演化要容易很多。如果在后续的发型版本中,你希望在抽象类中增加一个新的方法,
那么,你总是可以增加一个具体方法,他包含了一个合理的默认实现,然后,,该抽象类的所有已有的实现都自动提供了这个新的方法。对于接口,这样做是行不通的。
17.接口只是被用于定义类型
当一个类实现了一个接口的时候,这个接口被用作一个类型,通过此类型可以引用这个类的实例,因此一个类实现了一个接口,就表明客户可以对这个类的实例实施是某些动作,为了任何其他目的
而定义接口是不合适。
常量接口模式是对接口的不良使用。