第42条 Lambda优先于匿名类
42.1 匿名内部类
//1. 匿名内部类适合于需要函数对象的经典面向对象设计模式,特别是策略模式
//2. 为什么下面例子是策略模式的应用呢,因为sort方法,可以根据传入Comparator对象的不同,拥有不同的行为,让算法的变化,独立于使用算法的客户
//3. Comparator接口,将排序的行为封装起来,代表抽象策略,而具体的实现叫做具体策略
//4. 但匿名内部类的写法太繁琐,直接导致在Java中进行函数式编程太复杂
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
42.2 Lambda表达式
- Java8中,为了全力支撑函数式编程,首先建立了一个概念,即带有单个抽象方法的接口(函数式接口)是特殊的,应该特殊对待,因此建立了特殊的语法糖,简化它的创建代码,就形成了Lambda表达式
- 代码
//整个Lambda表达式代表的类型Comparator,以及s1、s2的类型String,还有重写的compare方法的返回值类型int,都没有自己定义,都是编译器利用上下文关系,自动进行类型推导得出的
//应尽量在定义时删除Lambda参数的类型,让编译器自己去进行类型推导,除非它能让程序变的更清晰,或者编译器确实无法推导出该类型
//类型推导的大部分信息是从传入的参数的泛型中获取到的信息,如果类型推导得到的信息不够多,就需要自己定义类型信息,因此为了方便,应尽可能使用泛型。例如下面方法中words如果是由原生态类型List定义的,那么编译器不知道s1是String类型,s1.length编译报错,必须自己定义s1为String,因此应使用带泛型的List<String>定义集合
Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
- lambda表达式传给枚举值的实例域(称为枚举实例域,实际上思路与策略枚举模式相同)从而优化代码
import java.util.function.DoubleBinaryOperator;
public enum Operation {
PLUS("+", (x, y) -> x + y), MINUS("-", (x, y) -> x - y), TIMES("*", (x, y) -> x * y), DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator op;
//DoubleBinaryOperator为Java8自带的函数式接口,其一般代表两个double值的运算
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() {
return symbol;
}
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
}
- 枚举实例域与特定于常量的类主体区别
- Lambda没有名称和文档,如果一个计算并不是一目了然的(因为缺文档,太复杂看不懂,还没文档描述),或行数太多(一般要求3行),就不应该放在Lambda中
- 枚举构造器是无法访问枚举的实例成员的,因此这个传入构造器的Lambda表达式,也无法访问枚举中的实例成员
42.3 构造器引用
Collections.sort(words, comparingInt(String::length));
42.4 Lambda表达式与匿名内部类对比
42.4.1 联系
- Lambda和匿名内部类,都相当于一个非静态内部类
- 非静态内部类中不允许定义静态成员,因此匿名内部类中无法定义static成员,而Lambda表达式本身就没有位置定义成员变量
- 非静态内部类定义在非静态方法a中时,为了方便的访问调用这个a方法的对象B,因此在该类内部会持有B对象的引用(在静态方法b中时,不会持有),被序列化时,会优先序列化对象B,而如果B没有实现Serializable接口,序列化会失败
- Lambda或匿名内部类的实例都无法被可靠的序列化和反序列化,因此尽可能不要序列化、反序列化他们,如果想序列化函数式接口的对象,例如Comparator的对象,那么先定义一个private static内部类,实现该接口,最后对该内部类的对象序列化、反序列化
42.4.2 区别
- Lambda表达式,只能创建函数式接口的对象
- Lambda表达式中,无法获取对自身的引用,即Lambda表达式中的this,指的是该表达式所在的方法的调用者。而匿名内部类中,this表示该匿名内部类对象本身
import java.io.Serializable;
@FunctionalInterface
interface TestInterface extends Serializable{
public void test(int a);
}
class TestClass{
public static void main(String[] args) {
TestInterface o = new TestInterface() {
@Override
public void test(int a) {
//打印:TestClass$1@2a098129,this表示当前匿名内部类的实例
System.out.println(this);
}
};
o.test(1);
TestClass aa = new TestClass();
aa.test();
}
public void test() {
TestInterface o1 = a->{
//打印:TestClass@387c703b,this为调用test方法的对象
System.out.println(this);
};
o1.test(12);
}
}
42.5 最佳实践
- 永远不要为函数式接口使用Lambda表达式
43 方法引用优先于Lambda
- 方法引用可以减少没必要的形参列表导致的样板代码
- 方法引用和Lambda,一般谁更简洁,更可读,就选用谁
- 方法引用比Lambda可读的场景
//1. Java8新增方法,当map中包含key对应的这个键,将原value与第二个参数1,用第三个参数提供的方案,进行组合,成新的value
//2. count、incr两个参数的书写,实际上没有价值,这个Lambda表达时只是想告诉你,该函数,返回的是两个参数的和
map.merge(key, 1, (count, incr) -> count + incr);
//3. 因此可以使用方法引用替换,表达取两个参数的和
map.merge(key, 1, Integer::sum);
- Lambda比方法引用可读的场景
@FunctionalInterface
public interface FunctionWu {
void accept();
}
public class GoshThisClassNameIsHumongous {
public static void main(String[] args) {
GoshThisClassNameIsHumongous a = new GoshThisClassNameIsHumongous();
//1. 有些情况下,参数的名字,对于阅读代码有帮助,这回导致Lambda更可读
//2. 当Lambda表达式内所调用的方法,与Lambda表达式在同一个类中,比如Lambda表达式() -> action()与其使用的action方法都在类GoshThisClassNameIsHumongous中,明显Lambda写起来更简明
//3. Lambda更简明是因为,一是该lambda表达式中没有形参列表,同时方法引用必须传入类名,而Lambda不用
a.test(()->action());
//4. 注意,action函数的返回值类型、形参列表,必须与函数式接口FunctionWu 中抽象方法accept的完全相同。具体原因参照方法引用与lambda表达式对应关系
//5. 书中讲了Function接口的静态方法identity的使用,不如直接用其lambda表达式,但我认为跟本条无关,这只能说明某些时候,静态方法不如Lambda表达式简单清晰,而不能说明方法引用不如Lambda表达式
a.test(GoshThisClassNameIsHumongous::action);
}
public static void action() {
System.out.println("成功");
}
public void test(FunctionWu a) {
a.accept();
}
}
//1. 在其他类中时,还是方法引用更复杂
public class OtherClass {
public static void main(String[] args) {
GoshThisClassNameIsHumongous a = new GoshThisClassNameIsHumongous();
a.test(()->GoshThisClassNameIsHumongous.action());
a.test(GoshThisClassNameIsHumongous::action);
}
}
第44条 坚持使用标准函数式接口
44.1 函数式接口改进模板模式
- 模板方法
//1. LinkedHashMap中有如下方法签名,它实际上是一个模板方法,默认返回false,当LinkedHashMap的对象调用put方法时,会调用这个方法,如果这个方法返回true,会删除最早放入该LinkedHashMap中的元素。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
- 覆盖模板方法
//LinkedHashMap本来无法作为缓存,因为其内的元素会永远增加,不会释放,但我们可以覆盖removeEldestEntry方法,让其内元素永远保留在100个以内
//此时
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > 100;
}
- 自定义函数式接口替代removeEldestEntry
//1. 新增函数式接口
import java.util.Map;
@FunctionalInterface
interface EldestEntryRemovalFunction<K, V> {
//注意,此处不能像原removeEldestEntry方法一样,只传入一个代表最先放入的键值对的Map.Entry<K,V> eldest对象
//因为原方法中直接可以调用Map对象的size方法,是因为原方法本身就是Map对象的一个实例方法,它能直接访问它所在的Map,但本方法不行,因此需要加入一个Map<K, V> map参数
boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}
//1. 为LinkedHashMap添加函数式接口类型的新属性,用于存放传入的函数式接口,修改LinkedHashMap的构造器,或创建其对象的静态工厂方法,为他们传入新增的函数式接口对象
//2. 假设我们修改LinkedHashMap接口如下
//新增属性
EldestEntryRemovalFunction<Map<K, V> , Map.Entry<K, V> > func;
...
//修改构造器
public LinkedHashMap(EldestEntryRemovalFunction func) {
super();
accessOrder = false;
this.func = func;
}
...
//修改原本调用removeEldestEntry方法的位置
//removeEldestEntry(first)
func.remove(this,first)
//修改客户端代码
LinkedHashMap a = new LinkedHashMap((map, mapentry) -> map.size() > 100);
- Jdk自带函数式接口替代自定义
//1. 没有必要自定义一个函数式接口EldestEntryRemovalFunction,Jdk本身就有可以接收两个不同类型参数,并返回boolean值的函数式接口BiPredicate,BiPredicate接口中,第一个泛型表示其第一个参数的类型,第二个泛型表示第二个参数的类型
//2. 使用自定义函数式接口,可以减少使用API的人的学习工作量
//修改LinkedHashMap中定义函数式接口类型属性的代码,其他代码省略
BiPredicate<<Map<K, V> , Map.Entry<K, V> > func;
44.2 Jdk自带的标准函数式接口
java.util.Function中共43个接口,其中有6个基础接口,可以通过这6个基础接口,推断出其余接口
44.2.1 基础接口
接口 | 函数签名 | 函数类型 | 范例 |
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 |
44.2.2 表示参数类型/返回值类型为基本类型的18种变体
- 命名方式:任何基础接口名前加int、long、double,共18个
- 例IntPredicate,它实际上和Predicate<int>,效果相同,但由于泛型不支持基本类型,而函数式接口的对象,会被多次使用,如果使用Predicate<Integer>,那么其方法,相当于boolean test(Integer value),那么每次调用Predicate对象的该方法, 相当于都要创建一个Integer类型的value,非常浪费资源,而int就不怎么耗资源
- 除了IntSupplier,是因为其方法没有参数列表,因此是其返回值为int型,其他都是方法的参数列表中,为int类型
- 除了IntFunction,是因为它表示返回值类型和参数列表类型不同的函数,所以它参数列表中,参数类型为int,但仍需一个表示其返回值类型的泛型IntFuntion<R>,其他变体,都不带泛型
44.2.3 表示传入一个基本类型/Object,返回另一种基本类型的Function的9种变体
- 表示传入int、long、double类型参数,得到非传入的类型的Function,共6(3*2*1)个,以"传入类型To+返回值类型+Function"命名,例如IntToLongFunction,其方法签名为long applyAsLong(int value)
- 表示传入Object类型参数,返回基本类型的Function,共3个,“To返回值类型Function”,例如ToIntFunction,方法签名为int applyAsInt(T value)
- 表示传入
44.2.4 表示两个参数的Predicate、Function、Consumer的3种变体
- 使用Bi+基础类型表示:例BiPredicate
44.2.5 表示特殊的BiFunction、BiConsumer的6种变体
- To+基本类型+BiFunction:表示传入不同的两个对象,返回一个基本类型的BiFunction,例:ToIntBiFunction,共3种
- Obj+基本类型+Consumer:表示可以消耗一个Object类型,和一个基本类型的Consumer,例:ObjLongConsumer,共3种
44.2.6 表示返回boolean类型的Supplier的1种变体
- BooleanSupplier
44.3 不要用泛型为包装类型的基础接口,替代表示基本类型的变体接口
- 例:不能用Consumer<Long>替代LongConsumer
- 每次调用Consumer<Long>对象的accept(Long t)方法,就会创建一个装箱类型Long的对象,会产生性能问题
44.4 需要自定义函数式接口的场景
- 没有任何的标准的函数接口,能满足需求,例如要一个带有三个参数的Predicate接口
- 需要一个抛出checked(必须被try-catch,或throws)异常的接口
- 需要一个通用,且能受益于它的描述性的名称,例如一看Comparator,就知道要用到一个比较器
- 需要创建的接口的实例,有着严格的条件限制(没看懂)
- 需要接口中提供大量好用的default方法
44.5 @FunctionalInterface注释
- 该注释在编译期,对接口进行检查,如果不是有且只有一个抽象方法,编译报错。可以有效避免后续维护人员,不小心给该接口增加抽象方法
- Comparator接口中,虽然除了compare这个抽象方法,还提供了另一个抽象方法boolean equals(Object obj),但这个抽象方法,是用于覆盖Object中的方法,因此不计入抽象方法总数。这样做的目的,就是强制实现Comparator的接口/类,都必须重写Object的equals方法
44.6 不要在相同参数位置,提供不同的函数接口来造成重载
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
//对于同一个submit方法,根据传入的函数式接口对象的不同,调用不同的方法,会引起客户端的歧义
//1. 必须加人为的转换,才使用Future<?> submit(Runnable task)方法
service.submit((Runnable) () -> test());
//2. 不加转换时,使用的是<T> Future<T> submit(Callable<T> task)方法
service.submit(() -> test());
}
public static int test() {
return 0;
}
}
第45条 谨慎使用Stream
45.1 迭代与pipeline写法
- 迭代写法:这里的迭代就是指通过for-each、或while,一次次的处理从而得到结果的方法,pipeline方法是指一条下来的
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
public class Anagrams {
public static void main(String[] args) throws IOException {
//文本内容
//abc
//cba
//bca
//acb
//1234
//4321
File dictionary = new File("C:\\Users\\含低调\\Desktop\\换位词测试.txt");
int minGroupSize = Integer.parseInt("1");
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
//1. java8新增方法,第一个参数为key值,如果在该map中存在,返回其value,如果不存在,将第一个参数作为key,将第一个参数以第二个参数对应的函数进行计算,作为value,并返回value
//2. 该方法将读取到的字符串abc,按顺序排序,然后以它作为groups这个map的key,然后新建一个TreeSet作为它的value,然后将字符串abc加入到这个TreeSet中
//3. 当读取到下一个abc的换位词时cba,以abc为key,发现groups这个map中存在该key,然后获取其value,也就是之前那个TreeSet,然后将cba也加入到这个TreeSet中
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
}
}
//4. 将所有的TreeSet取出,并循环,TreeSet中存放的换位词数如果大于定义的最小值,就打印该TreeSet大小,以及TreeSet中所有换位词
for (Set<String> group : groups.values())
if (group.size() >= minGroupSize)
System.out.println(group.size() + ": " + group);
}
//5. 将字符串排序后返回
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
- 滥用Stream:容易造成难以读懂和维护
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import static java.util.stream.Collectors.*;
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get("C:\\Users\\含低调\\Desktop\\换位词测试.txt");
int minGroupSize = Integer.parseInt("2");
//1. try-with-resources语句,保证Stream可以被关闭
//2. 此处,将文件中,每一行,作为一个元素,构成一个Stream<String>流words
try (Stream<String> words = Files.lines(dictionary)) {
//3. 将流转换成一个Map<String,List<String>>类型的map,map的key值为,words流中,每个元素,按字符顺序排序后,转为StringBuilder,再转为String,而value为一个list,这个list中,存放输入这个key的所有元素
words.collect(groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new, (sb, c) -> sb.append((char) c), StringBuilder::append).toString()))
//4. 之后将map的每一个value(即那个list),作为一个元素,构成新的流Stream<List<String>>,并过滤掉其元素长度,小于minGroupSize的元素,之后,将这个元素的长度+":"+元素本身,作为新元素,构成新的流Stream<String>。最后调用forEach方法打印流中所有元素
.values().stream().filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group).forEach(System.out::println);
}
}
}
- 恰当使用Stream
import static java.util.stream.Collectors.groupingBy;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Stream;
public class Anagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get("C:\\Users\\含低调\\Desktop\\换位词测试.txt");
int minGroupSize = Integer.parseInt("2");
try (Stream<String> words = Files.lines(dictionary)) {
//1. 之前groupingBy内,表示键的一系列操作太繁琐,改为使用自定义的alphabetize函数,将排序后的字符串作为key
//2. 这样做将具体的实现细节剥离出主程序,可以增加可读性,由于pipelines缺乏明确的类型信息,和可以命名的临时变量,因此这种辅助方法,对于其可读性的提升,比迭代式代码中,更加明显
words.collect(groupingBy(word -> alphabetize(word))).values().stream()
.filter(group -> group.size() >= minGroupSize)
//2. 之前通过map方法,将list的长度+":"+list本身,作为新流的元素,也是过于复杂,不如直接修改forEach中的打印方法来的简便
//3. 此处这个g,应该命名为group,但这样命名对于这个例子来讲,有点长
//4. 由于lambda表达式,没有参数的具体类型,因此lambda表达式中参数的命名,对于使用stream pipelines时,其代码可读性,至关重要
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
45.2 Stream处理char的问题
- 尽量不用Stream处理char值
public class TestChar {
public static void main(String[] args) {
//1. chars方法,会将字符串中的字符的int值,作为元素,组成流 打印结果:721011081081113211911111410810033
"Hello world!".chars().forEach(System.out::print);
//2. 必须强制转换,才能打印字符 打印结果:Hello world!
"Hello world!".chars().forEach(x->System.out.print((char)x));
}
}
45.3 Stream和迭代的选择
Stream pipeline利用函数对象,来描述重复的计算。迭代版代码中,使用代码块描述重复的计算
45.3.1 迭代
- 需要代码块具备,读取或修改范围内的任意局部变量。因为Lambda本质上是匿名内部类,因此只能读取final,或有效的final变量,而不能修改它
- 需要代码块具备,为外层方法返回值(不是为自己返回值),break、continue外层的循环,抛出该方法定义的任何checked异常
45.3.2 Stream
- 用一种规则转换元素
- 过滤元素
- 利用单个操作(添加、连接、计算最小值),合并元素
- 将元素存放到集合中
- 查找满足某些条件的元素
45.4 颠倒映射,访问最初阶段的元素
- 一个Stream可以有多个中间操作,这些中间操作执行过程中,会丢失调最初始的Stream中的元素,需要颠倒映射,获取最初的元素
import static java.math.BigInteger.ONE;
import static java.math.BigInteger.TWO;
import java.io.IOException;
import java.math.BigInteger;
import java.util.stream.Stream;
public class Anagrams {
public static void main(String[] args) throws IOException {
// 3. 梅森素数:2的素数次方-1也是素数,那么这个素数就是梅森素数
// 4. 此处,将所有素数n,进行2的n次方-1,然后用isProbablePrime(50)判断该值是否为素数,如果是,就代表它一定也是梅森素数,保留20个,并打印
// 5. 最初始时,Stream中存放的是从小到大的素数,后来经过map操作,以及filter操作,导致已经不知道最初始时Stream中元素都是什么
// 6. 由于我们知道最后的Stream中的元素,是由2的最初始元素次方-1得到的,因此你完全可以,用最后的元素+1再对2开放,得到最初始的p值
// 7. BigInteger类中恰好提供了这种计算,就是bitLength函数,因此你完全可以根据这个函数获得最初的Stream中元素值,这就叫做映射颠倒
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)).filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20).forEach(mp -> System.out.println(mp.bitLength() + ":" + mp));
}
// 2.即该方法返回所有素数,方法名primes译为"素数们",Stream中的方法其实都应该采用这种,可以根据方法名就推测出Stream中元素信息的这种命名习惯
static Stream<BigInteger> primes() {
// 1.iterate方法,表示流中第一个元素为方法中第一个参数,第二个元素为,第一个参数,经过第二个参数所代表的函数,而得到的值即BigInteger.TWO.nextProbablePrime,以此类推
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
}
45.5 Stream与迭代差不多的情况
模拟扑克牌的初始化
- 迭代
private static List<Card> newDeck() {
List<Card> result = new ArrayList<>();
//1. Suit和Rank都是枚举类,Card代表扑克牌,Suit代表扑克牌的花色,Rank代表扑克牌的大小(1/2/3../K)
//2. 将所有的花色Suit与所有的大小值Rank做笛卡尔积,并按顺序排列,其实就是对扑克牌初始化了
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
result.add(new Card(suit, rank));
return result;
}
- Stream
private static List<Card> newDeck() {
//1. flatMap
//1. flatMap也叫做扁平化处理函数,是将流中的元素,以其内函数的方式,转化为流,最后将所有元素转化出的流中元素进行合并。例如可以将一个字符串元素HAN,转为一个,以它每个字母作为元素的流
//2. 本方法中,将Suit中的每个枚举值,转化为,一个新的流,这个流中元素为以Suit这个枚举值为suit,以Rank中随机枚举值作为rank,而构成的所有Card对象
//3. 即每个Suit枚举值,对应一个流,最后将这些流中元素进行合并,得到一个新的流,并转为一个list集合
return Stream.of(Suit.values()).flatMap(suit -> Stream.of(Rank.values()).map(rank -> new Card(suit, rank)))
.collect(toList());
}
45.6 最佳实践
- 具体使用哪种方式实现,没有特定的要求,完全取决于个人偏好、编程环境、代码可读性、维护性、简洁性的总和考虑
- 如果不确定选哪种好,可以两种都尝试下
第46条 优先选择Stream中无副作用函数
意思就是Stream中,使用的函数,都应该遵循纯函数的要求,这样可以提升系统可读性、性能等
46.1 Stream模型
- 为了获得Stream带来的描述性、速度、并行性,你应该采用Stream模型编程
- 纯函数:
- 一个函数的结果,只依赖于它的输入,而不依赖于一个多变的状态,也不修改任何状态
- 无明显副作用:函数执行过程中,会与外部发生交互,就叫做副作用,可能包括,I/O 操作,修改函数入参或函数外部的变量,抛出异常等
private static int num = 3;
//该函数,结果不止依赖于其输入的number,还依赖于一个状态num,即当num改变,传入同样的number,结果不同,因此不是纯函数
private static int plus(int number) {
return number+num;
}
- Stream模型最重要的功能就是,将一系列的计算,构造成一系列的转换,通过纯函数计算上一次结果得到下一次的结果
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
//1. forEach为Stream一个操作,函数内修改了外部的对象(freq)的状态,因此不是纯函数,这也导致没有使用Stream的模板,也因此导致了可读性、性能等变差
//2. 正常来说,forEach应该只负责展示Stream中元素,而不是进行计算。有时也需要利用forEach将Stream计算后的结果,插入到已经存在的集合中
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
//1. 改进后的代码,以流中的元素的toLowerCase方法作为键,数量作为值,构成一个Map类型的freq
Map<String, Long> freq;
//2. Scanner转为Steam为Java9才新增的方法
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
46.2 Collectors与Collector
- Stream.collect方法,需要传入一个Collector对象
- Collector对象,为收集器,可以看做一个封装了可以将Stream多个元素合并到单个对象中的方法,的一个不透明的对象。收集器对象产生的对象一般是一个集合
- Collectors为Collector的工具类,用于生成不同类型的Collector对象
46.2.1 Collectors中的方法
- toList、toSet、toCollection:将Stream中元素收集到集合中
//1. 将元素集中到Collection中的收集器:收集成一个List的方法toList()、收集成Set的方法toSet()、收集成一个指定Collection的toCollection(collectionFactory)
//2. toSet、toList、toCollection都是Collectors的静态方法, 因此一般静态导入Collectors类中所有方法。而comparing为Comparator的静态方法
//3. 该方法,将freq这个Map的键取出来,形成Stream,并按freq的值(上个方法中,该Map的值应该为单词数量),倒序排列,取前十个,最后将这些个键,转为List
List<String> topTen = freq.keySet().stream().sorted(comparing(freq::get).reversed()).limit(10)
.collect(toList());
- toMap:将Stream中元素收集到映射中,每个Stream元素都有一个关联的键和值,多个Stream元素可以关联同一个键
//1. toMap(keyMapper,valueMapper),keyMapper为将元素映射成键的函数,valueMapper为将元素映射为值的函数
//2. 以下函数,将枚举值,转为一个Stream,然后以枚举值的toString函数的结果作为Map的键,枚举值本身作为Map的值,构建出一个名为stringToEnum 的Map
//3. 使用该方法,如果多个元素映射到同一个键,会抛出IllegalStateException
private static final Map<String, Operation> stringToEnum = Stream.of(values())
.collect(toMap(Object::toString, e -> e));
- toMap更复杂形式、groupingBy:解决多个元素同一个key问题
//1. toMap(keyMapper,valueMapper,mergeFunction):第三个函数,用于将Map的值,以该方式进行合并,即如果第三个函数为乘法,那么Map的值,就会是同一个键对应的所有值的乘积
//2. Artist为艺术家,Album为唱片,该方法,将所有唱片转换成Stream,然后以唱片的artist方法返回的艺术家作为键、唱片本身作为值,如果遇到同一个艺术家多张唱片,值保留发行数量(sales)最多的那张唱片
//3. maxBy为BinaryOperator的静态方法,它可以将Comparator转换为BinaryOperator,用于计算指定比较器产生的最大值
//4. comparing方法,返回Comparator对象,该对象使用其内的函数,来进行判断
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
//5. 带有三个函数的toMap,一般还可以用于保留最后的符合键值的Stream元素
toMap(keyMapper, valueMapper, (oldv1, newv1) -> newv1)
//6. toMap(keyMapper,valueMapper,mergeFunction,mapFactory):第四个参数可以用于指定想要的映射实现类型,例如EnumMap、TreeMap,三个参数时,默认为HashMap
//7. toConcurrentMap也有三种重载,可以生成ConcurrentHashMap实例
//1. groupingBy(classifier):返回一个可以生成映射的收集器,根据分类函数classifier,将Stream中元素进行分类,该分类函数,会被传入Stream的元素,并返回值代表该元素所属的类别,也就是映射的key,而Stream中对应该key的所有值,会被放入一个list中,作为这个映射的value
//2. 以字符串中所有字母自然顺序,建立新字符串,作为收集的映射的key,同一个key的字符串,都放入一个list中,作为其value
words.collect(groupingBy(word -> alphabetize(word)))
//3. groupingBy(classifier,downstream):downstream为下游收集器,可以将同一个key对应的所有value值,转换成一个值
//a. 例如可以传入toSet,表示将所有值放入Set中,而不是一个参数时所放入的list中。
//b. 也可以传入toCollection(collectionFactory),将元素放入自己想要的任何集合中。
//c. 有时也能传入counting()作为下游收集器,其value为该key的数量
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
//4. groupingBy(classifier,mapFactory,downstream):mapFactory可以指定收集器,所收集出的映射的类型,例如TreeMap
//5. groupingByConcurrent方法同样也提供了3中重载,生成ConcurrentHashMap实例
//6. partitioningBy(predicate)、partitioningBy(predicate,downstream):以boolean值,作为映射的键
- counting:只作为下游收集器,不应直接作为收集器
//1. 例如如下用法,想返回一个long型值,表示Stream中元素总和,就完全没有必要,因为可以直接使用Stream的count方法,获得相同的效果
- 下游收集器:summing、averaging、summarizing开头的9个方法。Stream上就有相应的功能。reducing、filtering、mapping、flatMapping、collectingAndThen。这些收集器视图部分复制Stream功能,专门为了做为下游收集器
- minBy、maxBy:接收一个比较器,返回根据比较器得到的Strea中的最小或最大的元素。他们内部实际上是调用了BinaryOperator的minBy和maxBy方法,他们的功能实际上就是Stream中min和max方法的粗略概括
- joining:只用于元素为CharSequence的Stream,例如字符串(字符串继承了CharSequence)
//1. 将所有Stream中元素,以指定分隔符分隔,返回一个String对象,但注意元素本身不能包含",",否则引起歧义
joining(delimiter)
//2. 以分隔符分隔,并加上前缀与后缀,例如Stream中包含元素came、saw、suffix,传入的参数分别为",","[","]",结果为[came,saw,conquered]
joining(delimiter,prefix,suffix)
- 最重要的收集器工厂为:toList、toSet、toMap、gorupingBy、joining
第47条 优先使用Collection而不是Stream作为返回类型
- Java8以前,如果想返回表示几个元素的序列,一般用表示集合的接口,例如Collection、Set、List
- 如果返回的元素的序列,只用于遍历其内元素,或这个序列根本无法实现Collection的一些方法(例如contains(Object)),此时一般使用Iterable作为返回类型
- 如果元素的序列中,元素为基本类型值,或对元素序列有严格的性能要求,就使用数组
- java8新增加了Stream,这使选择表示元素的序列的类型时,变得更加复杂
47.1 Stream与Iterable相互转换
47.1.1 Stream转Iterable
- Stream没有实现Iterable接口,而for-each循环底层是通过调用Iterable接口的iterator方法,得到一个迭代器,最后通过迭代器的next、hasNext方法完成的循环,因此无法使用for-each循环,遍历Stream中元素
- 但Stream的父接口BaseStream提供了一个与Iterable中的抽象方法iterator完全相同的方法,用于返回一个Iterator对象
- 因此当需要for-each遍历Stream中元素时,可以考虑如下写法
//1. for-each循环中,:后面需要一个实现了Iterable的类的对象
//2. ProcessHandle.allProcesses会返回一个Stream对象
//3. 此处不加强制转换编译会报错
for (ProcessHandle ph : (Iterable<ProcessHandle>)
ProcessHandle.allProcesses()::iterator){
//省略业务代码
}
//4. 相当于如下方式,将Stream对象,转为了一个Iterable对象
for (ProcessHandle ph : new Iterable<ProcessHandle>() {
@Override
public Iterator<ProcessHandle> iterator() {
return ProcessHandle.allProcesses().iterator();
}
}){
//省略业务代码
}
- 上面的方式代码过于杂乱、不清晰,因此考虑使用适配器方案,但适配器方案循环速度较慢
//新增该方法,将Stream转为Iterable
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
//此时客户端可转为如下形式,代码整洁,逻辑清晰
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
//省略业务代码
}
- 一般需要Stream转为Iterable是因为API中,只有Stream可以访问元素的序列,而后续操作又想通过for-each来遍历序列,例如45节中的Anagrams程序,Files.lines可以方便的将文件中的每一行作为一个元素,从而将整个文件转为元素的序列,API中没有其他好的方法可以将一个文件转为一个元素的序列
47.1.2 Iterable转Stream
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
47.2 定制Collection
- 对于大序列(序列中元素多,占用内存大),不应将该序列作为一个集合(泛指集合类型,Collection、Array都不应该用)的类型进行存储,因为会占用大量内存
- 如果返回的序列非常大,但可以被精准的描述,可以考虑实现一个专用的Collection
- 例如幂集,表示一个集合的所有子集合,例如{a,b,c}幂集为{{}, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}},也就是说n个元素的集合,其幂集中元素(集合)个数为2的n次方个
- 可以考虑通过实现AbstractList来定制一个集合表示幂集,该list中为幂集中的一个个子集合,其索引,例如6,二进制码为110,就表示,6对应的这个子集合中,包括原集合中的a、b两个元素,也就是{a,b}
- PowerSet
import java.util.*;
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() {
//原集合中3个元素,那么幂集就应该是2的3次方个
return 1 << src.size();
}
@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<>();
//1. >>=:index = index>>1
//2. 也就是,将索引值取出,例如6,转为二进制形式110,每次向右移动一位,表示依次取110中的每个元素,如果发现是1,那么就将原索引中对应位置的元素添加到返回的子集合中
for (int i = 0; index != 0; i++, index >>= 1)
if ((index & 1) == 1)
result.add(src.get(i));
return result;
}
};
}
}
- 定制Collection作为返回的序列的缺陷:size方法返回值为int型,因此Collection中最多可以存放Integer.MAX_VALUE个元素
- 无法定制Collection的情况:可能是因为序列的内容在迭代发生之前无法确定
47.3 元素为所有子列表的序列
- 有时选择方法返回的序列的类型时,只需要看是否易于实现
- {a,b,c}的子列表为{{},{a},{a,b}、{a,b,c},{b},{b,c},{c}}
- 前缀列表:包含原列表中第一个元素的子列表
- 后缀列表:包含原列表中最后一个元素的子列表
- 通过观察发现,一个元素的子列表,实际上就是依次对其所有前缀列表中的元素,取后缀列表,最后加上一个空列表
- {a,b,c}前缀列表为{a}、{a,b}、{a,b,c}
- {a}的后缀列表为{a}
- {a,b}的后缀列表为{b},{a,b}
- {a,b,c}的后缀列表为{c},{b,c},{a,b,c}
- 所有后缀列表相加,就是{a,b,c}的子列表
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
//1. concat永远连接两个Stream,此处用于连接一个表示空序列的Stream
//2. flatMap方法,将表示前缀列表的Stream中元素依次取出,得到他们的后缀列表
return Stream.concat(Stream.of(Collections.emptyList()),
prefixes(list).flatMap(SubLists::suffixes));
}
//返回前缀
private static <E> Stream<List<E>> prefixes(List<E> list) {
//rangeClosed就是将1到list.size的所有int值,组成一个Stream
//range与rangeClosed区别为,rangeClosed包含最后一个元素,range不包括
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()));
}
}
//上面代码就是如下for循环的Stream版本,但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));
47.4 最佳实践
- 需要返回对象序列时,能返回集合尽量返回集合
- 因为Collection实现了Iterable接口,又提供了一个转为Stream的stream方法,也就是说Collection可以同时提供迭代与Stream访问
- 如果序列中元素较少,返回一个标准的集合,例如ArrayList或HashSet
- 如果序列中元素较多,占用内存较大, 考虑定制Collection
- 如果无法返回Collection,就返回Stream或Iterable,哪个更自然使用哪个
- 如果序列需要被for-each处理,返回Iterable
- 如果序列需要在Stream pipeline中使用,返回Stream
- 公共API应提供两个返回值版本的方法
- 有时可以考虑哪种类型容易返回,就返回哪个
- 数组也通过Arrays.asList和Stream.of提供了迭代和Stream访问
第48条 谨慎使用Stream并行
48.1 最佳实践
- 尽量不要并行Stream pipeline,除非有足够理由保证计算的正确性,并能加快程序运行速度
- 对Stream进行不恰当的并行操作,会导致程序运行失败,或影响性能
- 使用并行Stream时,应在真实环境下查看运行结果是否正确,并测量性能是否提升