第二章 创建和销毁对象
第1条、用静态工厂方法替代构造器
它只是一个返回类实例的静态方法,不同于设计模式的工厂模式。
优点:
1.静态工厂方法有名称,易读、易用
一个类只能有一个带有指定签名的构造器,由于静态工厂方法有名称,所以它们不受上述限制。
public static Boolean valueOf(boolean b) {
return b? Boolean.TRUE : Boolean.FALSE;
}
2.可以灵活控制新对象的创建, 如单子模式、享元模式。
方法签名:方法的名字和参数列表,方法签名不包括方法的返回类型
享元模式:(Flyweight Pattern)运用共享技术有效的支持大量细粒度的对象。
3.可以返回原返回类型的任何子类型的对象。
4.所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
5.方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
缺点:
1. 类如果不含公有的或者受保护的构造器,就不能被子类化。
2.程序员很难发现它们,在API 文档中,它们没有像构造器那样在API 文档中明确标识出来, 因此对于提供了静态工厂方法而不是构造器的类来说,要想查明如何实例化一个类是非常困难的。
第2条:遇到多个构造器参数时要考虑使用构建器
静态工厂和构造器有个共同的局限性,不能很好地扩展到大量的可选参数
1. 重叠构造器( telescoping cons tructor )模式,在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,依此类推,最后一个构造器包含所有可选的参数。
但是当有许多参数的时候,客户端代码会很难缩写,并且仍然较难以阅读
2.JavaBeans 模式,在这种模式下,先调用一个无参构造器来创建对象,然后再调用setter 方法来设置每个必要的参数,以及每个相关的可选参数。
属性不可控
3.建造者模式。它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个buil der 对象。然后客户端在bui lder 对象上调用类似于setter 的方法,来设置每个相关的可选参数。最后客户端调用无参的build 方法来生成通常是不可变的对象。
如果类的构造器或者静态工厂中具有多个参数,设计这种类时, Builder模式就是一种不错的选择, 特别是当大多数参数都是可选或者类型相同的时候。与使用重叠构造器模式相比,使用Bui lder 模式的客户端代码将更易于阅读和编写,构建器也比JavaBeans 更加安全。
第3 条:用私有构造器或者枚举类型强化Singleton 属性
静态实例属性、静态实例方法、枚举
第4 条:通过私有构造器强化不可实例化的能力
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() { //副作用,它使得该类不能被子类化。
throw new AssertionError();//避免内部调用构造器
} ... // Remainder omitted
}
第5 条:优先考虑依赖注人来引用资源
依赖注入( dependency injection )的一种形式:词典( dictionary )是拼写检查器的一个依赖( depend ency ),在创建拼写检查器时就将词典注入( injected )其中
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
静态工具类和Singleton 类不适合于需要引用底层资源的类
第6 条:避免创建不必要的对象
通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。
如:
1.静态工厂方法Boolean. valueOf (String )几乎总是优先于构造器Boolean(String )
注意, 构造器Boolean(String )在Java 9 中已经被废弃了。
2. String s = new String("bikini"); // DON'T DO THIS!
改进后版本: String s = "bikini";
3. // Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
改进后版本
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
注意,与本条目对应的是第50 条中有关“保护性拷贝”( defensive copying )的内容。在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的Bug 和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。
第7 条:消除过期的对象引用
1、程序中过期引用,清除过期引用: elements[size] = null; // Eliminate obsolete reference
2、缓存的生命周期,长期需定期清理
3、监听器和回调:确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用( weakreference )
第8 条:避免使用终结方法和清除方法
1、终结方法(Finalizer):如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会终止。正常情况下,未被捕获的异常将会使线程终止,并打印出战轨迹( Stack Trace ),但是,如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。清除方法没有这个问题,因为使用清除方法的一个类库在控制它的线程。
java中添加 清除方法(cleaner),清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。
2、建议用try-with-resources关闭销毁不用的对象,
第9 条: try-with-resources 优先于try-finally
1、try-with-resources java7引入,要使用这个构造的资源,必须先实现AutoCloseable 接口其中包含了单个返回void 的close 方法
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
在处理必须关闭的资源时,始终要优先考虑用try- with-resources ,而不是用try-finally 。代码简洁且不会摸出重要异常
第三章 对于所有对象都通用的方法
第10 条:覆盖equals 时请遵守通用约定
覆盖equals,通常应用于值类,如String、Integer
枚举不需要覆盖equals,对于这种类,逻辑相同 和 对象等同是一回事。
equals 方法实现了等价关系( equi va lence relation ),其属性如下:
自反性( reflexive ) : 对于任何非null 的引用值x x . equals(x )必须返回true 。
对称性( symmetric ):对于任何非null 的引用值x 和y ,当且仅当y.equals(x )返回true 时', x.equals(y )必须返回true 。
传递性( transitive ) : 对于任何非null 的引用值x 、y 和z ,如果x.equals(y )返回true ,并且y.equals(z )也返回true ,那么x.equals(z )也必须返回true
一致性( consistent ) : 对于任何非nu ll 的引用值x 和y ,只要equals 的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y )就会一致地返回true,或者一致地返回false 。
对于任何非null 的引用值x, x.equals (null )必须返回false 。
第11 条:覆盖equals 时总要覆盖hashCode
在每个覆盖了equals 方法的类中, 都必须覆盖hashCode 方法。如果不这样做的话,就会违反hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作。下面是约定的内容,摘自Object 规范:
- 在应用程序的执行期间,只要对象的equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用, hashCode 方法都必须始终返回同一个值。在一个应用程序与另一个程序的执行过程中,执行hashCode 方法所返回的值可以不一致。
- 如果两个对象根据equals(Object )方法比较是相等的,那么调用这两个对象中的hashCode 方法都必须产生同样的整数结果。
- 如果两个对象根据equals(Object )方法比较是不相等的,那么调用这两个对象中的hashCode 方法,则不一定要求hashCode 方法必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表( hashtable )的性能。
计算hash值采用31 有个很好的特性,用移位和减法来代替乘法,可以得到更好的性能:
31 *i = ( i < < 5 ) - i 。
31 = 11111, 31*2-2 = 1000000 -10=111110=62
现代的虚拟机可以自动完成这种优化。
hashCode案例:
1、// Typical hashCode method
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
2、Objects 类有一个静态方法,它带有任意数量的对象,并为它们返回一个散列码。性能比自己的写的方法慢。
// One-line hashCode method - mediocre performance
@Override public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
3、如果一个类是不可变的,并且计算散列码的开销也比较大, 就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。Jaa 类库中许多类,比如String 和Integer ,都可以把它们的hashCode 方法返回的确切
// hashCode method with lazily initialized cached hash code
private int hashCode; // Automatically initialized to 0
@Override public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
return result;
}
不要试图从散列码计算中排除掉一个对象的关键域来提高性能。
第12 条:始终要覆盖toString
提供好的t。String 实现可以便类用起来更加舒适,使用了这个类的系统也更易于调试。
在实际应用中, toString 方法应该返回对象中包含的所有值得关注的信息;可以指定具体类的格式,如电话号码,指定格式的好处是,它可以被用作一种标准的、明确的、适合人阅读的对象表示法,通常最好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易地在对象及其字符串表示法之间来回转换。指定toString 返回值的格式也有不足之处:如果这个类已经被广泛使用,就必须始终如一地坚持这种格式。如果不指定格式,就可以保留灵活性,便于在将来的发行版本中增加信息,或者改进格式。
无论是否决定指定格式,都应该在文档中明确地表明你的意图。
第13 条: 谨慎地覆盖clone
Cloneable决定了Object中受保护的clone 方法实现的行为:如果一个类实现了Cloneable, Object 的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException 异常。事实上,实现Cloneable 接口的类是为了提供一个功能适当的公有的clone 方法。
Java 支持协变返回类型( covariant return type ) 。换句话说,目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类了
Entry 类中的深度拷贝方法递归地调用它自身,以便拷贝整个链表(它是链表的头节点) 。虽然这种方法很灵活,如果散列桶不是很长, 也会工作得很好,但是,这样克隆一个链表并不是一种好办法,因为针对列表中的每个元素,它都要消耗一段枝空间。如果链表比较长,这很容易导致枝溢出。为了避免发生这种情况,可以在deepCopy 方法中用迭代( iteration )代替递归( recursion ) :
// Recursively copy the linked list headed by this Entry
Entry deepCopy() {
return new Entry(key, value,next == null ? null : next.deepCopy());
}
// Iteratively copy the linked list headed by this Entry
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
对象拷贝的更好的办法是提供一个拷贝构造器( copy constructor)或拷贝工厂( copy factory ); 最好利用clone 方法复制数组。
第14 条:考虑实现Comparable 接口
在下面的代码中,依赖于 String 类实现了 Comparable 接口,去除命令行参数输入重复的字符串,并按照字母顺序排序, TreeSet中元素实现了 Comparable接口,根据comparaTo方法返回值进行排序
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
compareTo 方法的通用约定与equals 方法的约定相似:
将这个对象与指定的对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException 异常。
如果你想为一个实现了Comparable 接口的类增加值组件,请不要扩展这个类;而是要编写一个不相关的类,其中包含第一个类的一个实例。然后提供一个“视图”( view )方法返回这个实例。这样既可以让你自由地在第二个类上实现compare To 方法,同时也允许它的客户端在必要的时候,把第二个类的实例视同第一个类的实例
集合接口(Collection, Set 或Map )的通用约定是按照equals方法来定义的,但是有序集合使用了compareTo 方法而不是equals 方法
// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
第34 条: 用enum 代替int 常量
// Enum type that switches on its own value - questionable
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// Do the arithmetic operation represented by this constant
public double apply(double x, double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Unknown op: "+this);
}
}
有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的appl y 方法,并在特定于常量的类主体( constant-specific class body )中,用具体的方法覆盖每个常量的抽象apply 方法。这种方法被称作特定于常量的方法实现( constant-specific method implementation ),特定于常量的方法实现可以与特定于常量的数据结合起来。 :
枚举类型中的抽象方法必须被它的所有常量中的具体方法所覆盖。
// Enum type with constant-specific class bodies and data
public enum Operation {
PLUS("+") {public double apply(double x, double y) { return x + y; }},
MINUS("-") {public double apply(double x, double y) { return x - y; }},
TIMES("*") {public double apply(double x, double y) { return x * y; }},
DIVIDE("/") {public double apply(double x, double y) { return x / y; }};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
}
每当需要一组固定常量.并且在编译时就知道其成员的时候,就应该使用枚举
第42 条: Lambda 优先于匿名类
在Java 8 中,增加了函数接口( functional interface )、Lambda 和方法引用( methodreference ),使得创建函数对象(且mction object )变得很容易。与此同时,还增加了StreamApi,为处理数据元素的序列提供了类库级别的支持。
自从1997 年发布JDK 1.1 以来,创建函数对象的主要方式是通过匿名类。下面是一个按照字符串的长度对字符串列表进行排序的代码片段,它用一个匿名类创建了排序的比较函数(加强排列顺序):
函数对象:用带有单个抽象方法的接口(或者几乎不用的抽象类)作为函数类型( function type ),表示函数或者要采取的动作
// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
在Java 8 中,允许利用Lambda 表达式( Lambda expression ,简称Lambda )创建这些接口的实例。Lambda 类似于匿名类的函数,但是比它简洁得多。以下是上述代码用Lambda 代替匿名类之后的样子。
// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));
注意, Lambda 的类型( Comparator<String >)、其参数的类型( s 1 和s2 ,两个都是String )及其返回值的类型( int ),都没有出现在代码中。编译器利用一个称作类型推导( type inference )的过程,根据上下文推断出这些类型。
Lambda 限于函数接口。如果想创建抽象类的实例,可以用匿名类来完成,而不是用Lambda。
Lambda 无法获得对自身的引用。在Lambda 中,关键字this 是指外围实例,这个通常正是你想要的。在匿名类中,关键字this 是指匿名类实例。
Lambda 没有名称和文档;如果一个计算本身不是自描述的, 或者超出了几行, 那就不要把它放在一个Lambda 中。对于Lambda而言,一行是最理想的, 三行是合理的最大极限。如果违背了这个规则,可能对程序的可读性造成严重的危害。
第43 条:方法引用优先于Lambda
与匿名类相比, Lambda 的主要优势在于更加简洁。Java 提供了生成比Lambda 更简洁函数对象的方法: 方法引用( mothod reference ) 。
map.merge(key, 1, (count, incr) -> count + incr);
这行代码中使用了merge 方法,这是Java 8 版本在Map 接口中添加的。如果指定的键没有映射, 该方法就会插入指定值;如果有映射存在, merge 方法就会将指定的函数应用到当前值和指定值上,并用结果覆盖当前值。这行代码代表了merge 方法的典型用例。
从Java 8 开始, Integer(以及所有其他的数字化基本包装类型)提供了一个名为sum 的静态方法,它的作用也同样是求和。我们只要传人一个对该方法的引用,就可以更轻松地得到相同的结果:
map.merge(key ,1, Integer:: sum );
使用方法引用通常能够得到更加简短、清晰的代码。如果Lambda 太长,或者过于复杂,还有另一种选择: 从Lambda 中提取代码,放到一个新的方法中,并用该方法的一个引用代替Lambda。
只要方法引用更加简洁、清晰,就用方法引用;如果方法引用并不简洁,就坚持使用Lambda 。
service.execute(GoshThisClassNameIsHumongous::action);
service.execute(() -> action());
第44 条:坚持使用标准的函数接口
只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。
java.util.Function 中共有43 个接口。别指望能够全部记住它们,但是如果能记住其中6 个基础接口,必要时就可以推断出其余接口了。基础接口作用于对象引用类型。Operator 接口代表其结果与参数类型一致的函数。Predicate接口代表带有一个参数并返回一个boolean 的函数。Function 接口代表其参数与返回的类型不一致的函数。Supplier 接口代表没有参数并且返回(或“提供”)一个值的函数。最后, Consumer 代表的是带有一个函数但不返回任何值的函数,相当于消费掉了其参数。这6 个基础函数接口概述如下:
UnaryOperator和BinaryOperator 分别对应单元算子和二元算子。
Interface | Function Signature | Example |
UnaryOperator<T> | T apply(T t) | String::toLowerCase |
BinaryOperator<T> | T apply(T t1, T t2) | BigInteger::add |
Predicate<T> | boolean test(T t) | Collection::isEmpty |
Function<T,R> | R apply(T t) | Arrays::asList |
Supplier<T> | T get() | Instant::now |
Consumer<T> | void accept(T t) | System.out::println |
千万不要用带包装类型的基础函数接口来代替基本函数接口,基本类型优于装箱基本类型
如果你所需要的函数接口与Comparator 一样具有一项或者多项以下特征, 则必须认真考虑自己编写专用的函数接口,而不是使用标准的函数接口:
- 通用,并且将受益于描述性的名称。
- 具有与其关联的严格的契约。
- 将受益于定制的缺省方法。
缺省方法:其允许在接口中增加新的方法,并在接口实现中可用,与正常方法不同,在方法声明之前加上default关键字,同时提供实现,因此无需修改实现类。
函数接口用@ FunctionalInterface 注解,它是是针对lambda设计的,他的接口只有一个抽象方法。
最后,不要在相同的参数位置,提供不同的函数接口来进行多次重载,这样可能在客户端导致歧义。
总而言之,既然Java 有了Lambda ,就必须时刻谨记用Lambda 来设计API 。输入时接受函数接口类型,并在输出时返回之。一般来说,最好使用j ava.util.function.Function 中提供的标准接口,但是必须警惕在相对罕见的几种情况下,最好还是自己编写专用的函数接口。
第45 条:谨慎使用Stream
在Java 8 中增加了Stream API ,简化了串行或并行的大批量操作。这个API 提供了两个关键抽象: Stream (流)代表数据元素有限或无限的顺序, Stream pipeline (流管道)则代表这些元素的一个多级计算。Stream 中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int、long 和double 。
一个Stream pipeline 中包含一个源Stream ,接着是0 个或者多个中间操作( intermediat巳operation )和一个终止操作( terminal operation ) 。每个中间操作都会通过某种方式对Stream进行转换,例如将每个元素映射到该元素的函数,或者过滤掉不满足某些条件的所有元素。所有的中间操作都是将一个Stream 转换成另一个Stream ,其元素类型可能与输入的Stream一样,也可能不同。终止操作会在最后一个中间操作产生的Stream 上执行一个最终的计算。例如将其元素保存到一个集合中,并返回某一个元素,或者打印出所有元素等。
Stream pipeline 通常是lazy 的:直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远都不会被计算。
在默认情况下, Stream pipeline 是按顺序运行的。要使pipelin巳并发执行,只需在该pipeline 的任何Stream 上调用parallel 方法即可,但是通常不建议这么做
// Prints all large anagram groups in a dictionary iteratively
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
}
}
for (Set<String> group : groups.values()) {
if (group.size() >= minGroupSize)
System.out.println(group.size() + ": " + group);
}
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
这里使用了Java 8 中新增的computeIfAbsent 方法。这个方法会在映射中查找一个键:如果这个键存在,该方法只会返回与之关联的值。如果键不存在,该方法就会对该键运用指定的函数对象算出一个值,将这个值与键关联起来,并返回计算得到的值。computeIfAbsent方法简化了将多个值与每个键关联起来的映射实现。
// Tasteful use of streams enhances clarity and conciseness
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
} // alphabetize method is the same as in original version
}
即使你之前没怎么接触过Stream ,这段程序也不难理解。它在try-with-resources 块中打开词典文件,获得一个包含了文件中所有代码的Stream 。Stream 变量命名为words,是建议S tream 中的每个元素均为单词。这个Stream 中的pipe line 没有中间操作;它的终止操作将所有的单词集合到一个映射中,按照它们的字母排序形式对单词进行分组(详见第46 条) 。这个映射与前面两个版本中的是完全相同的。随后,在映射的values ()视图中打开了一个新Stream<List<String >> 。当然,这个Stream 中的元素都是换位词分组。Stream 进行了过滤,把所有分组大小小于minGroupSize 的单词都去掉了,最后,通过终止操作的forEach 打印出剩下的分组。
在没有显式类型的情况下,仔细命名Lambda参数, 这对于Stream pipeline的可读性至关重要。
Java 不支持基本类型的char Stream,最好避免利用Stream 来处理char值。
如本条目中的范例程序所示, Stream pipeline 利用函数对象(一般是Lambda 或者方法引用)来描述重复的计算,而迭代版代码则利用代码块来描述重复的计算。
下列工作只能通过代码块,而不能通过函数对象来完成:
- 从代码块中,可以读取或者修改范围内的任意局部变量;从Lambda 则只能读取final 或者有效的final 变量[ JLS 4.12.4 ],并且不能修改任何local 变量。
- 从代码块中,可以从外国方法中return 、break 或continue 外围循环,或者抛出该方法声明要抛出的任何受检异常;从Lambda 中则完全无法完成这些事情。
stream可以使得完成下列这些工作变得易如反掌:
- 统一转换元素的序列
- 过滤元素的序列
- 利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序
- 将元素的序列存放到一个集合中,比如根据某些公共属性进行分组
- 搜索满足某些条件的元素的序列
总之,Stream 和 迭代代码块选择合适的用,不知选哪个时,可以两个都试试。
第46 条:优先选择Stream 中无副作用的函数
例:构建一张表格,显示这些单词在一个文本文件中出现的频率:
// Uses the streams API but not the paradigm--Don't do this!
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
使用了Stream 、Lambda 和方法引用,并且得出了正确的答案。简而言之,这根本不是Stream 代码;只不过是伪装成Stream 代码的迭代式代码。这段代码利用一个改变外部状态(频率表)的Lambda ,完成了在终止操作的forEach 中的所有工作。forEach 操作的任务不只展示由Stream 执行的计算结果,这在代码中并非好事,改变状态的Lambda 也是如此。
// Proper use of streams to initialize a frequency table
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
这个代码片段的作用与前一个例子一样,只是正确使用了Stream API ,变得更加简洁、清晰。
Stream终止操作forEach 应该只用于报告St ream 计算的结果,而不是执行计算。有时候,也可以将forEach 用于其他目的,比如将Stream 计算的结果添加到之前已经存在的集合中去。
将Stream 的元素集中到一个真正的Collection 里去的收集器比较简单。有三个这样的收集器: toList ()、toSet ()和toCollection(collectionFactory ) 。它们分别返回一个列表、一个集合和程序员指定的集合类型。
总而言之,编写Stream pipeline 的本质是无副作用的函数对象。这适用于传入Stream及相关对象的所有函数对象。终止操作中的forEach 应该只用来报告由Stream 执行的计算结果,而不是让它执行计算。为了正确地使用Stream ,必须了解收集器。最重要的收集器工厂是toList 、toSet 、toMap 、groupingBy 和joining 。