高效Java(第三版) Effective Java
1. 考虑使用静态工厂方法替代构造方法 2. 当构造方法参数过多时使用builder模式 3. 使用私有构造方法或枚类实现Singleton属性 4. 使用私有构造方法执行非实例化 5. 使用依赖注入取代硬连接资源 6. 避免创建不必要的对象 7. 消除过期的对象引用 8. 避免使用Finalizer和Cleaner机制 9. 使用try-with-resources语句替代try-finally语句 10. 重写equals方法时遵守通用约定 11. 重写equals方法时同时也要重写hashcode方法 12. 始终重写 toString 方法 13. 谨慎地重写 clone 方法 14. 考虑实现Comparable接口 15. 使类和成员的可访问性最小化 16. 在公共类中使用访问方法而不是公共属性 17. 最小化可变性 18. 组合优于继承 19. 如果使用继承则设计,并文档说明,否则不该使用 20. 接口优于抽象类 21. 为后代设计接口 22. 接口仅用来定义类型 23. 优先使用类层次而不是标签类 24. 优先考虑静态成员类 25. 将源文件限制为单个顶级类 26. 不要使用原始类型 27. 消除非检查警告 28. 列表优于数组 29. 优先考虑泛型 30. 优先使用泛型方法 31. 使用限定通配符来增加API的灵活性 32. 合理地结合泛型和可变参数 33. 优先考虑类型安全的异构容器 34. 使用枚举类型替代整型常量 35. 使用实例属性替代序数 36. 使用EnumSet替代位属性 37. 使用EnumMap替代序数索引 38. 使用接口模拟可扩展的枚举 39. 注解优于命名模式 40. 始终使用Override注解 41. 使用标记接口定义类型 42. lambda表达式优于匿名类 43. 方法引用优于lambda表达式 44. 优先使用标准的函数式接口 45. 明智审慎地使用Stream 46. 优先考虑流中无副作用的函数 47. 优先使用Collection而不是Stream来作为方法的返回类型 48. 谨慎使用流并行 49. 检查参数有效性 50. 必要时进行防御性拷贝 51. 仔细设计方法签名 52. 明智而审慎地使用重载 53. 明智而审慎地使用可变参数 54. 返回空的数组或集合不要返回null 55. 明智而审慎地返回Optional 56. 为所有已公开的API元素编写文档注释 57. 最小化局部变量的作用域 58. for-each循环优于传统for循环 59. 熟悉并使用Java类库 60. 需要精确的结果时避免使用float和double类型 61. 基本类型优于装箱的基本类型 62. 当有其他更合适的类型时就不用字符串 63. 注意字符串连接的性能 64. 通过对象的接口引用对象 65. 接口优于反射 66. 明智谨慎地使用本地方法 67. 明智谨慎地进行优化 68. 遵守普遍接受的命名约定 69. 仅在发生异常的条件下使用异常 70. 对可恢复条件使用检查异常,对编程错误使用运行时异常 71. 避免不必要地使用检查异常 72. 赞成使用标准异常 73. 抛出合乎于抽象的异常 74. 文档化每个方法抛出的所有异常 75. 在详细信息中包含失败捕获信息 76. 争取保持失败原子性 77. 同步访问共享的可变数据 78. 避免过度同步 79. EXECUTORS, TASKS, STREAMS 优于线程 80. 优先使用并发实用程序替代wait和notify 81. 线程安全文档化 82. 明智谨慎地使用延迟初始化 83. 不要依赖线程调度器 84. 其他替代方式优于Java本身序列化 85. 非常谨慎地实现SERIALIZABLE接口 86. 考虑使用自定义序列化形式 87. 防御性地编写READOBJECT方法 88. 对于实例控制,枚举类型优于READRESOLVE 89. 考虑序列化代理替代序列化实例

优先使用Collection而不是Stream来作为方法的返回类型

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。

47. 优先使用Collection而不是Stream来作为方法的返回类型

许多方法返回元素序列(sequence)。在Java 8之前,通常方法的返回类型是CollectionSetList这些接口;还包括Iterable和数组类型。通常,很容易决定返回哪一种类型。规范(norm)是集合接口。如果该方法仅用于启用for-each循环,或者返回的序列不能实现某些Collection方法(通常是contains(Object)),则使用迭代(Iterable)接口。如果返回的元素是基本类型或有严格的性能要求,则使用数组。在Java 8中,将流(Stream)添加到平台中,这使得为序列返回方法选择适当的返回类型的任务变得非常复杂。

你可能听说过,流现在是返回元素序列的明显的选择,但是正如条目 45所讨论的,流不会使迭代过时:编写好的代码需要明智地结合流和迭代。如果一个API只返回一个流,并且一些用户想用for-each循环遍历返回的序列,那么这些用户肯定会感到不安。这尤其令人沮丧,因为Stream接口在Iterable接口中包含唯一的抽象方法,Stream的方法规范与Iterable兼容。阻止程序员使用for-each循环在流上迭代的唯一原因是Stream无法继承Iterable。

遗憾的是,这个问题没有好的解决方法。 乍一看,似乎可以将方法引用传递给Stream的iterator方法。 结果代码可能有点嘈杂和不透明,但并非不合理:

// 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的类型推断在此上下文中能够正常工作:

// 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方法优于scannerscanner在读取文件时无声地吞噬所有异常。理想情况下,我们也会在迭代版本中使用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.asListStream.of方法提供简单的迭代和流访问。 如果返回的序列小到足以容易地放入内存中,那么最好返回一个标准集合实现,例如ArrayListHashSet。 但是不要在内存中存储大的序列,只是为了将它作为集合返回

如果返回的序列很大但可以简洁地表示,请考虑实现一个专用集合。 例如,假设返回给定集合的幂集(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作为返回类型而不是StreamIterable的缺点:Collection有int返回类型的size的方法,该方法将返回序列的长度限制为Integer.MAX_VALUE或231-1。Collection规范允许 size方法返回231 - 1,如果集合更大,甚至无限,但这不是一个完全令人满意的解决方案。

为了在AbstractCollection上编写Collection实现,除了Iterable所需的方法之外,只需要实现两种方法:containssize。 通常,编写这些方法的有效实现很容易。 如果不可行,可能是因为在迭代发生之前未预先确定序列的内容,返回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.rangeIntStream.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,那么应该随意返回流,因为它们将允许流和迭代处理。