3.2 实现机制
例3-3中,整个过程被分解为两种更简单的操作:过滤和计数,看似有化简为繁之嫌——例3-1中只含一个for循环,两种操作是否意味着需要两次循环?事实上,类库设计精妙,只需对艺术家列表迭代一次。
通常,在Java中调用一个方法,计算机会随即执行操作:比如,System.out.println("Hello World");会在终端上输出一条信息。Stream里的一些方法却略有不同,它们虽是普通的Java方法,但返回的Stream对象却不是一个新集合,而是创建新集合的配方。现在,尝试思考一下例3-4中代码的作用,一时毫无头绪也没关系,稍后会详细解释。
例3-4 只过滤,不计数
allArtists.stream().filter(artist -> artist.isFrom("London"));
这行代码并未做什么实际性的工作,filter只刻画出了Stream,但没有产生新的集合。像filter这样只描述Stream,最终不产生新集合的方法叫作惰性求值方法;而像count这样最终会从Stream产生值的方法叫作及早求值方法。
如果在过滤器中加入一条println语句,来输出艺术家的名字,就能轻而易举地看出其中的不同。例3-5对例3-4作了一些修改,加入了输出语句。运行这段代码,程序不会输出任何信息!
例3-5 由于使用了惰性求值,没有输出艺术家的名字
allArtists.stream().filter(artist -> {System.out.println(artist.getName());return artist.isFrom("London");});
如果将同样的输出语句加入一个拥有终止操作的流,如例3-3中的计数操作,艺术家的名字就会被输出(见例3-6)。
例3-6 输出艺术家的名字
long count = allArtists.stream().filter(artist -> {System.out.println(artist.getName());return artist.isFrom("London");}).count();
以披头士乐队的成员作为艺术家列表,运行上述程序,命令行里输出的内容如例3-7所示。
例3-7 显示披头士乐队成员名单的示例输出
John LennonPaul McCartneyGeorge HarrisonRingo Starr
判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是Stream,那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是它的合理之处。计数的示例也是这样运行的,但这只是最简单的情况:只含两步操作。
整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后调用一个build方法,这时,对象才被真正创建。
读者一定会问:“为什么要区分惰性求值和及早求值?”只有在对需要什么样的结果和操作有了更多了解之后,才能更有效率地进行计算。例如,如果要找出大于10的第一个数字,那么并不需要和所有元素去做比较,只要找出第一个匹配的元素就够了。这也意味着可以在集合类上级联多种操作,但迭代只需一次。
