Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。可是Java 9 只是一个过渡版本,因此建议安装JDK 10。java
许多方法返回元素序列(sequence)。在Java 8以前,一般方法的返回类型是Collection
,Set
和List
这些接口;还包括Iterable
和数组类型。一般,很容易决定返回哪种类型。规范(norm)是集合接口。若是该方法仅用于启用for-each循环,或者返回的序列不能实现某些Collection
方法(一般是contains(Object)
),则使用迭代(Iterable
)接口。若是返回的元素是基本类型或有严格的性能要求,则使用数组。在Java 8中,将流(Stream)添加到平台中,这使得为序列返回方法选择适当的返回类型的任务变得很是复杂。git
你可能据说过,流如今是返回元素序列的明显的选择,可是正如条目 45所讨论的,流不会使迭代过期:编写好的代码须要明智地结合流和迭代。若是一个API只返回一个流,而且一些用户想用for-each循环遍历返回的序列,那么这些用户确定会感到不安。这尤为使人沮丧,由于Stream接口在Iterable接口中包含惟一的抽象方法,Stream的方法规范与Iterable兼容。阻止程序员使用for-each循环在流上迭代的惟一缘由是Stream没法继承Iterable。程序员
遗憾的是,这个问题没有好的解决方法。 乍一看,彷佛能够将方法引用传递给Stream的iterator方法。 结果代码可能有点嘈杂和不透明,但并不是不合理:github
// Won't compile, due to limitations on Java's type inference for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) { // Process the process }
不幸的是,若是你试图编译这段代码,会获得一个错误信息:数组
Test.java:6: error: method reference not expected here for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
为了使代码编译,必须将方法引用强制转换为适当参数化的Iterable类型:框架
// Hideous workaround to iterate over a stream for (ProcessHandle ph : (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator)
此代码有效,但在实践中使用它太嘈杂和不透明。 更好的解决方法是使用适配器方法。 JDK没有提供这样的方法,可是使用上面的代码片断中使用的相同技术,很容易编写一个方法。 请注意,在适配器方法中不须要强制转换,由于Java的类型推断在此上下文中可以正常工做:ide
// Adapter from Stream<E> to Iterable<E> public static <E> Iterable<E> iterableOf(Stream<E> stream) { return stream::iterator; }
使用此适配器,可使用for-each语句迭代任何流:性能
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) { // Process the process }
注意,条目 34中的Anagrams
程序的流版本使用Files.lines
方法读取字典,而迭代版本使用了scanner
。Files.lines
方法优于scanner
,scanner
在读取文件时无声地吞噬全部异常。理想状况下,咱们也会在迭代版本中使用Files.lines
。若是API只提供对序列的流访问,而程序员但愿使用for-each语句遍历序列,那么他们就要作出这种妥协。学习
相反,若是一个程序员想要使用流管道来处理一个序列,那么一个只提供Iterable的API会让他感到不安。JDK一样没有提供适配器,可是编写这个适配器很是简单:翻译
// Adapter from Iterable<E> to Stream<E> public static <E> Stream<E> streamOf(Iterable<E> iterable) { return StreamSupport.stream(iterable.spliterator(), false); }
若是你正在编写一个返回对象序列的方法,而且它只会在流管道中使用,那么固然能够自由地返回流。相似地,返回仅用于迭代的序列的方法应该返回一个Iterable
。可是若是你写一个公共API,它返回一个序列,你应该为用户提供哪些想写流管道,哪些想写for-each语句,除非你有充分的理由相信大多数用户想要使用相同的机制。
Collection
接口是Iterable
的子类型,而且具备stream
方法,所以它提供迭代和流访问。 所以,Collection
或适当的子类型一般是公共序列返回方法的最佳返回类型。 数组还使用Arrays.asList
和Stream.of
方法提供简单的迭代和流访问。 若是返回的序列小到足以容易地放入内存中,那么最好返回一个标准集合实现,例如ArrayList
或HashSet
。 可是不要在内存中存储大的序列,只是为了将它做为集合返回。
若是返回的序列很大但能够简洁地表示,请考虑实现一个专用集合。 例如,假设返回给定集合的幂集(power set:就是原集合中全部的子集(包括全集和空集)构成的集族),该集包含其全部子集。 {a,b,c}的幂集为{{},{a},{b},{c},{a,b},{a,c},{b,c},{a,b , C}}。 若是一个集合具备n个元素,则幂集具备2n个。 所以,你甚至不该考虑将幂集存储在标准集合实现中。 可是,在AbstractList
的帮助下,很容易为此实现自定义集合。
诀窍是使用幂集中每一个元素的索引做为位向量(bit vector),其中索引中的第n位指示源集合中是否存在第n个元素。 本质上,从0到2n-1的二进制数和n个元素集和的幂集之间存在天然映射。 这是代码:
// Returns the power set of an input set as custom collection public class PowerSet { public static final <E> Collection<Set<E>> of(Set<E> s) { List<E> src = new ArrayList<>(s); if (src.size() > 30) throw new IllegalArgumentException("Set too big " + s); return new AbstractList<Set<E>>() { @Override public int size() { return 1 << src.size(); // 2 to the power srcSize } @Override public boolean contains(Object o) { return o instanceof Set && src.containsAll((Set)o); } @Override public Set<E> get(int index) { Set<E> result = new HashSet<>(); for (int i = 0; index != 0; i++, index >>= 1) if ((index & 1) == 1) result.add(src.get(i)); return result; } }; } }
请注意,若是输入集合超过30个元素,则PowerSet.of
方法会引起异常。 这突出了使用Collection
做为返回类型而不是Stream
或Iterable
的缺点:Collection
有int返回类型的size
的方法,该方法将返回序列的长度限制为Integer.MAX_VALUE
或231-1。Collection
规范容许size
方法返回231 - 1,若是集合更大,甚至无限,但这不是一个彻底使人满意的解决方案。
为了在AbstractCollection
上编写Collection
实现,除了Iterable
所需的方法以外,只须要实现两种方法:contains
和size
。 一般,编写这些方法的有效实现很容易。 若是不可行,多是由于在迭代发生以前未预先肯定序列的内容,返回Stream仍是Iterable的,不管哪一种感受更天然。 若是选择,可使用两种不一样的方法分别返回。
有时,你会仅根据实现的易用性选择返回类型。例如,假设但愿编写一个方法,该方法返回输入列表的全部(连续的)子列表。生成这些子列表并将它们放到标准集合中只须要三行代码,可是保存这个集合所需的内存是源列表大小的二次方。虽然这没有指数幂集那么糟糕,但显然是不可接受的。实现自定义集合(就像咱们对幂集所作的那样)会很乏味,由于JDK缺乏一个框架Iterator实现来帮助咱们。
然而,实现输入列表的全部子列表的流是直截了当的,尽管它确实须要一点的洞察力(insight)。 让咱们调用一个子列表,该子列表包含列表的第一个元素和列表的前缀。 例如,(a,b,c)的前缀是(a),(a,b)和(a,b,c)。 相似地,让咱们调用包含后缀的最后一个元素的子列表,所以(a,b,c)的后缀是(a,b,c),(b,c)和(c)。 洞察力是列表的子列表只是前缀的后缀(或相同的后缀的前缀)和空列表。 这一观察直接展示了一个清晰,合理简洁的实现:
// Returns a stream of all the sublists of its input list public class SubLists { public static <E> Stream<List<E>> of(List<E> list) { return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(SubLists::suffixes)); } private static <E> Stream<List<E>> prefixes(List<E> list) { return IntStream.rangeClosed(1, list.size()) .mapToObj(end -> list.subList(0, end)); } private static <E> Stream<List<E>> suffixes(List<E> list) { return IntStream.range(0, list.size()) .mapToObj(start -> list.subList(start, list.size())); } }
请注意,Stream.concat
方法用于将空列表添加到返回的流中。 还有,flatMap
方法(条目 45)用于生成由全部前缀的全部后缀组成的单个流。 最后,经过映射IntStream.range
和IntStream.rangeClosed
返回的连续int值流来生成前缀和后缀。这个习惯用法,粗略地说,流等价于整数索引上的标准for循环。所以,咱们的子列表实现似于明显的嵌套for循环:
for (int start = 0; start < src.size(); start++) for (int end = start + 1; end <= src.size(); end++) System.out.println(src.subList(start, end));
能够将这个for循环直接转换为流。结果比咱们之前的实现更简洁,但可能可读性稍差。它相似于条目 45中的笛卡尔积的使用流的代码:
// Returns a stream of all the sublists of its input list public static <E> Stream<List<E>> of(List<E> list) { return IntStream.range(0, list.size()) .mapToObj(start -> IntStream.rangeClosed(start + 1, list.size()) .mapToObj(end -> list.subList(start, end))) .flatMap(x -> x); }
与以前的for循环同样,此代码不会包换空列表。 为了解决这个问题,可使用concat
方法,就像咱们在以前版本中所作的那样,或者在rangeClosed
调用中用(int) Math.signum(start)
替换1。
这两种子列表的流实现均可以,但都须要一些用户使用流-迭代适配器( Stream-to-Iterable adapte),或者在更天然的地方使用流。流-迭代适配器不只打乱了客户端代码,并且在个人机器上使循环速度下降了2.3倍。一个专门构建的Collection实现(此处未显示)要冗长,但运行速度大约是个人机器上基于流的实现的1.4倍。
总之,在编写返回元素序列的方法时,请记住,某些用户可能但愿将它们做为流处理,而其余用户可能但愿迭代方式来处理它们。 尽可能适应两个群体。 若是返回集合是可行的,请执行此操做。 若是已经拥有集合中的元素,或者序列中的元素数量足够小,能够建立一个新的元素,那么返回一个标准集合,好比ArrayList。 不然,请考虑实现自定义集合,就像咱们为幂集程序里所作的那样。 若是返回集合是不可行的,则返回流或可迭代的,不管哪一个看起来更天然。 若是在未来的Java版本中,Stream
接口声明被修改成继承Iterable
,那么应该随意返回流,由于它们将容许流和迭代处理。