书没看完, 本文正常情况下一周更新一次
个人认为这本书写的不怎样, 中文翻译过来的版本更别说了, 有些地方理解意思全靠猜, 网上也没有勘误表.英文水平过关的还是找找看Java 8的一些其他文档吧.
// 顺序处理
import static java.util.stream.Collectors.toList;
List heavyApples = inventory.stream().filter((Apple a)->a.getWeight() > 150).collect(toList());
// 并行处理
import static java.util.stream.Collectors.toList;
List heavyApples = inventory.parallelStream().filter((Apple a)->a.getWeight() > 150).collect(toList());
// 两种方式
(parameters) -> expression; // 表达式
(parameters) -> { statement }; // 语句
函数式接口 | 函数描述符 | 原始类型特化 |
---|---|---|
Predicate | T->boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer | T->void | IntConsumer, LongConsumer, DoubleConsumer |
Function |
T->R | IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction |
Supplier | () -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator | T->T | IntUnaryOperator, LongOperator, DoubleOperator |
BinaryOperator | (T, T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate |
(L,R)->T | |
BiConsumer |
(T, U)->void | ObjectIntConsumer, ObjectLongConsumer, ObjectDoubleConsumer |
BiFunction |
(T, U)->R | ToIntBiFunction |
// 定义自己的函数式接口
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException ;
}
BufferedReaderProcessor p = (BufferedReader b) -> b.readLine();
// 将 Lambda 包含到try/catch 中
Function f = (BufferedReader b) ->{
try{
return b.readLine();
}
catch(IOException e){
throw new RuntimeException(e);
}
};
Lambda的类型检查是从使用Lambda的上下文推断出来的. Lambda上下文中需要的类型被称为目标类型.
有了目标类型的概念, 同一个Lambda表达式就可以与不同的函数式接口联系起来, 只要他们的抽象方法签名能够兼容.例如, 以下的两个赋值是有效的:
Callable c = () -> 42;
PrivilegedAction p = () -> 41;
// Predicate 返回一个boolean
Predicate p = s -> list.add(s);
// Consumer 返回一个void
Consumer p = s -> list.add(s);
方法引用让你可以重复使用现有函数的定义, 并像Lambda一样传递他.
方法引用可以被看作是仅仅调用特定方法的Lambda的一种快捷写法.当你需要使用方法引用时, 目标引用放在分隔符 :: 前面, 方法名称放到后面.例如: Apple::getWeight
// Lambda
(args) -> ClassName. StaticMethod(args);
// 转换为方法引用
ClassName:: StaticMethod
// Lambda
(arg0, test) -> args. instanceMethod(test);
// 转换成方法引用, arg0 的类型是ClassName
ClassName:: instanceMethod
// Lambda
(args) -> expr. instanceMethod(args);
// 转换成方法引用
expr:: instanceMethod
编译器会进行一种Lambda表达式类似的类型检查过程, 来确定对于给定的函数式接口, 这个方法是否有效, 所以需要方法的签名必须和上下文类型匹配.
// 假设一个构造函数没有参数, 他适合Supplier的签名, 可以写成
() -> Apple;
// 或者
Supplier c1 = Apple::new;
Apple a = c1.get();
// 或者
Supplier c2 = () -> new Apple();
Apple a = c2.get();
// 如果构造函数有一个参数, Apple(Integer weight)
Function f1 = Apple::new;
Apple a2 = f1.apply(20);
// 等价于
Function f1 = (weight) -> new Apple(weight);
Apple a2 = f1.apply(20);
第一步: 传递代码
第二步: 使用匿名内部类
第三部: 使用Lambda表达式
第四部: 使用方法引用
// Comparator具有一个叫做comparing的静态辅助方法, 他可以接收一个Function 来提取 Comparable的键值, 并生成一个 Comparator对象.
Comparator c = Comparator.comparing((Apple a) -> a.getWeight());
// 比较器复合
inventory.sort(comparing(Apple::getWeight));
// 逆序
inventory.sort(comparing(Apple::getWeight)). reversed();
// 比较器链, 两个苹果重量一样时, 进一步使用按照国家排序
inventory.sort(comparing(Apple::getWeight)). reversed().thenComparing(Apple:;getCountry);
// 谓词复合
// 谓词接口包含三个方法: negate, and 和 or
Predicate redApple = (Apple a) -> "red".equals(a.getColor());
// 非红色苹果
Predicate notRedApple = redApple.negate();
// 红色并且重的苹果
Predicate redAndHeavy = readApple.and(a -> a.getWeight() > 150);
// or 操作同 and操作
// !!!! and, or 方式是按照表达式链中的位置, 从左到右确定优先级的.
// a.or(b).and(c) 可以看做 (a || b) && c
// Function 接口配备了andThen 和 compose两个默认方法, 他们都会返回Function的一个实例.
// (a +1 ) * 2
Function f = x -> x+1;
Function g = x -> x*2;
Function h = f. andThen(g);
int result = h.apply(1);
Function f = x -> x+1;
Function g = x -> x*2;
Function h = g. compse(f);
int result = h.apply(1);
流是Java API的新成员, 他允许你以申明的方式处理数据集合(通过查询语句来表达, 而不是临时写一个实现). 此外流可以透明地并行处理, 你无需写任何多线程代码.
filter, map, sorted, collect 等操作是与具体线程无关的高层次构件, 所以他们的内部可以是单线程的, 也可以透明地利用你的多核架构.
流的简短定义就是 从支持数据处理的源生成的元素序列.
粗略地说, 集合和流之间的差异就在于什么时候计算. 集合是一个内存中的数据结构, 它包含数据结构中目前所有的值–集合中的每个元素都的先算出来才能添加到集合中. 相比之下, 流则是概念上固定的数据结构, 其元素则是按需计算的.
和迭代器类似, 流只能遍历一次.
使用Collection接口需要用户去做迭代就叫做外部迭代. Stream库使用内部迭代–他帮你吧迭代做了, 还把得到的流值存在了某个地方, 你只要给出一个函数说要干什么就可以了.
内部迭代可以透明地并行处理, 或者使用更优化的顺序来进行处理. 外部迭代的优化会比较困难, 需要自己处理并行问题了.
可以链接起来的操作是 中间操作 , 关闭流的操作是 终端操作 .
诸如filter, map等中间操作会返回另外一个流. 这种让多个操作可以连接起来形成一个查询. 重要的是, 除非流水线上出现一个终端操作, 否则不会中间操作不会执行任何处理.
流的优化: 使用短路; 将多个操作合并到同一次遍历中(这种技术叫做循环合并).
终端操作会从流的流水线生成结果. 其结果是任何不是流的值.
使用内部迭代的话, Stream API可以决定并行执行你的代码, 使用外部迭代就办不到了, 只能使用单一线程迭代.
Stream的filter操作接收一谓词作为参数, 并返回一个包含所有符合谓词操作的流
流支持一个distinct 操作, 他会返回一个元素各异(根据流生成元素的hashCode和equals方法来实现)的流.
流支持limit(n)的方法, 该方法会返回一个不超过指定长度的流. 如果流是有序的, 则最多会返回前n个元素. 如果流是无序的那么limit的结果不会以任何顺序排列.
流支持skip(n)方法, 返回一个扔掉前n个元素的流. 如果流中元素不足n个则返回一个空流.
流支持map方法, 它将一个函数作为参数. 这个函数会作用到每个元素上, 并将其映射成一个新的元素.
// 示例: 对于一张单词表, 如何返回一个列表, 列出里面各不相同的字符呢?
// ["hello", "world"]
// 第一个版本的代码可能是这样的, 但是这个结果有个问题, 他返回的是Stream, 而不是Stream
words. Stream(). map(w->w.split("")).distinct().collect(Collectors.toList());
// 想要一个字符流我们可以使用Arrays.stream(), 他接收一个数组并生成一个流. 但是还是有一个问题, 接收了两个数组, 所以最后生成了俩个流, 但是我们想要的是一个字符流
words. Stream(). map(w->w.split("")).map(Arrays::stream).distinct().collect(Collectors.toList());
// 使用flatmap
words. Stream(). map(w->w.split("")).flatmap(Arrays::stream).distinct().collect(Collectors.toList());
简而言之, flatmap让你把一个流中的每个值都转换成另一个流, 再把每个流连接起来成为一个流.
// 测验 5.2: 映射
// (1) 给定一个数字序列, 如何返回一个由每个数字的平方组成的列表呢? 例如 给定[1, 2, 3, 4] 返回 [1, 4, 9, 16]
// (2) 给定两个数字列表, 如何返回所有的数对呢? 例如给定列表[1, 2, 3] 和 [3, 4], 应该返回[(1,3), (1,4), (2,3), (2,4), (3,3), (3,4)]
Stream API提供了allMatch, anyMatch, noneMatch, findFirst, findAny这些可以查看数据中某些元素是否匹配一个给定属性.
anyMatch: 检查谓词是否至少匹配一个元素. 返回一个boolean是一个终端操作.
allMatch: 检查谓词是否匹配所有元素.
noneMatch: 检查是否流中没有任何元素符合给定谓词的[]匹配.
// 短路求值
// 有些操作不需要处理整个流就能得到结果. 例如: 假设你需要对一个用and连接起来的大布尔表达式求值. 不管表达式多长, 你只需要找到一个表达式为false, 就可以推断整个表达式将返回false, 所以用不着计算整个表达式, 这就是短路.
findAny() 方法返回当前流中的任意元素. 他可以和其他流操作结合使用. 返回Optional.
findFirst() 方法返回流中的第一个元素.
Optional 简介
Optional类是一个容器类, 代表一个值存在或者不存在. findAny()可能什么都没有找到, 返回Optional就不用返回null了.
// 何时使用findAny和 findFirst
// 你可能会想, 为什么同时会有findFirst和findAny呢? 答案是并行, 找到第一个元素在并行上限制太多. 如果你不关心返回的是哪个元素, 请使用findAny, 因为他在使用并行流的时候限制很少.
归约操作: 将流中的所有元素反腐结合起来, 得到一个值.
numbers = [4, 5, 3, 9];
// for 循环求和
int sum = 0;
for(int x : numbers)
{
sum += x;
}
// 使用reduce 进行归约
int sum = numbers. stream().reduce(0, (x, y)->x+y);
// reduce 接收两个参数, 一个初始值, 一个BinaryOperator来将两个元素结合起来产生一个新值.
// reduce 是如何对一个数字流进行求和的: 首先, 0作为Lambda的第一个参数(x), 从流中获取4 作为第二个参数(y). 0+4得到4, 它生成了新的累积值. 然后再用累积值和流中下一个元素5调用Lambda, 产生新值9.....
// reduce 还有一个重载的变体, 他不接受初始值, 但是结果会返回一个Optional对象.
// 计算最大值
Optional<Integer> max = numbers. stream().reduce(Integer::max);
// 计算最小值
Optional<Integer> min = numbers. stream().reduce(Integer::min);
// 归约方法的优势和并行化
// 相比于逐步迭代求和, 使用reduce的好处在于, 这里的迭代被内部迭代 抽象掉了, 这让内部实现得以选择并行执行reduce操作.
// 流操作: 无状态和有状态
// 无状态: 诸如filter, map等操作会从输入流中获取每一个元素, 并在输出流中得到0个或一个结果. 这些操作一般是无状态的. 他们没有内部状态.
// 有状态: 诸如sort, distinct等操作一开始都和filter, map差不多, 都是接收一个流, 在生成一个流, 但是有一个关键区别. 从流中排序和删除重复项目时都需要知道先前的历史. 这些操作被称为有状态操作.
(1) 找出2011年发生的所有交易, 并按照交易额排序(从低到高).
(2) 交易员都在哪些不同的城市工作过.
(3) 查找所有来自于剑桥的交易员, 并按姓名排序
(4) 返回所有交易员的姓名字符串, 按字母顺序排序
(5) 有没有交易原始在木兰工作的
(6) 打印生活在剑桥的交易员的所有交易额
(7) 所有交易中, 最高的交易额是多少
(8) 找到交易额最小的交易
Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brian = new Trader("Brian", "Cambridge");
List<Transaction> transactions = Arrays.asList(
new Transaction(brian, 2011, 300),
new Transaction(raoul, 2012, 1000),
new Transaction(raoul, 2011, 400),
new Transaction(mario, 2012, 710),
new Transaction(mario, 2012, 700),
new Transaction(alan, 2012, 950)
);
// Trader和Transaction类的定义:
public class Trader {
private String name;
private String city;
public Trader(String n, String c){
this.name = n;
this.city = c;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
public String toString() {
return "Trader{" +"name='" + name + '\'' + ", city='" + city + '\'' + '}';
}
}
public class Transaction {
private Trader trader;
private Integer year;
private Integer value;
public Transaction(Trader trader, Integer year, Integer value) {
this.trader = trader;
this.year = year;
this.value = value;
}
public Trader getTrader() {
return trader;
}
public void setTrader(Trader trader) {
this.trader = trader;
}
public Integer getYear() {
return year;
}
public void setYear(Integer year) {
this.year = year;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
@Override
public String toString() {
return "Transaction{" + "trader=" + trader + ", year=" + year + ", value=" + value + '}';
}
}
// (4) 返回所有交易员的姓名字符串, 按字母顺序排序
String traderStr =
transactions.stream()
// 提取所有交易员姓名,生成一个 Strings 构成的 Stream
.map(transaction -> transaction.getTrader().getName())
// 只选择不相同的姓名
.distinct()
// 对姓名按字母顺序排序
.sorted()
// 逐个拼接每个名字,得到一个将所有名字连接起来的 String
.reduce("", (n1, n2) -> n1 + " " + n2);
// 这里最后字符串连接的时候效率不高, 他每次迭代都要建立一个新的String对象. 建议使用Collectors.joining(), 他的内部是使用StringBuilder实现的.
String traderStr =
transactions.stream().map(transaction ->
transaction.getTrader().getName()).distinct() .sorted().collect(Collectors.joining());
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
// 这段代码是有问题的, 他暗含了一个自动装箱的过程
Java 8 引入了三个原始类型特化流接口来解决自动装箱的问题, IntStream, DoubleStream, LongStream, 分别将流中的元素特化为int, long, double, 从而避免了暗含的装箱成本.
// 上面的代码可以改写为下面这种
int calories = menu.stream() .mapToInt(Dish::getCalories).sum();
// 这里如果流是空的, 返回值就是0. IntStream 还有其他方法 例如max, min, average等.
// 如果需要将原始流转换为一般流, 使用boxed则可以实现
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
Java 8 引入了两个可以用于IntStream和 LongStream的静态方法生成范围数值, range和rangeClosed. 这两个方法都是第一个参数接收初始值, 第二个参数接收结束值. range不包含结束值, rangeClosed 包含结束值.