欢迎光临散文网 会员登陆 & 注册

effective java 3-第7章 lambda和stream[47]Stream优先于用Collection作为返回类型

2023-03-23 22:35 作者:CC挑灯夜读_谷  | 我要投稿

    许多方法都返回元素的序列。在Java 8之前,这类方法明显的返回类型是集合接口Collection、Set和List、Iterable以及数组类型。一般来说,很容易确定要返回这其中哪一种类型。标椎是一个集合接口。如果某个方法只为for-each 循环或者返回序列而存在,无法用它来实现一些Collection方法(一般是contains(Object)) 那么就用Iterable 接口吧。如果返回的元素是基本类型值,或者有严格的性能要求,就使用数组。在Java8 中增加了Stream ,本质上导致给序列化返回的方法选择适当返回类型的任务变得更复杂了。

    或许你曾听说过,现在Stream 是返回元素序列最明显的选择了,但如第45条所述,Stream 并没有淘汰迭代:要编写优秀的代码必须巧妙地将Stream与迭代结合起来启用。如果一个API 只返回一个Stream ,那些想要用for-each 循环遍历返回序列的用户肯定要失望了。因为Stream 接口只在 Iterable 接口中包含了唯一一个抽象方法,Stream 对于该方法的规范也适用于Iterable 的。唯一一个可以让程序员避免用for-each 循环遍历Stream 的是Stream 无法扩展 Iterable 接口。

    遗憾的是,这个问题还没有适当的解决办法。乍看之下,好像给Stream 的Iterator方法传入一个方法引用可以解决。这样得到的代码可能有点杂乱,不清晰,但也不算难以理解:

    

遗憾的是,如果想要编译这段代码,就会得到一条报错信息:

为了使代码能够进行编译,必须将方法引用转换成适当参数化的 Iterable

这个客户端代码可行,但是实际使用时过于杂乱、不清晰。更好的解决办法是使用适配器方法。JDK 没有提供这样的方法,但是编写起来很容易,使用在上述代码中内嵌的相同方法即可。注意,在适配器方法中没有必要进行转换,因为Java的类型引用在这里正好派上了用场:


    注意,第34条中 Anagrams 程序的Stream 版本是使用 Files.lines 方法读取词典,而迭代版本则使用了扫描器(scanner)。Files.lines 方法优于扫描器,因为后者摸摸底吞掉了在读取文件过程中遇到的所有异常。最理想的方式是在迭代版本中也使用Files.lines。这是程序员在特定情况下所做的一种妥协,比如当API 只有Stream 能访问序列,而他们想通过for-each 语句遍历该序列的时候。

    反过来说,想要利用Stream pipeline 处理序列的程序员,也会被只提供Iterable的API 搞得束手无策。同样地JDK 没有提供适配器,但是编写起来也很容易:

    如果在编写一个返回对象序列的方法时,就知道它只在Stream pipeline 中使用,当然就可以放心地返回Stream 了。同样地,当返回序列的方法只在迭代中使用时,则应该返回Iterable。但如果是用公共的API 返回序列,则应该为那些想要编写Stream pipeline,以及想要编写for-each 语句的用户分别提供,除非有足够的理由相信大多数用户都想要使用相同的机制。

    Collection接口是 Iterable 的一个子类型,它有一个stream 方法,因此提供和了迭代和stream 访问。对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型。数组也通过Arrays.asList 和Stream.of 方法提供了简单的迭代和stream 访问。如果返回的序列足够小,容易存储,或许最好返回标准的集合实现,如 ArrayList 和HashSet。但是千万别在内存中保存巨大的序列,将它作为集合返回即可

    如果返回的序列很大,但是能被准确表述,可以考虑实现一个专用的集合。假设想要返回一个指定集合的幂集(power set),其中包含它所有的子集。{a,b,c} 的幂集是{ {},{a},{b},{c},{a,b} ,{a,c}, {b,c},{b,c}, {a,b,c}  }。如果集合中有n个元素,它的幂集就有2n个。因此,不必考虑将幂集保存在标准的集合实现中。但是,有了AbstractList 的协助,为此实现定制集合就很容易了。

    技巧在于,用幂集中每个元素的索引作为位向量,在索引中排第n位,表示源集合中第n位元素存在或不存在。实质上,在二进制数0至2n-1 和有n位元素的集合的幂集之间,有一个自然映射。代码如下:

    

    注意,如果输入值集合中超过30个元素,PowerSet.of 会抛出异常。这正是用Collection而不是用Stream 或Iterable 作为返回类型的缺点:Collection 有一个返回 int 类型的size 方法,它限制返回的序列长度为Integer.MAX_VALUE 或者 2^31 -1 。如果集合更大,甚至无限大,Collection规范确实允许 size 方法返回2^31 -1 ,但这并非是最令人满意的解决方案。

    为了在AbstractCollection 上编写一个 Collection 实现,除了 Iterable 必须的那一个方法之外,只需要再实现两个方法: contains和size 。这些方法经常很容易编写出高效的实现。如果不可行,或许是因为没有在迭代发生之前先确定序列的内容,返回Stream 或者 Iterable,感觉哪一种更自然即可。如果能选择,可以尝试着分别用两个方法返回。    

    有时候在选择返回类型时,只需要看是否易于实现即可。例如,要编写一个方法,用它返回一个输入列表的所有(相邻的)子列表。它只用三行代码来生成这些子列表,并将它们放在一个标准的集合中,但存放这个集合所需的内存是源列表大小的平方。这虽然没有幂集那么糟糕,但显然也是无法接受的。像给幂集实现定制的集合那样,确实很繁琐,这个可能还更甚,因为JDK没有提供基本的Iterator实现来支持。

    但是,实现输入列表的所有子列表的Stream 是很简单的,尽管它确实需要有点洞察力。我们把包含列表第一个元素的子列表称作列表的前缀。例如 (a,b,c) 的前缀是(a )  (a ,b) 和 (a, b ,c) 。同样的,把包含最后一个元素的子列表称作后缀。因此,(a, b, c) 的后缀是 (a, b ,c)  (b, c,) 和 (c )。 考察洞察力的是,列表的子列表不过是前缀的后缀(或者说后缀的前缀)和空列表。这一发现直接带来了一个清晰且简洁的实现:

    

    注意,它用Stream.concat 方法将空列表添加到返回的Stream 。另外还用flatMap方法(详见45条)生成了一个包含了所有前缀的所有后缀的Stream。最后,通过映射IntStream.range 和 IntStream.rangeClosed返回的连续int 值的 Stream,生成了前缀和后缀。通俗地将,这一术语的意思就是指数为整数的标准for 循环的Stream 版本。因此,这个子列表实现本质上与明显的嵌套式for 循环相类似:

        

    这个for 循环也可以直接翻译成一个Stream。这样得到的结果比前一个实现更加简洁,但是可读性稍微差了一点。它本质上与45条中笛卡尔积的Stream 代码相类似:

    

    像前面的for 循环一样,这段代码也没有发出空列表。为了修正这个错误,也应该使用concat,如前一个版本中那样,或者用ranClosed调用中的(int)Math.signum(start) 代替1。

    子列表的这些Stream 实现都很好,但这两者都需要用户在任何更适合迭代的地方,采用Stream-to-Iterable适配器,或者用Stream。Stream-to-Iterable适配器不仅打断了客户端代码,在我的机器(本书作者 Joshua Bloch)上循环的速度还降低了2.3倍。专门构建的Collection实现(此处没有展示)相当繁琐,但是运行速度在我的机器上比基于Stream 的实现快了约1.4倍。

    总而言之,在编写返回一系列元素的方法时,要记住有些用户可能想要当做Stream 处理,而其他用户可能想要迭代。要尽量两边兼顾。如果可以返回集合,就返回集合。如果集合中已经有元素,或者序列中的元素数量很少,足以创建一个新的集合,那么就返回一个标准的集合,如ArrayList。否则就要考虑实现一个定制的集合,如幂集(power set)范例中所示。如果无法返回集合,就返回Stream或者 Iterable,感觉哪一种更自然即可。如果在未来的Java发行版本中,Stream 接口声明被修改成扩展了Iterable接口,就可以放心地返回Stream了,因为它们允许进行Stream 处理和迭代。

effective java 3-第7章 lambda和stream[47]Stream优先于用Collection作为返回类型的评论 (共 条)

分享到微博请遵守国家法律