Java8新特性之Steam流式编程
特地感谢鲁班⼤叔的分享,原学习地址:
以下是学习过程整理的笔记
1、简介
Stream 流处理,⾸先要澄清的是 java8 中的 Stream 与 I/O 流 InputStream 和 OutputStream 是完全不同的概念。
Stream 机制是针对集合迭代器的增强。流允许你⽤声明式的⽅式处理数据集合(通过查询语句来表达,⽽不是临时编写⼀个实现)2、创建对象流的三种⽅式
1. 由集合对象创建流。对⽀持流处理的对象调⽤ stream()。⽀持流处理的对象包括 Collection 集合及其⼦类
List<Integer> list = Arrays.asList(1,2,3);
Stream<Integer> stream = list.stream();
2. 由数组创建流。通过静态⽅法 Arrays.*stream()* 将数组转化为流(Stream)
IntStream stream = Arrays.stream(new int[]{3, 2, 1});
3. 通过静态⽅法 Stream.of() ,但是底层其实还是调⽤ Arrays.stream()
Stream<Integer> stream = Stream.of(1, 2, 3);
注意:
还有两种⽐较特殊的流
空流:pty()
⽆限流:**ate() ** 和 **Stream.iterate() **。可以配合 limit() 使⽤可以限制⼀下数量
// 接受⼀个 Supplier 作为参数
// 初始值是 0,新值是前⼀个元素值 + 2
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
3、流处理的特性
1. 不存储数据
2. 不会改变数据源
3. 不可以重复使⽤
测试⽤例:
dfrey.stream.features;
import org.junit.Assert;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 流特性
*
* @author godfrey
* @since 2021-08-15
*/
class StreamFeaturesTest {
/**
* 流的简单例⼦
*/
@Test
public void test1() {
List<Integer> list = Stream.of(1, 2, 5, 9, 7, 3).filter(val -> val > 2).sorted().List());
for (Integer item : list) {
System.out.println(item);
}
}
/**
* 流不会改变数据源
*/
@Test
public void test2() {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(1);
Assert.assertEquals(3, list.stream().distinct().count());
Assert.assertEquals(4, list.size());
}
/**
* 流不可以重复使⽤
*/
@Test(expected = IllegalStateException.class)
public void test3() {
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<Integer> newStream = integerStream.filter(val -> val > 2);
integerStream.skip(1);
}
}
⾸先,test1() 向我们展⽰了流的⼀般⽤法,由下图可见,源数据流经管道,最后输出结果数据。
然后,我们先看 test3(),源数组产⽣的流对象 integerStream 在调⽤ filter() 之后,数据⽴即流向了 newStream。正因为流“不保存数据”的特性,所以重复利⽤integerStream 再次调⽤ skip(1) ⽅法,会抛出⼀个 *IllegalStateException* 的异常:
java.lang.IllegalStateException: stream has already been operated upon or closed
所以说流不存储数据,且流不可以重复使⽤。
最后,我们来看 test2(),尽管我们对 list 对象⽣成的流 list.stream() 做了去重操作 distinct() ,但是并不影响源数据对象 list。
4、流处理的操作类型
Stream 的所有操作连起来组合成了管道,管道有两种操作:
第⼀种,中间操作(intermediate)。调⽤中间操作⽅法返回的是⼀个新的流对象
第⼆种,终值操作(terminal)。在调⽤该⽅法后,将执⾏之前所有的中间操作,并返回结果
5、流处理的执⾏顺序
为了更好地演⽰效果,我们⾸先要了解⼀下 Stream.peek() ⽅法,这个⽅法和 Stream.forEach() 使⽤⽅法类似,都接受 Consumer 作为参数
流操作⽅法流操作类型
peek()中间操作
forEach()终值操作
所以,我们可以⽤ peek 来证明流的执⾏顺序。
我们定义⼀个 Apple 对象:
der;
/**
* @author godfrey
* @since 2021-08-15
*/
public class Apple {
/**
* 编号
*/
private int id;
/**
* 颜⾊
*/
private String color;
/**
* 重量
*/
private int weight;
/**
* 产地
*/
private String birthplace;
public Apple(int id, String color, int weight, String birthplace) {
this.id = id;
this.weight = weight;
this.birthplace = birthplace;
}
//Setter、Getter省略
}
然后创建多个苹果放到 appleStore 中
public class StreamTest {
private static final List<Apple> appleStore = Arrays.asList(
new Apple(1, "red", 500, "湖南"),
new Apple(2, "red", 100, "天津"),
new Apple(3, "green", 300, "湖南"),
new Apple(4, "green", 200, "天津"),
new Apple(5, "green", 100, "湖南")java stream
);
public static void main(String[] args) {
appleStore.stream().filter(apple -> Weight() > 100)
.peek(apple -> System.out.println("通过第1层筛选 " + apple))
.filter(apple -> "green".Color()))
.peek(apple -> System.out.println("通过第2层筛选 " + apple))
.filter(apple -> "湖南".Birthplace()))
.
peek(apple -> System.out.println("通过第3层筛选 " + apple))
.List());
}
}
测试结果如下:
以上测试例⼦的执⾏顺序⽰意图:
总之,执⾏顺序会⾛⼀个“之”字形
注意:
如果我们注释掉 .List()),我们会发现⼀⾏语句也不会打印出来。这刚好证明了:
通过连续执⾏多个操作倒便就组成了 Stream 中的执⾏管道(pipeline)。需要注意的是这些管道被添加后并不会真正执⾏,只有等到调⽤终值操作之后才会执⾏。
6、⽤流收集数据与 SQL 统计函数
Collector 被指定和四个函数⼀起⼯作,并实现累加 entries 到⼀个可变的结果容器,并可选择执⾏该结果的最终变换。这四个函数就是:
接⼝函数作⽤返回值
supplier()创建并返回⼀个新的可变结果容器Supplier
accumulator()把输⼊值加⼊到可变结果容器BiConsumer
combiner()将两个结果容器组合成⼀个BinaryOperator
finisher()转换中间结果为终值结果Function
Collectors 则是重要的⼯具类,提供给我⼀些 Collector 实现。 Stream 接⼝中 collect() 就是使⽤ Collector 做参数的。其中,collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) ⽆⾮就是⽐ Collector 少⼀个 finisher,本质上是⼀样的!
遍历在传统的 javaEE 项⽬中数据源⽐较单⼀⽽且集中,像这类的需求都我们可能通过关系数据库中进⾏获取计算。
现在的互联⽹项⽬数据源成多样化有:关系数据库、NoSQL、Redis、mongodb、ElasticSearch、Cloud Server 等。这时就需我们从各数据源中汇聚数据并进⾏统计。
Stream + Lambda的组合就是为了让 Java 语句更像查询语句,取代繁杂的 for 循环。
CREATE TABLE `applestore` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '编号',
`color` VARCHAR (50) COMMENT '颜⾊',
`weight` INT COMMENT '重量',
`birthplace` VARCHAR (50) COMMENT '产地',
PRIMARY KEY (`id`)
) COMMENT = '⽔果商店';
另外还有数据初始化语句
INSERT INTO applestore VALUES (1, "red", 500,"湖南");
INSERT INTO applestore VALUES (2, "red", 100,"湖南");
INSERT INTO applestore VALUES (3, "green", 300, "湖南");
INSERT INTO applestore VALUES (4, "green", 200, "天津");
INSERT INTO applestore VALUES (5, "green", 100, "湖南");
测试⽤例:
public class StreamStatisticsTest {
List<Apple> appleStore;
@Before
public void initData() {
appleStore = Arrays.asList(
new Apple(1, "red", 500, "湖南"),
new Apple(2, "red", 100, "天津"),
new Apple(3, "green", 300, "湖南"),
new Apple(4, "green", 200, "天津"),
new Apple(5, "green", 100, "湖南")
);
}
@Test
public void test1() {
Integer weight1 = appleStore.stream().collect(Collectors.summingInt(apple -> Weight()));
System.out.println(weight1);
Integer weight2 = appleStore.stream().collect(Collectors.summingInt(Apple::getWeight));
System.out.println(weight2);
}
}
6.1、求和
Collectors.summingInt()
Collectors.summingLong()
Collectors.summingDouble()
通过引⽤import static java.util.stream.Collectors.summingInt就可以直接调⽤ summingInt() Apple::getWeight() 可以写为 apple -> Weight(),求和函数的参数是结果转换函数 Function
6.2、求平均值
Collectors.averagingInt()
Collectors.averagingKLong()
Collectors.averagingDouble()
6.3、归约
@Test
public void reduce() {
Integer sum = appleStore.stream().collect(reducing(0, Apple::getWeight, (a, b) -> a + b));
System.out.println(sum);
}
归约就是为了遍历数据容器,将每个元素对象转换为特定的值,通过累积函数,得到⼀个最终值。
转换函数,函数输⼊参数的对象类型是跟 Stream 中的 T ⼀样的对象类型,输出的对象类型的是和初始值⼀样的对象类型
累积函数,就是把转换函数的结果与上⼀次累积的结果进⾏⼀次合并,如果是第⼀次累积,那么取初始值来计算
累积函数还可以作⽤于两个 Stream 合并时的累积,这个可以结合 groupingBy 来理解
初始值的对象类型,和每⼀次累积函数输出值的对象类型是相同的,这样才能⼀直进⾏累积函数的运
算。
归约不仅仅可以⽀持加法,还可以⽀持⽐如乘法以及其他更⾼级的累积公式。
计数只是归约的⼀种特殊形式
6.4、分组
分组就和 SQL 中的 GROUP BY ⼗分类似,所以 groupingBy() 的所有参数中有⼀个参数是 Collector接⼝,这样就能够和求和/求平均值/归约⼀起使⽤。
传⼊参数的接⼝是 Function 接⼝,实现这个接⼝可以是实现从 A 类型到 B 类型的转换
其中有⼀个⽅法可以传⼊参数Supplier mapFactory,这个可以通过⾃定义 Map⼯⼚,来创建⾃定义的分组 Map
分区只是分组的⼀种特殊形式
Collectors.partitioningBy() 传⼊参数的是 Predicate 接⼝,
分区相当于把流中的数据,分组分成了“正反两个阵营”
7、数值流
我们之前在求和时⽤到的例⼦,appleStore.stream().collect(summingInt(Apple::getWeight)),我就被 IDEA 提醒:
appleStore.stream().collect(summingInt(Apple::getWeight))
The 'collect(summingInt())' can be replaced with 'mapToInt().sum()'
这就告诉我们可以先转化为数值流,然后再⽤ IntStream 做求和。
Java8引⼊了三个原始类型特化流接⼝:IntStream,LongStream,DoubleStream,分别将流中的元素特化为 int,long,double。
普通对象流和原始类型特化流之间可以相互转化
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论