第一条:考虑用静态工程方法代替构造器
优点:
* 静态工厂方法有名称,更能便于人们理解
* 可以不必每次调用它们的时候都创建一个新对象,类似于单例模式
* 可以返回原返回类型的任何子类型对象,更加灵活便于修改
* 在创建参数化实例的时候,他们是代码变得更加简洁,例如: Map<String, List<String>> map = HashMap.newInstance();
缺点:
* 类如果不含公有或者受到保护的构造器,就不能被子类化。
* 静态工厂方法用于其他的静态方法实际上没有区别。为区分其他静态方法,一般将工程方法命名为以下名称:
* valueOf或of:返回的实例与他的参数具有相同的值
* getInstance:获取实例,通过不同的参数来获取,如果是Singleton将会返回唯一实例
* netInstance:和getInstance类似,但每次获取的实例都和其他实例不同
(第2~7没有保存,浏览器崩掉了就没有了,不想再写了,这玩意居然没有自动保存的功能,伤心)
第8条:覆盖equals是请遵守通用约定
不覆盖equals方法,类的每个实例都和它本身相等,如果满足一下任何一个条件,这就正式所期望的结果。
1. 类的每个实例本质都是唯一的
2. 不关心类是否提供了“逻辑相等”的测试功能。
3. 超类已经覆盖了equals,从超类继承过来的行为对于子类 也是合适的。
4. 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。
以上4种不需要覆盖equals,需要覆盖equals一般作用于“值类”(value class)
1.自反性。对于任何非null的引用值x,x.equals(x)必须返回true
2.对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true是,x.equals(y)必须也返回true
3.传递性。对于任何非null的引用值x和y、z,当且仅当x.equals(y)返回true,并y.equals(z)也返回true,那么x.equals(z)也必须返回true
4.一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致的返回false。
5.对于任何非null的引用值x,x.equals(null)必须返回false
实现高质量equals方法的诀窍
1. 使用==操作符检查参数是否为这个对象的引用,如果是就返回true
2. 使用instanceof操作符检查参数是否为正确的类型,如果不是就返回false
3. 把参数转换成正确的类型
4. 对于该类中每个“关键”域(significant)(每一个属性),检查参数中的域是否与该对象中对应的域相匹配,如果全部检查都通过则传true,否则传false
5. 当编写好了equals方法之后,应该为自己三个问题:它是否是对称的、传递的、一致的
需要注意的是
1. 覆盖equals的时候总要覆盖hashCode
2. 不要企图让equals方法过于智能
3. 不要将equals声明中的Object对象替换为其他类型,例如: public boolean equals(MyClass o) {...}
这个方法并没有覆盖Object.equals,因为此方法的参数本该是Object类型
第9条:覆盖equals是总要覆盖hashCode
如果不这样做的话,在这些类使用基于散列的集合将会不能正常运作,比较常见的散列集合有:HashMap、HashSet、Hashtable
在Object规范[JavaSE6]中有说明
如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须返回同样的整数结果。
如果我们不覆盖hashCode,会有什么问题呢?以下是一个示例,User.java
package com.model;
import java.util.Date;
public class User {
private String name;
private int age;
private Date reg;
public static class Builder {
//默认值
private String name = "用户";
private int age = 18;
private Date reg = new Date(System.currentTimeMillis());
public Builder(String name) {
this.name = name;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder reg(Date reg) {
this.reg = reg;
return this;
}
public User build() {
return new User(this);
}
}
public User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.reg = builder.reg;
}
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
if(obj == null) {
return false;
}
if(obj == this) {
return true;
}
if(obj instanceof User) {
User user = (User) obj;
if(user.age == this.age && user.name.equals(this.name) && user.reg.equals(this.reg)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
这个类中只覆盖了equals,当我们使用User类作为HashMap的key的时候
Date reg = new Date(System.currentTimeMillis());
User user = new User.Builder("test").reg(reg).build();
Map<User, String> map = new HashMap<>();
map.put(user, "slience");
System.out.println(map.get(new User.Builder("test").reg(reg).build()));
我们期望的输出是slience
,但是因为前后两个build的用户hashCode返回值不一样(HaseMap是以hash值作为判别是否相同的标准的),所以将会输出null,我们在User.java中添加hashCode覆盖
@Override
public int hashCode() {
// TODO Auto-generated method stub
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
result = 31 * result + reg.hashCode();
return result;
}
这样再运行的时候HashMap就可以返回正确的数值了。(上面代码中使用31是因为31是一个奇素数,这主要在hashCode以位运算来计算的时候有好处)
另外需要说明的是,如果散列码(hashcode)计算的代价太昂贵,可以在实体中用一个属性来缓存起来。
第10条:始终都要覆盖toString
Object.toString方法返回的数据太过笼统,不能很好的显示完整对象的信息
新的toString方法应该在返回值中包含所有必要的值得关注的信息
不过我自己一般直接用lombok中的@Data就成,很少自己写toString
第11条:谨慎地覆盖clone
clone方法用于克隆(拷贝)一个对象,这个对象应该做到
1. x.clone() != x
2. x.clone().getClass() == x.getClass()
3. x.clone().equals(x) == true
我们如果想要让类使用clone方法需要让他实现Cloneable接口,举例来说
package com.test;
import java.util.Date;
import org.junit.Test;
public class User implements Cloneable {
private String name;
private int age;
private Date reg;
@Override
protected User clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return (User) super.clone();
}
}
需要注意的是如果对象是有属性是一个数组或者Map这类的东西,那么在clone的时候应该遍历全部,重新new一个新的数组给新克隆的对象,否则会造成克隆对象和原对象的数组属性所指向的数据相同。举个例子的话在Hashtable的源代码中有如下代码
/**
* Hashtable bucket collision list entry
*/
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
它的clone方法就是递归克隆了整个数组。
第12条:考虑实现Comparable接口
compareTo方法主要用来比较两者孰大孰小,它和equals的相关规定类似。即
x.compareTo(y)>0 && y.companyTo(z)>0,那么x.compareTo(y)>0等
需要注意的是有的类equals和compareTo的结果可能不同,举例来说就是,new BigDecimal(“1.0”)和new BigDecimal(“1.00”)的equals结果是false,但是compareTo的结果是0。
需要注意的是在写compareTo的时候如果要用x-y是否大于0来判断x大还是y大,需要避免这种情况——x和y的差值大于Integer.MAX_VALUE,如果差值过大可能会出现溢出造成判断结果有误。
第13条:使类和成员的可访问性最小化
设计良好的模块会隐藏所有的实现细节,把他的API与它的实现清晰地隔开来,然后模块直接只通过他们的API进行通信,一个模块不需要知道其他模块的内部工作情况————这个概念被称为信息隐藏。
信息隐藏的好处在于:解除组成系统的各模块之间的耦合关系,使得各个模块可以独立的开发、测试、优化、使用、理解和修改。以便加快系统开发的速度。
在做信息隐藏的时候需要注意,如果方法覆盖了一个超类中的一个方法,子类中的访问级别就不允许使用低于超类中访问级别。这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例。
还有一个需要注意的地方在于:实例域或者静态域不能是公有的。举例来说Student类中的Books数组这种引用变量不可以是公有的,因为一旦设为公有,那就意味着放弃对这个域中的值进行限制的能力,别的类可以随意修改Books中的值。哪怕Books是final的,也可以通过修改它所指向的Book实体来修改。
为了弥补这个缺陷,我们可以将实例域设为私有的然后将获取实例域的方法设为共用的,此方法每次返回私有的实例域的一个clone,就像这样
private static final Date[] TIMES_PRI = {new Date(System.currentTimeMillis())};
public static final Date[] values() {
return TIMES_PRI.clone();
}
第14条:在公有类中使用访问方法而非公有域
反例
public class Point {
public double x;
public double y;
}
正例
public class Point {
private double x;
private double y;
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
}
第15条:是可变性最小化
为了使类变为不可变的,需要遵循以下五条规则
1. 不提供任何修改对象的方法
2. 确保类不会被拓展,一般做法是使这个类设为final
3. 使所有的域(属性)都是final
4. 是所有的域(属性)都是私有的
5. 确保对于任何可变组件的互斥访问。如果类具有可变对象的域,则必须 确保该类的客户端无法获得指向这些对象的引用。