1、stream是什么?
java8中的stream是对集合对象功能的增强,对集合对象进行非常便利、高效的聚合操作或大批量的数据操作。借助lambda表达式,提高了编程效率跟程序可读性。
原始的循环只能串行方式一个一个遍历,每读完一个item再读下一个item,然后根据条件查询出所需要的数据。stream是更高级的Iterator,可以根据比如每个字符的首字母等条件隐世的内部遍历,做出相应的数据转换。stream依赖java7中的Fork/Join框架通过并行方式遍历拆分任务和加速处理过程,即将数据分成多段,每一个都在不同线程中处理,然后将结果一起输出。
Java 的并行 API 演变历程基本如下:
- 1.0-1.4 中的 java.lang.Thread
- 5.0 中的 java.util.concurrent
- 6.0 中的 Phasers 等
- 7.0 中的 Fork/Join 框架
- 8.0 中的 Lambda
2、stream的使用:
功能:发现 type 为 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合,
如果是用java7
List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
if(t.getType() == Transaction.GROCERY){
groceryTransactions.add(t);
}
}
Collections.sort(groceryTransactions, new Comparator(){
public int compare(Transaction t1, Transaction t2){
return t2.getValue().compareTo(t1.getValue());
}
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
transactionsIds.add(t.getId());
}
如果是用stream,代码更简洁、可读,并且是并发模式,执行速度快
List<Integer> transactionsIds =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(java.util.stream.Collectors.toList());
stream管道模型:
继续改造,想要实现处理并行代码,在Java8中非常简单:只需要使用parallelStream()
取代stream()
就可以了
List<Integer> transactionsIds =
transactions.parallelStream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(Comparator().reversed())
.map(Transaction::getId)
.collect(java.util.stream.Collectors.toList());
stream详细模型如下:
我们首先通过stream()
函数从一个交易列表中获取一个Stream对象。这个数据源是一个交易的列表,将会为Stream提供一系列元素。接下来,我们对Stream对象应用一些列的聚合操:filter
(通过给定一个谓词来过滤元素),sorted
(通过给定一个比较器实现排序),和map
(用于提取信息)。除了collect
其他操作都会返回Stream,这样就可以形成一个管道将它们连接起来,我们可以把这个链看做是一个对源的查询条件。
在collect被调用之前其实什么实质性的东西都都没有被调用。 collect被调用后将会开始处理管道,最终返回结果(结果是一个list)。
概念:
-
一系列元素:Stream对一组有特定类型的元素提供了一个接口。但是Stream并不真正存储元素,元素根据需求被计算出结果。
-
源:Stream可以处理任何一种数据提供源,比如结合、数组,或者I/O资源。
-
聚合操作:Stream支持类似SQL一样的操作,常规的操作都是函数式编程语言,比如filter,map,reduce,find,match,sorted,等等。
Stream操作还具备两个基本特性使它与集合操作不同:
-
管道:许多Stream操作会返回一个stream对象本身。这就允许所有操作可以连接起来形成一个更大的管道。这样就可以进行特定的优化了,比如懒加载和短回路,我们将在下面介绍。
-
内部迭代:和集合的显式迭代(外部迭代)相比,Stream操作不需要我们手动进行迭代。
3、Stream VS Collection
Collection和Stream都对一些列元素提供了一些接口。他们的不同之处是:Collection是和数据相关的,Stream是和计算相关的。
使用Collection做外部迭代(使用Collection接口需要用户做迭代(比如使用foreach),这种方式叫外部迭代):
List<String> transactionIds = new ArrayList<>();
for(Transaction t: transactions){
transactionIds.add(t.getId());
}
使用Stream做内部迭代(内部迭代是它会自己为你做好迭代)
List<Integer> transactionIds =
transactions.stream()
.map(Transaction::getId)
4、使用Stream处理数据
1)Stream 接口定义了许多操作,可以被分为两类。
-
filter,sorted,和map,这些可以连接起来形成一个管道的操作
-
collect,可关闭管道返回结果的操作
可以被连接起来的操作叫做中间操作
。你可以把他们连接起来,因为他们返回都类型都是Stream。关闭管道的操作叫做终结操作
。他们可以从管道中产生一个结果,比如一个List,一个Integer,甚至一个void。中间操作其实不执行任何处理直到一个终结操作被调用;终结操作通常可以被合并,并且被终结操作一次性执行。
2)函数用法
①sorted用法:
(1)sorted()
默认使用自然序排序, 其中的元素必须实现Comparable
接口
(2)sorted(Comparator<? super T> comparator)
:我们可以使用lambada 来创建一个Comparator
实例。可以按照升序或着降序来排序元素。
list.stream().sorted() //自然排序
list.stream().sorted(Comparator.reverseOrder())//自然逆排序
list.stream().sorted(Comparator.comparing(Student::getAge)) //自然排序
list.stream().sorted(Comparator.comparing(Student::getAge).reversed()) //根据年龄逆排序
②collect:是一个终端操作,它接收的参数是将流中的元素累积到汇总结果的各种方式(称为收集器)
工厂方法 | 返回类型 | 用于 |
toList | List<T> | 把流中所有元素收集到List中 |
示例:List<Menu> menus=Menu.getMenus.stream().collect(Collectors.toList()) | ||
toSet | Set<T> | 把流中所有元素收集到Set中,删除重复项 |
示例:Set<Menu> menus=Menu.getMenus.stream().collect(Collectors.toSet()) | ||
toCollection | Collection<T> | 把流中所有元素收集到给定的供应源创建的集合中 |
示例:ArrayList<Menu> menus=Menu.getMenus.stream().collect(Collectors.toCollection(ArrayList::new)) | ||
Counting | Long | 计算流中元素个数 |
示例:Long count=Menu.getMenus.stream().collect(Collectors.counting()); | ||
SummingInt | Integer | 对流中元素的一个整数属性求和 |
示例:Integer count=Menu.getMenus.stream().collect(Collectors.summingInt(Menu::getCalories)) | ||
averagingInt | Double | 计算流中元素integer属性的平均值 |
示例:Double averaging=Menu.getMenus.stream().collect(Collectors.averagingInt(Menu::getCalories)) | ||
Joining | String | 连接流中每个元素的toString方法生成的字符串 |
示例:String name=Menu.getMenus.stream().map(Menu::getName).collect(Collectors.joining(“, ”)) | ||
maxBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最大元素的optional |
示例:Optional<Menu> fattest=Menu.getMenus.stream().collect(Collectors.maxBy(Menu::getCalories)) | ||
minBy | Optional<T> | 一个包裹了流中按照给定比较器选出的最大元素的optional |
示例: Optional<Menu> lessest=Menu.getMenus.stream().collect(Collectors.minBy(Menu::getCalories)) | ||
Reducing | 归约操作产生的类型 | 从一个作为累加器的初始值开始,利用binaryOperator与流中的元素逐个结合,从而将流归约为单个值 |
示例:int count=Menu.getMenus.stream().collect(Collectors.reducing(0,Menu::getCalories,Integer::sum)); | ||
collectingAndThen | 转换函数返回的类型 | 包裹另一个转换器,对其结果应用转换函数 |
示例:Int count=Menu.getMenus.stream().collect(Collectors.collectingAndThen(toList(),List::size)) | ||
groupingBy | Map<K,List<T>> | 根据流中元素的某个值对流中的元素进行分组,并将属性值做为结果map的键 |
示例:Map<Type,List<Menu>> menuType=Menu.getMenus.stream().collect(Collectors.groupingby(Menu::getType)) | ||
partitioningBy | Map<Boolean,List<T>> | 根据流中每个元素应用谓语的结果来对项目进行分区 |
示例:Map<Boolean,List<Menu>> menuType=Menu.getMenus.stream().collect(Collectors.partitioningBy(Menu::isType)); |
3)总结:
Stream的操作包括如下三个东西:
- 一个需要进行数据查询的数据源(比如一个collection)
- 一连串组成管道的中间操作
- 一个执行管道并产生结果的终结操作
Stream提供的操作可分为如下四类:
-
过滤:有如下几种可以过滤操作
filter(Predicate)
:使用一个谓词java.util.function.Predicate
作为参数,返回一个满足谓词条件的stream。distinct
:返回一个没有重复元素的stream(根据equals的实现)limit(n)
: 返回一个不超过给定长度的streamskip(n)
: 返回一个忽略前n个的stream
-
查找和匹配:一个通常的数据处理模式是判断一些元素是否满足给定的属性。可以使用
anyMatch
,allMatch
, 和noneMatch
操作来帮助你实现。他们都需要一个predicate
作为参数,并且返回一个boolean作为作为结果(因此他们是终结操作)。比如,你可以使用allMatch来检车在Stream中的所有元素是否有一个值大于100,像下面代码中表示的那样。
//allMatch检测在Stream中的所有元素是否有一个值大于100
boolean expensive =
transactions.stream()
.allMatch(t -> t.getValue() > 100);
- 获取任意元素:Stream提供了
findFirst
和findAny
,可以从Stream中。它们可以和Stream的其他操作连接在一起,比如filter
。findFirst和findAny都返回一个Optional对象,
Optional<Transaction> =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny();
- 映射:Stream支持map方法,map使用一个函数作为一个参数,你可以使用map从Stream的一个元素中提取信息。在下面的例子中,我们返回列表中每个单词的长度。
List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
List<Integer> wordLengths =
words.stream()
.map(String::length)
.collect(toList());
- 计算:使用
reduce
操作,比如“交易中最大值的id”或者“计算交易金额总和”。reduce可以将一个操作应用到每个元素上,知道输出结果。reduce也经常被叫做折叠操作,因为你可以看到这种操作像把一个长的纸张(你的stream)不停地折叠直到想成一个小方格,这就是折叠操作。
int sum = 0;
for (int x : numbers) {
sum += x;
}
//使用stream,初始值为0。一个将连个数相加返回一个新值的BinaryOperator
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
5、数值型Stream
你已经看到了你可以使用reduce方法来计算一个Integer的Stream了。然而,我们却执行了很多次的开箱操作去重复地把一个Integer对象添加到另一个上。如果我们调用sum
方法岂不是很好?像下面代码那样,这样代码的意图也更加明确。
int statement =
transactions.stream()
.map(Transaction::getValue)
.sum(); // 这里是会报错的
复制代码
在Java 8 中引入了三种原始的特定数值型Stream接口来解决这个问题,它们是IntStream
, DoubleStream
, 和 LongStream
。它们各自可以数值型Stream变成一个int、double、long。
可以使用mapToInt, mapToDouble, and mapToLong将通用Stream转化成一个数值型Stream,我们可以将上面代码改成下面代码。当然你可以使用通用Stream类型取代数值型Stream,然后使用开箱操作。
int statementSum =
transactions.stream()
.mapToInt(Transaction::getValue)
.sum(); // 可以正确运行
数值类型Stream的另一个用途就是获取一个区间的数。比如你可能想要生成1到100之前的所有数。Java 8在IntStream, DoubleStream, 和 LongStream 中引入了两个静态方法来帮助生成一个区间,它们是range
和 rangeClosed
.
这两个方法以区间开始的数为第一个参数,以区间结束的数为第二个参数。但是range的区间是开区间的,rangeClosed是闭区间的。下面是一个使用rangeClosed返回10到30之间的奇数的stream。
IntStream oddNumbers =
IntStream.rangeClosed(10, 30)
.filter(n -> n % 2 == 1);
6、创建Stream
1)从一个集合中获取一个Stream
2)使用过数值类型Stream
通过数值或者数组创建Stream可以很直接:对于数值是要使用静态方法Stream .of,对于数组使用静态方法Arrays.stream ,像下面代码这样:
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);
复制代码
你可以使用Files.lines静态方法将一个文件转化为一个Stream。比如,下面代码计算一个文件的行数。
long numberOfLines =
Files.lines(Paths.get(“yourFile.txt”), Charset.defaultCharset())
.count();
7、无穷Stream
Stream的有两个静态方法Stream.iterate
和Stream.generate。
这两个方法可以一直产生元素。这也是我们叫无穷Stream的原因:Stream没有一个固定的大小,但是它和从固定大小的集合中创建的stream是一样的。
下面代码是一个使用iterate
创建了包含一个10的倍数的Stream。iterate的第一个参数是初始值,第二个至是用于产生每个元素的lambda表达式(类型是UnaryOperator)。
Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);
我们可以使用limit
操作将一个无穷的Stream转化为一个大小固定的stream,像下面这样:
numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40
Stream<Double> generateA = Stream.generate(new Supplier<Double>() {
@Override
public Double get() {
return java.lang.Math.random();
}
});
//等价于
Stream<Double> generateB = Stream.generate(()-> java.lang.Math.random());
//等价于
Stream<Double> generateC = Stream.generate(java.lang.Math::random);
参考资料:
3、链接: 使用Java 8 Stream像操作SQL一样处理数据(上) - 掘金