Walt You - 行是知之始

《Effective Java》学习日志(八)59:了解和使用类库

2018-12-21


学习资料主要参考: 《Effective Java Third Edition》,作者:Joshua Bloch



场景

假设您要生成零和某个上限之间的随机整数。

面对这个常见的任务,许多程序员会编写一个看起来像这样的小方法:

// Common but deeply flawed!
static Random rnd = new Random();

static int random(int n) {
	return Math.abs(rnd.nextInt()) % n;
}

这种方法可能看起来不错,但它有三个缺陷。 首先,如果n是2的小幂,则随机数的序列将在相当短的时间段后重复。 第二个缺陷是,如果n不是2的幂,那么平均而言,某些数字的返回频率会高于其他数字。 如果n很大,这种效果可能非常明显。 以下程序有力地证明了这一点,该程序在精心挑选的范围内生成了一百万个随机数,然后打印出在该范围的下半部分中有多少个数字:

public static void main(String[] args) {
    int n = 2 * (Integer.MAX_VALUE / 3);
    int low = 0;
    for (int i = 0; i < 1000000; i++)
        if (random(n) < n/2)
        	low++;
    System.out.println(low);
}

如果随机方法正常工作,程序将打印一个接近50万的数字,但如果你运行它,你会发现它打印的数字接近666,666。随机方法产生的数字的三分之二落在其范围的下半部分!

随机方法的第三个缺陷是,在极少数情况下,它可以在灾难性的情况下失败,返回超出指定范围的数字。这是因为该方法尝试通过调用Math.abs将rnd.nextInt()返回的值映射到非负int。如果nextInt()返回Integer.MIN_VALUE,则Math.abs也将返回Integer.MIN_VALUE,余数运算符(%)将返回负数,假设n不是2的幂。这几乎肯定会导致程序失败,并且失败可能难以重现。

要编写一个纠正这些缺陷的随机方法的版本,你必须知道关于伪随机数生成器,数论和二进制补码算法的相当数量。幸运的是,你不必这样做 - 它已经为你完成了。它叫做Random.nextInt(int)。您不必关心它如何完成其工作的细节(尽管您可以研究文档或源代码,如果您很好奇)。一位具有算法背景的高级工程师花了很多时间设计,实现和测试这种方法,然后向该领域的几位专家展示,以确保它是正确的。然后,该库经过了近百年的测试,发布,并被数百万程序员广泛使用。该方法尚未发现任何缺陷,但如果要发现缺陷,将在下一个版本中修复。通过使用标准库,您可以利用编写它的专家的知识以及在您之前使用它的人的经验

使用库

优点

从Java 7开始,您不应再使用Random。对于大多数用途,选择的随机数生成器现在是ThreadLocalRandom。它产生更高质量的随机数,而且速度非常快。在我的机器上,它比Random快3.6倍。对于fork连接池和并行流,请使用SplittableRandom。

使用这些库的第二个好处是,您不必浪费时间将临时解决方案编写到与您的工作仅有轻微关联的问题上。如果你像大多数程序员一样,你宁愿花时间在应用程序上而不是在底层管道上工作。

使用标准库的第三个优点是,它们的性能会随着时间的推移而不断提高,而您无需付出任何努力。 因为许多人使用它们并且因为它们被用于行业标准基准测试,所以提供这些库的组织有强烈的动力使它们运行得更快。 多年来,许多Java平台库都经过重写,有时会反复重复,从而显着提升性能。

使用库的第四个优点是它们倾向于随着时间的推移获得功能。 如果某个库遗失了某些东西,开发人员社区就会知道它,并且可能会在后续版本中添加缺少的功能。

使用标准库的最后一个优点是您可以将代码置于主流中。 这样的代码更容易被大量开发人员读取,维护和重用。

获取类库信息

鉴于所有这些优点,使用类库设施优先于临时实现似乎是合乎逻辑的,但许多程序员却没有。

为什么不? 也许他们不知道类库的存在。 每个主要版本的库中都添加了许多功能,并且可以随时了解这些新增内容。 每次Java平台的主要版本发布时,都会发布一个描述其新功能的网页。 这些页面非常值得一读[Java8-feat,Java9-feat]。

为了强调这一点,假设您想编写一个程序来打印命令行中指定的URL的内容(这大致与Linux curl命令相同)。 在Java 9之前,这段代码有点乏味,但在Java 9中,transferTo方法被添加到InputStream中。 这是一个使用这种新方法执行此任务的完整程序:

// Printing the contents of a URL with transferTo, added in Java 9
public static void main(String[] args) throws IOException {
    try (InputStream in = new URL(args[0]).openStream()) {
    	in.transferTo(System.out);
    }
}

这些库太大了,无法学习所有文档[Java9-api],但每个程序员都应该熟悉java.lang,java.util和java.io及其子包的基础知识。 可以根据需要获取其他类库的知识。 总结类库的设施超出了本项目的范围,这些设施多年来一直在增长。

几个类库特别值得一提。集合框架和流库(项目45-48)应该是每个程序员的基本工具包的一部分,java.util.concurrent中的并发实用程序的部分也应如此。该软件包包含用于简化多线程编程任务的高级实用程序和低级原语,以允许专家编写自己的高级并发抽象。第80和81项讨论了java.util.concurrent的高级部分。

有时,类库设施可能无法满足您的需求。您的需求越专业化,就越有可能发生这种情况。虽然您的第一个冲动应该是使用库,但如果您已经查看了它们在某些区域提供的内容并且它不能满足您的需求,那么请使用备用实现。任何有限的库集都会提供功能上的漏洞。如果您无法在Java平台库中找到所需内容,那么您的下一个选择应该是查看高质量的第三方库,例如Google优秀的开源Guava库[Guava]。如果在任何适当的库中找不到所需的功能,您可能别无选择,只能自己实现。

总结

总而言之,不要重新发明轮子。

如果您需要做一些似乎应该相当普遍的事情,那么库中可能已经有了一个可以满足您需求的工具。如果有,请使用它;如果您不知道,请检查。一般来说,库代码可能比您自己编写的代码更好,并且可能会随着时间的推移而改进。这并不反映你作为程序员的能力。规模经济决定了图书馆代码得到的关注远远超过大多数开发人员可以承担的相同功能。


Content