Effective Java 3rd (二)

8 篇文章 0 订阅
4 篇文章 0 订阅

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. 最小化可变性

不可变类:它的实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的
使类不可变的做法:

  1. 不要提供修改对象状态的方法。
  2. 确保这个类不能被继承。
  3. 把所有属性设置为 final。
  4. 把所有的属性设置为 private。
  5. 确保对任何可变组件的互斥访问(引用可变对象的属性,需要防止客户端拿到这个属性)
    优点:
    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的规则,写好文档。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值