11.当你覆写equals时每次都覆写hashCode
不这么做会使hashmap,hashset等需要依赖哈希的equals方法返回错误的结果。
1、两个对象的equals(Object)相等,返回的hashcode也需要返回相同的整数。
2、对象的信息没修改,同一个应用执行期间的hashcode必须相等。
3、两个对象的equals(Object)不相等,则不一定要返回不同的整数,但是每次返回不同的整数有利于散列性能(避免冲突)。
hash值分散的建议:
1.声明一个整数变量名字为result,为你的对象的第一个重要域,初始化它为哈希码c。
2.为剩余的每个重要域f,做如下事情:
a. 为域计算一个整数哈希码c
i. 如果域是一个原始类型,那么计算Type.hashCode(f),这里Type是相应于f类型的原始装箱类型。
ii. 如果域是一个对象引用,而且这个类的equals方法通过递归调用equals比较域,那么在域上递归调用hashCode。如果需要一个更加复杂的比较,为这个域以规范形式计算一个“规范形式”。如果这个域的值是空,那么使用0(或者某个另外一个常量,但是0是惯例的)。
iii. 如果域是一个队列,那么对待它好像每个重要元素是一个单独的域。就是说,通过递归地应用这些规则为每个重要元素计算一个哈希码,然后结合步骤2.b的值。如果队列没有重要元素,使用一个常量,最好不是0。如果所有的元素是重要的,使用Arrays.hashCode。
// 典型的hashCode方法
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
选择31做乘数的原因
在不考虑性能的情况下可以考虑用 Objects.hash(Object…values) 实际上调用的是 Arrays.hashCode(Object[] a)
耗性能原因:
1 可变参数,见条目53
2 如果是原始类,需要拆箱装箱
不可变对象建议缓存hashcode
12. 始终重写 toString 方法
不重写toString 生成的格式 :
由类名后跟一个@和哈希码的无符号十六进制表示组成
优点
输出,调试方便
缺点
格式固定,改变将可能会引起其他类的日志格式、解析错误等
建议:
如果重写了toString方法,在类注释中写上toString方法的格式
13. 谨慎地重写 clone 方法
1、需要复制的对象的类必须实现Cloneable接口,否则调用clone方法会报CloneNotSupportedException。
2、Object.clone() 可以返回子类。
3、System.arraycopy,数组调用clone都是浅拷贝。
4、clone方法避免调用可重写的方法,会导致克隆的对象不一致。
5、线程安全的类,clone方法需要加锁。
6、不建议使用object的方法,建议用构造器复制或者静态工厂复制
// Copy constructor
public Yum(Yum yum) { ... };
// Copy factory
public static Yum newInstance(Yum yum) { ... };
复制构造方法及其静态工厂变体与 Cloneable/clone 相比有许多优点:
它们不依赖风险很大的语言外的对象创建机制;
不要求遵守那些不太明确的惯例;
不会与 final 属性的正确使用相冲突;
不会抛出不必要的检查异常;
不需要类型转换。
入参可以自定义
14. 考虑实现 Comparable 接口
1.约定类似于equals方法:
对称性
传递性
等于0情况下equals也需要相等(推荐项),即x.compareTo(y) == 0) == (x.equals(y))
2.使用Comprator的静态方法,链式风格编码,此处静态导入了Comprator类
// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
3.compareTo方法中不要使用属性相减(会导致整数溢出或者浮点数精度丢失),用Number.compare(o1,o2)
// BROKEN difference-based comparator - violates transitivity!
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
15. 使类和成员的可访问性最小化
信息隐藏或封装:组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。
优点:
可并行开发
减轻维护负担
松耦合
做法:
让每个类或成员尽可能地不可访问,使用尽可能低的访问级别。
公开属性缺点:
1.公共可变属性的类通常不是线程安全的
2.限制属性添加新能力
其他:
类实现接口会减少方法访问的能力:实现接口的方法都必须为public。
16. 在公共类中使用访问方法而不是公共属性
如果一个类在其包之外是可访问的,则提供访问方法来保留更改类内部表示的灵活性。
如果一个类是包级私有的,或者是一个私有的内部类,那么暴露它的数据属性就没有什么本质上的错误。
java.awt.Point 和 java.awt.Dimension 是反例
17. 最小化可变性
不可变类:它的实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的。
使类不可变的做法:
- 不要提供修改对象状态的方法。
- 确保这个类不能被继承。
- 把所有属性设置为 final。
- 把所有的属性设置为 private。
- 确保对任何可变组件的互斥访问(引用可变对象的属性,需要防止客户端拿到这个属性)
优点:
1.不可变对象本质上是线程安全的; 它们不需要同步,可以共享,不需要也不建议做防御性拷贝。
2.可以共享内部信息
3.为其他对象提供了很好的构件,如创建了一个新的该对象的一个新对象出来而不会破坏原来的对象
4.不可变对象提供了免费的原子失败机制
缺点:
对于每个不同的值都需要一个单独的对象。
需要考虑将不可变对象设计的小,或者缓存起来。
总结:
不要为每个类提供set方法。
如果无法设计一个完全不可变的类,则考虑限制部分属性的可变性。
构造方法创建对象并将所有的属性初始化,可提供静态工厂方法初始化,并将构造器私有。
18. 组合优于继承
组合:新类A中持有一个B类的实例。
转发:新类中的方法调用组合实例中的方法。
继承缺点:
1.父类后续版本的变化可能会影响到子类,如参数变化,返回类型,逻辑变化等,最为致命的是如果父类添加的新方法的签名,刚好跟子类之前添加的新方法的签名一样。
组合优点:
健壮,不被父类影响
组合缺点:
无法使用在回调中。如B包装了A,在回调方法中用A对象来做回调,此时A并不知道B包装了A。
什么情况下使用继承:
is-a原则:例如形状类Shap,Circle圆形类是一种形状,则Circle类可以考虑继承Shap
19. 要么设计继承并提供文档说明,要么禁用继承
设计一个继承类的时候,无法决定暴露哪些受保护的成员。所以,必须编写测试子类。
需要注意:
1 父类中不能调用可变的方法,否则可能会引起不能预估的错误,例如:
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
// Blank final, set by constructor
private final Instant instant;
Sub() {
instant = Instant.now();
}
// Overriding method invoked by superclass constructor
@Override
public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
将打印一次null,一次instant实例。
2 父类若实现了Serializable接口则,readResolve和writeReplace方法必须确保不是private的防止子类方法被忽略。
20. 接口优于抽象类
优点:
1、接口能定义成mixin(混合类型),即类除了表示自己的类型之外,还能表示成接口的类型,在注入很有用;也能把类转换成接口变成带公共方法的集合体。
这个特性虽然抽象类也有,但是抽象类无法只能通过继承,而java只提供单继承,所以不能定义成混合类型。
2、接口允许构建非层级类型的框架。平级,不像抽象类一样需要多级:
具有唱、写歌特性,并且有自己类型的接口
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
接口缺点:
1、不允许默认方法实现equals,hashcode的默认实现。
2、不允许包含实例属性或非公共静态成员(私有静态方法除外)。
3、不能将默认方法添加到不受控制的接口中。(作者的意思可能是没权限修改的接口吧)。
可以通过提供一个抽象的骨架实现类与接口结合使用,如实现接口不能实现的方法,提供一些新特性,结合模板方法模式等,例如AbstractCollection,AbstractSet。
骨架类的优点:
提供一些帮助方法方便子类实现一些通用功能。
缺点:
不是必须的,如果骨架类无法提供协助子类化,则子类完全可以自己实现接口。
必须遵循项目19的规则,写好文档。