5 泛型
第23条: 请不要在新代码中使用原生态类型
声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口。
每个泛型都对应一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。例如,List<E>
对应的原生态类型是List。
如果使用List这种原生态类型,就会失掉类型安全性,但是如果使用像 List<Object>
这样的参数化类型,则不会。
本条规则的两个例外:
(1)在类文字(class literal)中必须使用原生态类型。
List.class、String[].class等都是合法的。但是List<String>.class
和 List<?>.class
不合法
(2)由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof是非法的。
利用泛型来使用instanceof的首选方法:
if (o instanceof Set) {
Set<?> m = Set<?> o;
}
第24条: 消除非受检告警
如果无法消除告警,同时又可以证明引起告警的代码是类型安全的,只有在这种情况下才可以用一个@SuppressWarnings("unchecked")
注解来禁止这条警告。
应该在尽可能小的范围中使用@SuppressWarnings
注解。这个注解应该用在变量声明、或者是非常简短的方法或者构造器上面。永远不要在整个类上使用@SuppressWarnings
注解,这么做可能会掩盖了重要的警告。
使用@SuppressWarnings
注解的时候,都要注释一下为什么这里的代码是类型安全的。
第25条: 列表优先于数组
数组是协变的(covariant):如果Sub是Super的子类型,那么Sub[]就是Super[]的子类型。
泛型是不可变的(invariant):对于任意两个不同的类型Type1和Type2,List<Type1>
既不是List<Type2>
的子类型,也不是List<Type2>
的超类型。
数组是具体化的(reified):数组是在运行时才知道并检查元素的类型。
泛型是通过擦除(erasure)来实现的:泛型只是在编译时强化类型信息,并在运行时丢弃类型信息。擦除使得使用泛型的代码可以合没有使用泛型的代码随意进行互用。
创建泛型数组是非法的:因为它不是类型安全的。如果合法,编译器在其他正确的程序中发生的转换就会在运行时失败,抛出ClassCastException。
不可具体化的(non-reifiable)类型是指其运行时表示法包含的信息比它编译时表示法包含的信息更少的类型。唯一可具体化的参数化类型是无限制的通配符类型,如List<?>
和Map<?,?>
第26条: 优先考虑泛型
第27条: 优先考虑泛型方法
静态工具方法尤其适合泛型化。Collections中的所有“算法方法(如binarySearch和Sort)”都泛型化了。
第28条:利用有限制通配符来提升API的灵活性
第29条:优先考虑类型安全的异构容器
当一个类的字面文字(class)被用在方法中,来传达编译时和运行时的类型信息,就被称作type token。
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
if (type == null) {
throw new NullPointerException("Type is null");
}
favorites.put(type, instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
6 枚举和注解
第30条: 用enum代替int常量
java枚举可以将常量和一些数据关联起来。
特定于常量的方法实现(constant-specific method implementation):
public enum Operation {
PLUS {double apply(double x, double y) {return x + y;}},
MINUS {double apply(double x, double y) {return x - y;}},
TIMES {double apply(double x, double y) {return x * y;}},
DIVIDE {double apply(double x, double y) {return x / y;}};
abstract double apply(double x, double y);
}
特定于常量的方法实现通常可以合特定于常量的数据结合起来使用:
public enum Operation {
PLUS("+") {double apply(double x, double y) {return x + y;}},
MINUS("-") {double apply(double x, double y) {return x - y;}},
TIMES("*") {double apply(double x, double y) {return x * y;}},
DIVIDE("/") {double apply(double x, double y) {return x / y;}};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
abstract double apply(double x, double y);
@Override
public String toString() {
return symbol;
}
public static void main(String[] args) {
double x = 1;
double y = 2;
for (Operation op : Operation.values()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
}
output:
1.000000 + 2.000000 = 3.000000
1.000000 - 2.000000 = -1.000000
1.000000 * 2.000000 = 2.000000
1.000000 / 2.000000 = 0.500000
策略枚举的使用:
/**
* 每当添加一种枚举常量时,就会强制选择一种加班报酬策略。
* 如果多个常量枚举同时共享相同的行为,则考虑策略枚举。
*
* @since 2022-12-13
*/
public enum PayrollDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY),
FRIADAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
private enum PayType() {
WEEKDAY {
@Override
double overTimePay(double hrs, double payRate) {
return hrs <= HOURS_PER_SHIFT ? 0 : (hrs - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
@Override
double overTimePay(double hrs, double payRate) {
return hrs * payRate / 2;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double overTimePay(double hrs, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overTimePay(hoursWorked, payRate);
}
}
}
第31条: 用实例域代替序数
永远不要根据枚举的序数导出与它关联的值,而是要将其保存在一个实例域中。
第32条: 用EnumSet代替位域
public class Text {
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKENTHROUGH};
public void applyStyles(Set<Style> styleSet) {
}
public static void main(String[] args) {
new Text().applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
第33条: 用EnumMap代替序数索引
/**
* 香草类
*
* @since 2022-12-13
*/
public class Herb {
public enum Type {annual, perennial, biennial};
private final String name;
private final Type type;
Herb(String name, Type type) {
this.name = name;
this.type = type;
}
@Override
public String toString() {
return name;
}
/**
* 将所有的香草归类展示
*
* @param args
*/
public static void main(String[] args) {
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Type, Set<Herb>>(Herb.Type.class);
for (Herb.Type type : Herb.Type.values()) {
herbsByType.put(type, new HashSet<Herb>());
}
for (Herb herb : garden) {
herbsByType.get(herb.type).add(herb);
}
System.out.println(herbsByType);
}
}
/**
* 输入物质的状态变化,输出经历了哪种物质变化过程
*
* @since 2022-12-13
*/
public enum Phase {
solid, liquid, gas;
public enum Transition {
melt(solid, liquid), freeze(liquid, solid),
boil(liquid, gas), condense(gas, liquid),
sublime(solid, gas), deposit(gas, solid);
private final Phase source;
private final Phase dest;
Transition(Phase src, Phase dest) {
this.source = src;
this.dest = dest;
}
private static final Map<Phase, Map<Phase, Transition>> map = new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
static {
for (Phase phase : Phase.values()) {
map.put(phase, new EnumMap<Phase, Transition>(Phase.class));
for (Transition transition : Transition.values()) {
map.get(transition.source).put(transition.dest, transition);
}
}
}
public static Transition from(Phase source, Phase dest) {
return map.get(source).get(dest);
}
}
}
第34条: 用接口模拟可伸缩的枚举
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
plus("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
minus("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
},
times("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
divide("/") {
@Override
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
public enum ExtendedOperation implements Operation {
exp("^") {
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
remainder("%") {
@Override
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public static void main(String[] args) {
double x = 3;
double y = 4;
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants()) {
System.out.printf("%f %s %f = %f\r\n", x, op, y, op.apply(x, y));
}
}
}
第35条: 注解优先于命名模式
JAVA 1.5之前,一般使用命名模式(naming pattern)表名某些程序元素需要通过某种工具或者框架进行处理。例如,Junit要求它的用户一定要使用test作为测试方法名的开头。
命名模式的缺点:
(1)拼写错误会导致失败,但是没有任何提示。tsetXXX方法写错了名字,Junit不会出错,并且也不会去执行用例。造成错误的安全感(即使测试方法没有执行,也没有报错,给人以测试通过的假象)
(2)无法确保它们只用于相应的程序元素上。假设将某个类称为testXXX,希望JUnit测试它的所有的方法。Junit同样不会报错,也不会执行测试。
(3)命名模式没有提供将参数值与程序元素关联起来的好方法。例如,想要支持一种测试类别,它只在抛特殊异常的时候才会成功。
第36条: 坚持使用Override注解
应在想要覆盖超类声明的每个方法声明中使用Override注解。
使用Override注解的好处,如果覆写的方法的声明的返回值或者参数写错了的话,编译的时候就会报错。
第37条: 用标记接口定义类型
标记接口(marker interface)是指没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口。例如,Serializable接口,实现这个接口的类表明它的实例可以被写到ObjectOutputStream中。
如果想要定义类型,一定要使用接口。
7 方法
第38条: 检查参数的有效性
对于未被导出的方法(unexported method),作为包的创建者,你可以控制这个方法将在哪些情况下被调用。因此你可以,并且你也应该确保只将有效的参数值传递进来。因此,非公有方法通常应该使用断言(assertion)来检查他们的参数。 例如:
// private helper function for a recursive sort.
private static void sort(long[] a, int offset, int lenght) {
assert a!= null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
// do the compulation
............
}
例外情况:在有些情况下,有效性检查非常expensive,或者根本是不切实际的,或者有效性检查已经隐含在计算过程中完成了。例如,Collections.sort(List), 列表中的所有对象都应该是可以相互比较的。排序过程中就会进行比较,提前检查列表中的元素是否可以相互比较是没有多少意义的。
第39条: 必要时进行保护性拷贝
Java是一门安全的语言(safe language),它对于缓冲区溢出、数组越界、非法指针以及其他内存破坏错误都自动免疫。在设计类的时候,可以确切地知道无论系统其他部分发生什么事情,这些类的约束都可以保持为真。对于那些“把所有的内存当做一个巨大的数组来看”的语言来说,这是不可能的。
假设类的客户端会尽可能地破坏这个类的约束条件,因此你必须保护性地设计程序。
考察下面这个类,它声称可以表示一段不可变的时间周期:
public class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
这个类约束了start <= end, 但是因为Date类是可变的,因此很容易违反这个约束条件:
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78); // 很容易就改掉了p的内部数据。
为了避免内部信息被改变,对构造器的每个可变参数进行保护性拷贝是必要的 :
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
注意:保护性拷贝是在检查参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象。 没有使用Date的clone方法来拷贝对象,不能保证clone方法是返回的java.util.Date的对象(有可能返回不可信子类的实例)。 为了阻止这种攻击,对于参数类型可以被不可信方子类化的参数,请不要使用clone方法进行保护性拷贝。
虽然对构造器进行了改造,但是改变Period实例仍然是有可能的,因为它的getter方法提供了对其可变内部成员的访问能力:
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
period.end().setYear(78); // 还是很容易就改掉了p的内部数据。
返回可变内部域的保护性拷贝:
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
对于Period的例子中,有经验的程序员通常使用Date.getTime()返回的long型作为内部时间表达法,而不是使用Date(因为Date是可变的)。
第40条: 谨慎地设计方法签名
(1)谨慎地选择方法的名称。
(2)不要过于追求提供便利的方法。只有当一项操作被经常用到的时候,才考虑为它提供快捷方式(shorthand)。如果不能确定,还是不提供快捷为好。
(3)避免过长的参数列表。相同类型的长参数列表格外有害。 如果函数使用者弄错了参数顺序时,程序仍然可以编译和运行,只不过这些程序不会按照作者的意图进行工作。
缩短过长参数列表的三种方法:
(1)把方法分解成多个方法,每个方法只需要原来参数列表的一个子集。
(2)创建辅助类(helper class),用来保存参数的分组。
如果一个频繁出现的参数序列可以被看作是代表了某个独特的实体,建议使用这种方法。
(3)Builder模式。
对于参数类型,要优先使用接口而不是类。
对于boolean类型,要优先使用两个元素的枚举类型。
第41条:慎用重载
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> list) {
return "Set";
}
public static String classify(Collection<?> c) {
return "Unkown Collection";
}
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));
}
}
}
Unkown Collection
Unkown Collection
Unkown Collection
对于重载方法是在编译时做出的决定。 for循环中的三次迭代,参数编译时类型都是相同的:Collection<?>
使用重载机制安全而保守的策略是:永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数(varargs),就不要重载它了。 这样的话就不会陷入到“对于任何一组实际的参数,哪个重载方法是适用的”这样的疑问中。例如ObjectOutputStream类中提供了writeBoolean(boolean)、wirteInt(int)、writeLong(int)等等,而不是write()重载方法。
下面就是因为拆装箱而引入的重载的问题:
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<>();
List<Integer> list = new ArrayList<>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
// list.remove(i); // 这里是remove(int),所以是:[-3, -2, -1], [-2, 0, 2]
list.remove(Integer.valueOf(i)); // remove(Integer), 这里就是remove元素了,结果回和set一样。
}
System.out.println(set + ", " + list);
}
}
第42条:慎用可变参数
可变参数不是在编译时失败,而是到运行时才会失败。
第43条: 返回长度为0的数组或者集合,而不是null
第44条: 为所有导出的API元素编写文档注释
所有的public或者protected的类、接口、方法或者变量等等。