目录
- 仔细设计方法签名
- 明智审慎地使用重载
- 明智审慎地使用可变参数
- 返回空的数组或集合,不要返回 null
- 明智审慎地返回 Optional
- 为所有已公开的 API 元素编写文档注释
- 最小化局部变量的作用域
- for-each 循环优于传统 for 循环
- 了解并使用库
- 若需要精确答案就应避免使用 float 和 double 类型
51. 仔细设计方法签名
仔细选择方法名名称
名称应始终遵守标准命名约定且长度尽可能短。
不要过分地提供方便的方法
每种方法都应该“尽其所能”,对于接口更是如此,太多方法不便于调用方学习。
避免过长的参数列表
目标是四个或更少的参数,参数太多难以记住,顺序弄错概率增加。
有三种技术可以缩短过长的参数列表:
1、将方法分解为多个方法,每个方法只需要参数的一个子集。例如java.util.List,没有提供查询头尾节点的方法,而是提供了subList,indexOf,lastIndexOf 方法让客户端自由组合调用。
2、**创建辅助类来保存参数组。**如果多个地方重复传相同的参数,则可以考虑使用该方法。
3、采用 Builder 模式
对于参数类型,优先选择接口而不是类,原因很简单,传接口更灵活。
与布尔型参数相比,优先使用两个元素枚举类型,如果布尔类型在方法中是明确的,则可以用布尔类型,以后也可以增加其他选线。
52. 明智审慎地使用重载
重载(overloaded)方法之间的选择是静态的,而重写(overridden)方法之
间的选择是动态的。
// Broken! - What does this program print?
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
//这个方法才能解决重载的问题
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" :c instanceof List ? "List" : "Unknown 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));
}
}
这段代码将输出三次classify(Collection<?> c)
重载(overloaded)方法之间的选择是静态的,而重写(overridden)方法之间的选择是动态的。
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) {
List<Wine> wineList = List.of(new Wine(), new SparklingWine(), new Champagne());
for (Wine wine : wineList)
System.out.println(wine.name());
}
}
这段代码将会打印 wine,sparkling wine 和 champagne
一个安全和保守的策略是永远不要导出两个具有相同参数数量的重载。
例如 ObjectOutputStream 的 writeBoolean(boolean) 、 writeInt(int) 和 writeLong(long),而不是重载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);
}
System.out.println(set + " " + list);
}
}
这段代码的目的是想移除大于0的元素,但是实际上打印的是[-3, -2, -1][-2, 0, 2]
原因是ArrayList有两个remove方法,上面的方法调用了remove(index)方法,每次都是去移除对应下标的元素,应该改为:
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove((Integer) i); // or remove(Integer.valueOf(i))
}
53. 明智审慎地使用可变参数
可变参数方法正式名称称为可变的参数数量方法『variable arity methods』 [JLS, 8.4.1],接受零个或多个指定类型的参数。 可变参数机制首先创建一个数组,其大小是在调用位置传递的参数数量,然后将参数值放入数组中,最后将数组传递给方法。
使用可变参数需要防止调用方传入无参,需要做校验:
/
/ The WRONG way to use varargs to pass one or more arguments!
static int min(int... args) {
if (args.length == 0)
throw new IllegalArgumentException("Too few arguments");
int min = args[0];
for (int i = 1; i < args.length; i++)
if (args[i] < min)
min = args[i];
return min;
}
这种方式的缺点是运行时才能发现错误,更好的方式是强制至少传入一个参数
// The right way to use varargs to pass one or more arguments
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs)
if (arg < min)
min = arg;
return min;
}
使用可变参数,每次都会创建参数列表一样大小的数组,当性能需要考虑时,可以预估使用多少参数的几率比较大,重载多个方法
public void foo() { }
public void foo(int a1) { }
public void foo(int a1, int a2) { }
public void foo(int a1, int a2, int a3) { }
public void foo(int a1, int a2, int a3, int... rest) { }
54. 返回空的数组或集合,不要返回 null
// Returns null to indicate an empty collection. Don't do this!
private final List<Cheese> cheesesInStock = ...;
/**
* @return a list containing all of the cheeses in the shop,
* or null if no cheeses are available for purchase.
*/
public List<Cheese> getCheeses() {
return cheesesInStock.isEmpty() ? null: new ArrayList<>(cheesesInStock);
}
上面这段代码,客户端需要判断是否为null,如果忘记判断,可能会导致运行时错误
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
System.out.println("Jolly good, just the thing.");
数组和列表返回空对象
//The right way to return a possibly empty collection
public List<Cheese> getCheeses() {
return new ArrayList<>(cheesesInStock);
}
//The right way to return a possibly empty array
public Cheese[] getCheeses() {
return cheesesInStock.toArray(new Cheese[0]);
}
55. 明智审慎地返回 Optional
编写在特定情况下无法返回任何值的方法时,可以采用三种方法
1、抛出异常 (需要调用方捕获)
2、返回null (需要调用方判断)
3、返回Optional (调用方可以选择如何处理)
// Returns maximum value in collection as an Optional<E>
public static <E extends Comparable<E>>
Optional<E> max(Collection<E> c) {
if (c.isEmpty())
return Optional.empty();
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = Objects.requireNonNull(e);
return Optional.of(result);
}
客户端可以选择处理方式
// Using an optional to provide a chosen default value
String lastWordInLexicon = max(words).orElse("No words...");
// Using an optional to throw a chosen exception
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
并不是所有的返回类型都能从 Optional 的处理中获益。容器类型,包括集合、映射、Stream、数组和Optional,不应该封装在 Optional 中。与其返回一个空的 Optional<List> ,不还如返回一个空的List<T>。
56. 为所有已公开的 API 元素编写文档注释
要正确地记录 API,必须在每个导出的类、接口、构造方法、方法和属性声明之前加上文档注释。
方法的文档注释应该简洁地描述方法与其客户端之间的契约。除了为继承而设计的类中的方法 (详见第 19 条)之外,契约应该说明方法做什么,而不是它如何工作的。
泛型,枚举和注释需要特别注意文档注释。 记录泛型类型或方法时,请务必记录所有类型参数。
57. 最小化局部变量的作用域
用于最小化局部变量作用域的最强大的技术是再首次使用的地方声明它。
就是说,局部变量最好在使用之前声明,过早声明可能后面不会使用到,例如条件选择,提前返回。
**几乎每个局部变量声明都应该包含一个初始化器。**如果还没有足够的信息来合理地初始化一个变量,那么应该推迟声明,直到认为可以这样做。try catch块除外,因为在catch块或者finally中需要用到。
优先使用for循环而不是while,下面这段代码,由于作用范围太大导致bug
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { // BUG!
doSomethingElse(i2.next());
}
用for循环则不会
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
// Compile-time error - cannot find symbol i
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
Element e2 = i2.next();
... // Do something with e2 and i2
}
58. for-each 循环优于传统 for 循环
传统for循环:
1、使用iterator
// Not the best way to iterate over a collection!
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e
}
2、for 下标
// Not the best way to iterate over an array!
for (int i = 0; i < a.length; i++) {
... // Do something with a[i]
}
使用for each的原因
1、通常循环只需要取出元素,而foreach循环隐藏了其它变量,减少引用出错几率。
2、更简洁,可读性高。
3、第一点的补充,嵌套循环更有优势,外层循环作用域在内层循环内,可能用错变量。
有三种情况无法使用foreach
1、有损过滤
需要删除其中的元素,需要用迭代器
2、转换
如果需要遍历一个列表或数组并替换其元素的部分或全部值,那么需要列表迭代器或数组索引来替换元素的值
3、并行迭代
如果需要并行地遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变
量都可以同步进行
实现Iterable接口的类都能使用foreach
59. 了解并使用库
返回0到n之间的随机整数
// Common but deeply flawed!
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
这个方法有3个缺点:
1、如果 n 是小的平方数,随机数序列会在相当短的时间内重复。
2、如果 n 不是 2 的幂,那么平均而言,一些数字将比其他数字更频繁地返回
3、如果 n 很大,这种效果会很明显,在极少数情况下会返回超出指定范围的数字
从 Java 7 开始,就不应该再使用 Random。在大多数情况下,选择的随机数生成器现在是
ThreadLocalRandom。 它能产生更高质量的随机数,而且速度非常快。(Random产生的随机数,需要通过cas比较种子产生,ThreadLocalRandom则把种子放在ThreadLocal里面)
对于fork 连接池和并行流,使用 SplittableRandom。
使用标准库的优点:
1、复用前人经验。
2、专注于需求。
3、版本升级带来性能提升。
4、更容易其他人阅读,毕竟是公共的api。
60. 若需要精确答案就应避免使用 float 和 double 类型
计算机内部是用二进制表示的,小数无法精确表示,这跟小数在计算机中的存储方式有关。
解决方法是用BigDecimal来计算
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS;funds.compareTo(price) >= 0;price = price.add(TEN_CENTS)) {
funds = funds.subtract(price);
itemsBought++;
}
System.out.println(itemsBought +"items bought.");
System.out.println("Money left over: $" + funds);
}
注意,需要用BigDecimal的字符串的构造函数
使用BigDecimal的缺点是api操作麻烦,性能较差。
更好的解决方法是将小数等价扩大成整数,例如计算3.4元-2.7元=34毛-27毛