关于本章,Joshua Bloch的主要观点是我们本以为覆盖的地方实际没有。
作者举了一个覆盖equals的例子。
import java.util.Set;import java.util.HashSet;
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
public boolean equals(Bigram value) {
return b.first == first && b.second == second;
}
public int hashCode() {
return 31 * first + second;
}
public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++) {
for (char ch = 'a'; ch <= 'z'; ch++) {
s.add(new Bigram(ch, ch));
}
}
System.out.println(s.size());
}
}
输出结果是260,因为在HashSet中比较元素时,还用的未被覆盖的比较方式==,由于各个元素重新分配了空间,因而就不等了。sample中新生成的equals并未生效。为什么呢?
因为要覆盖equals,需要接受Object类型的参数,若注明了@Override,错误就显而易见了。更正如下:
@Overridepublic boolean equals(Object value) {
Bigram b = (Bigram) value;
return b.first == first && b.second == second;
} No.37慎用重载
看下Joshua Bloch给的例子:
public class CollectionClassifier {public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> s) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections) {
System.out.println(classify(c));
}
}
}
我们期待会依次打印”Set”, “List”, “Unknown”。
实际是打印了3次”Unknown”,为什么呢?
因为classify方法被重载了,而要调用哪个重载方法是在编译时做出决定的,参数编译时类型都是相同的: Collection<?>
我们常会将覆盖方法和重载方法进行比较,前者的选择则是动态的,选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行类型。
class Wine {String name() {return "wine";}
}
class SparklingWine extends Wine {
@Override
String name() {return "sparkling wine";}
}
class Champagne extends SparklingWine {
@Override
String name() {return "champagne";}
}
public class Overriding {
public static void main(String[] args) {
Wine[] wines = {new Wine(), new SparklingWine(), new Champagne{}};
for (Wine wine : wines) {
system.out.println(wine.name());
}
}
}
output: wine, sparkling wine, champagne.
另一个常见的重载错误:
public static void main(String[] args) {Set<Integer> setT = new TreeSet<>();
List<Integer> listT = new ArrayList<>();
for (int i = 0; i < 6; i++) {
setT.add(i);
listT.add(i);
}
for (int i = 0; i < 3; i++) {
setT.remove(i);
listT.remove(i);
}
System.out.println(setT);
System.out.println(listT);
}
listT并没有如我们所愿,去移除前三个元素,而是移除了奇数位的元素。
因为list<E> 包含2个remove的实现,remove<E>以及remove(int), 后者是移除index位置的元素,而index是动态变化的。
关于重载,Joshua Bloch给了一点极端的建议,可以参考下:
始终为方法起不同的名字,而不使用重载机制,能够重载方法并不意味着应该重载方法。
例如,ObjectOutputStream类,对于每个基本类型,以及几种引用类型,它的write方法都有一种变形,这些变形并不是重载write方法,而是具有诸如writeBoolean(boolean), writeInt(int), writeLong(long)这样的签名。
No.38只针对异常的情况才使用异常为什么使用异常?
可以提高程序的可读性、可靠性和可维护性。
异常是为了在异常情况下使用而设计的,勿将它们用于普通的控制流,可能掩藏了错误本身。
No.39对可恢复的情况使用受检异常,对编程错误使用运行时异常何时使用受检异常?
使用受检异常,调用者可以发现并进行处理。
未受检异常包括运行时异常和错误:
都是不需要捕获的,往往属于不可恢复的情形。
Joshua Bloch 给出建议:对于可以恢复的情况,使用受检的异常;对于程序错误,使用运行异常。
No.40避免不必要地使用受检的异常受检的异常是程序设计的一项很好的特性,会强迫开发者处理异常的条件,大大增强可靠性;但是过分使用受检异常会使api使用起来非常不方便。
No.41优先使用标准的异常常见的异常:
IllegalArgumentException
IllegalStateException
NullPointerException
IndexOutOfBoundsException
ConcurrentModificationException 在禁止并发修改的情况下,检测到对象的并发修改。
UnsupportedOperationException 对象不支持用户请求的方法。
No.42抛出与抽象相对应的异常若方法抛出的异常和其所执行的任务没有明显的关系(当方法传递由底层抽象抛出,与更高层间的关系),会让人困惑。如何避免呢?
刚高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,该行为被称作异常转译。
try {//...
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
关于异常转译,作者给了一些经验之谈:
1. 尽管异常转译和不加选择地从底层传递异常的做法相比有所改进,但是不能被滥用。应尽量避免抛出异常,在给底层传递参数之前,检查更高层方法的参数的有效性,从而避免低层方法抛出异常。
我的理解是,保证代码的准确性和稳定性,使用try来给自己的代码留有余地,严格一点。
2. 可以绕开异常,将高层方法的调用者和低层的问题隔离开来。
我的理解是,在一些不可避免的异常模块,我们没有必要一定throw出去,一些没有必要通知高层的部分,我们以log形式记录日志即可了。