什么是流
在java中,集合是一个非常基本的结构,几乎每个Java应用程序都会制造和处理集合。而对集合的操作中,有很多是类似于数据库操作的。在Java程序中,如果要处理集合中的数据,我们就需要一遍一遍的遍历集合,从而对集合中的数据进行处理。我们也知道,像这种标准的数据结构,类似于数据库中的数据,使用sql这种语法会很方便也很直观。在Java8之前没有专门针对集合的操作api,但在Java8之后,我们可以将集合序列化为Stream,使用Stream中的函数来对集合进行操作。
如何使用流
//Java8之前的写法
List<Dish> lowCaloricDishes = new ArrayList<>(); //有一个盘子列表
for(Dish d: menu){ //筛选出价格比较低的盘子
if(d.getCalories() < 400){
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){ //对价格比较低的盘子进行排序
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){ //盘子名称的列表
lowCaloricDishesName.add(d.getName());
}
上面的写法,总共对盘子进行了两次的遍历,而且是单线程进行遍历的,在数据量比较大的时候,非常的占用内存和cpu时间。下面我们看看Java8的写法:
Java8的写法:
List<String> lowCaloricDishesName = menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());
//如果要使用多线程方式,直接将stream()改为parallelStream()即可
List<String> lowCaloricDishesName = menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dishes::getCalories))
.map(Dish::getName)
.collect(toList());
可以看到,使用Java8语法我们可以非常简单的实现以上功能,而且可以简单的使用并行来处理该问题。
下面我们分别看看使用流的几种中间操作和终端操作。
筛选和切片
- 用谓词筛选
//用谓词筛选流 menu.stream() .filter(Dish::isVegetarian) .collect(toList()) .forEach(System.out::println);
- 筛选各异的元素
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4); numbers.stream() .filter(i -> i % 2 == 0) .distinct() .forEach(System.out::println);
- 截断流
//截断流 menu.stream() .filter(d -> d.getCalories()>300) .limit(3) .collect(Collectors.toList()) .forEach(System.out::println);
- 跳过元素(和limit互补)
//跳过元素 menu.stream() .filter(d -> d.getCalories()>300) .skip(3) .collect(toList()) .forEach(System.out::println);
映射
- 对流中的每个元素应用函数
menu.stream() .map(Dish::getName) .collect(toList()) .forEach(System.out::println);
- 流的扁平化
//要找到一串文字中的所有不重复的字符 //错误示范1 List<String> words = Arrays.asList("我是一个程序员", "我喜欢写程序"); words.stream() .map(word -> word.split("")) .distinct() .collect(toList()) .forEach(System.out::println); //此处输出:[Ljava.lang.String;@1e80bfe8 //[Ljava.lang.String;@66a29884 //错误示范2 words.stream() .map(word -> word.split("")) .map(Arrays::stream) .distinct() .collect(toList()) .forEach(System.out::println); //输出:java.util.stream.ReferencePipeline$Head@79fc0f2f //java.util.stream.ReferencePipeline$Head@50040f0c //正确示范 words.stream() .map(word -> word.split("")) .flatMap(Arrays::stream) .distinct() .collect(toList()) .forEach(System.out::print); //输出:我是一个程序员喜欢写
查找和匹配
- 检查谓词是否至少匹配一个元素
//检查集合中是否至少匹配一个元素 if (menu.stream().anyMatch(Dish::isVegetarian)) { System.out.println("这个菜单里面有蔬菜"); }
- 检查谓词是否匹配所有元素
boolean isHealthy = menu.stream() .allMatch(d -> d.getCalories() < 1000); System.out.println(isHealthy);
- 查找元素
menu.stream() .filter(Dish::isVegetarian) .findAny() .ifPresent(System.out::println);
- 查找第一个元素
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5); someNumbers.stream() .map(x -> x * x) .filter(x -> x % 3 == 0) .findFirst() .ifPresent(System.out::println); // 9
规约
-
元素求和
numbers.stream() .reduce((a, b) -> (a + b)) .ifPresent(System.out::println);
-
最大值和最小值
numbers.stream().reduce(Integer::max).ifPresent(System.out::println); numbers.stream().reduce(Integer::min).ifPresent(System.out::println);
归约方法的优势与并行化
相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。现在重要的是要认识到,可变的累加器模式对于并行化来说是死路一条。你需要一种新的模式,这正是reduce所提供的。使用流来对所有的元素并行求和时,你的代码几乎不用修改: stream()换成了parallelStream()。
但要并行执行这段代码也要付一定代价: 传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。流操作:有状态和无状态
诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。
但诸如reduce、 sum、 max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。不管流中有多少元素要处理,内部状态都是有界的。
相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
付诸实践
- 交易员和交易
(1) 找出2011年发生的所有交易,并按交易额排序(从低到高)。 (2) 交易员都在哪些不同的城市工作过? (3) 查找所有来自于剑桥的交易员,并按姓名排序。 (4) 返回所有交易员的姓名字符串,按字母顺序排序。 (5) 有没有交易员是在米兰工作的? (6) 打印生活在剑桥的交易员的所有交易额。 (7) 所有交易中,最高的交易额是多少? (8) 找到交易额最小的交易。
//以下是你要处理的领域,一个Traders和Transactions的列表: 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类的定义如下: @Data public class Trader{ private final String name; private final String city; } @Data public class Transaction{ private final Trader trader; private final int year; private final int value; }
- 解答
package top.hengshare.interviewer.java8.stream; import lombok.Data; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @Data public class Trader { private final String name; private final String city; } @Data class Transaction{ private final Trader trader; private final int year; private final int value; public static void main(String[] args) { 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) ); //(1) 找出2011年发生的所有交易,并按交易额排序(从低到高)。 transactions.stream() .filter(t -> t.getYear()==2011) .sorted(Comparator.comparing(Transaction::getValue)) .collect(Collectors.toList()) .forEach(System.out::println); //(2) 交易员都在哪些不同的城市工作过? transactions.stream() .map(t -> t.getTrader().getCity()) .distinct() .collect(Collectors.toList()) .forEach(System.out::println); //或者 transactions.stream() .map(t -> t.getTrader().getCity()) .collect(Collectors.toSet()) .forEach(System.out::println); //(3) 查找所有来自于剑桥的交易员,并按姓名排序。 transactions.stream() .map(Transaction::getTrader) .filter(t -> "Cambridge".equals(t.getCity())) .distinct() .sorted(Comparator.comparing(Trader::getName)) .forEach(System.out::println); //(4) 返回所有交易员的姓名字符串,按字母顺序排序。 String str = transactions.stream() .map(t -> t.getTrader().getName()) .distinct() .sorted() .collect(Collectors.joining()); System.out.println(str); //(5) 有没有交易员是在米兰工作的? boolean b = transactions.stream() .anyMatch(t -> "Milan".equals(t.getTrader().getCity())); System.out.println(b); //(6) 打印生活在剑桥的交易员的所有交易额。 transactions.stream() .filter(t -> "Cambridge".equals(t.getTrader().getCity())) .map(Transaction::getValue) .forEach(System.out::println); //(7) 所有交易中,最高的交易额是多少? transactions.stream() .map(Transaction::getValue) .reduce(Integer::max) .ifPresent(System.out::println); //(8) 找到交易额最小的交易。 transactions.stream() .min(Comparator.comparing(Transaction::getValue)) .ifPresent(System.out::println); } }
数值流
- 原始类型流特化
使用reduce方法可以计算list中元素的总和,但是有时候却会暗藏一个装箱和拆箱的操作,比如下面这段代码:
为了解决这个Integer拆箱转换为int的操作,我们可以直接将该int calories = menu.stream() .map(Dish::getCalories) .reduce(0, Integer::sum);
stream<integer>
转换为intstream
流//Stream<Integer>映射到数值流intstream int calories = menu.stream() .mapToInt(Dish::getCalories) .sum(); //intstream转换成流对象stream<Integer> IntStream intStream = menu.stream().mapToInt(Dish::getCalories); Stream<Integer> stream = intStream.boxed(); //使用默认值OptionalInt OptionalInt maxCalories = menu.stream() .mapToInt(Dish::getCalories) .max(); int max = maxCalories.orElse(1);
- 数值范围
- 数值流应用:勾股数
构建流
- 由值创建流
- 由数组创建流
- 由文件创建流
- 由函数生成流:创建无限流