Java 8 Stream
概要
- Java 8 API 添加了一种新的抽象称为流Stream,Stream以一种声明的方式处理数据。
- Stream以一种类似SQL语句从数据库查询数据的直观方式在Java集合运算和表达的抽象中体现。
- Stream API 可以极大提高Java程序员的生产力并写出高效率、干净、简洁的代码。
- 将集合看作流,流通过管道传输并在节点上进行筛选、排序、聚合等处理。
- 管道处理分为中间操作处理(intermediate operation)和最终操作处理(terminal operation)。
什么是Stream?
Stream(流)是一个来自数据源的元素队列并支持聚合操作。
注:
元素队列:特定类型对象并形成一个队列。
数据源:流的来源。可以是集合,数组,I/O channel,产生器generator等。
聚合操作:filter、map、reduce、find、match、sorted等中间操作。
Pipelining::中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style)。这样做可以对操作进行优化,比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代:以前对集合遍历都是通过Iterator或者For-Each的方式,显式的在集合外部进行迭代,这叫做外部迭代。Stream提供了内部迭代的方式,通过访问者模式(Visitor)实现。
生成流
使用Collection下的 stream() 和 parallelStream() 方法。
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream(); //获取一个顺序流
Stream<String> parallelStream = list.parallelStream(); //获取一个并行流
使用Arrays 中的 stream() 方法,将数组转成流。
Integer[] nums = new Integer[10];
Stream<Integer> stream = Arrays.stream(nums);
使用Stream中的静态方法:of()、iterate()、generate()。
Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 2).limit(6);
stream2.forEach(System.out::println); // 0 2 4 6 8 10
Stream<Double> stream3 = Stream.generate(Math::random).limit(2);
stream3.forEach(System.out::println);
使用 BufferedReader.lines() 方法,将每行内容转成流。
BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream<String> lineStream = reader.lines();
lineStream.forEach(System.out::println);
使用 Pattern.splitAsStream() 方法,将字符串分隔成流。
Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);
中间操作
forEach
Stream提供'forEach'来迭代流中的元素,对比之前的几种迭代方式如下:
public class Test {
public static void main(String[] args) {
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
//1、迭代器iterator
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//2、传统for循环
for (int i = 0; i < strings.size(); i++) {
if(!strings.get(i).isEmpty()) {
System.out.println(strings.get(i));
}
}
//3、foreach增强
for (String string : strings) {
if(!string.isEmpty()) {
System.out.println(string);
}
}
//4、foreach
strings.forEach(System.out::println);
}
}
filter
用于筛选符合条件的元素。
public class Test {
public static void main(String[] args) {
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
strings.stream().filter(str -> !str.isEmpty()).forEach(System.out::println);
}
}
limit
用于获取指定数量的流。
public class Test {
public static void main(String[] args) {
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
strings.stream().limit(5).forEach(System.out::println);
}
}
sorted
用于对流中元素的排序操作。
public class Test {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5,8,9,7,11);
//升序
list.stream().sorted().forEach(System.out::println);
//降序
list.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
//多条件排序
//list.stream().sorted(Comparator.comparing(Class::method).thenComparing(Class::method))
}
}
map、flatMap
相同点:
- 都是依赖FuncX(入参,返回值)进行转换(将一个类型依据程序逻辑转换成另一种类型,根据入参和返回值)。
- 都能在转换后直接被subscribe
区别:
- map返回的是结果集,flatmap返回的是包含结果集的Observable(返回结果不同)
- map被订阅时每传递一个事件执行一次onNext方法,flatmap多用于多对多,一对多,再被转化为多个时,一般利用from/just进行一一分发,被订阅时将所有数据传递完毕汇总到一个Observable然后一一执行onNext方法(执行顺序不同)>>>>(如单纯用于一对一转换则和map相同)
- map只能单一转换,单一只的是只能一对一进行转换,指一个对象可以转化为另一个对象但是不能转换成对象数组(map返回结果集不能直接使用from/just再次进行事件分发,一旦转换成对象数组的话,再处理集合/数组的结果时需要利用for一一遍历取出,而使用RxJava就是为了剔除这样的嵌套结构,使得整体的逻辑性更强。)
- flatmap既可以单一转换也可以一对多/多对多转换,flatmap要求返回Observable,因此可以再内部进行from/just的再次事件分发,一一取出单一对象(转换对象的能力不同)
public class Test {
List<String[]> eggs = new ArrayList<>();
@Before
public void init() {
// 第一箱鸡蛋
eggs.add(new String[]{"鸡蛋_1", "鸡蛋_1", "鸡蛋_1", "鸡蛋_1", "鸡蛋_1"});
// 第二箱鸡蛋
eggs.add(new String[]{"鸡蛋_2", "鸡蛋_2", "鸡蛋_2", "鸡蛋_2", "鸡蛋_2"});
}
// 自增生成组编号
static int group = 1;
// 自增生成学生编号
static int student = 1;
/**
* 把二箱鸡蛋分别加工成煎蛋,还是放在原来的两箱,分给2组学生
*/
@Test
public void map() {
eggs.stream()
.map(x -> Arrays.stream(x).map(y -> y.replace("鸡", "煎")))
.forEach(x -> System.out.println("组" + group++ + ":" + Arrays.toString(x.toArray())));
/*
控制台打印:------------
组1:[煎蛋_1, 煎蛋_1, 煎蛋_1, 煎蛋_1, 煎蛋_1]
组2:[煎蛋_2, 煎蛋_2, 煎蛋_2, 煎蛋_2, 煎蛋_2]
*/
}
/**
* 把二箱鸡蛋分别加工成煎蛋,然后放到一起【10个煎蛋】,分给10个学生
*/
@Test
public void flatMap() {
eggs.stream()
.flatMap(x -> Arrays.stream(x).map(y -> y.replace("鸡", "煎")))
.forEach(x -> System.out.println("学生" + student++ + ":" + x));
/*
控制台打印:------------
学生1:煎蛋_1
学生2:煎蛋_1
学生3:煎蛋_1
学生4:煎蛋_1
学生5:煎蛋_1
学生6:煎蛋_2
学生7:煎蛋_2
学生8:煎蛋_2
学生9:煎蛋_2
学生10:煎蛋_2
*/
}
}
并行(parallel)
充分利用多线程,提高程序运行效率
public class Test {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(5,8,9,7,11);
list.parallelStream().forEach(System.out::println);
}
}
注意:使用并行流虽然可以提高程序运行效率,但是不代表所有情况都得使用并行流,盲目使用可能导致以下结果:
- 效率不增反减。
- 增加额外的复杂度,程序更加容易出错。
- 运行结果不正确。
解释:
- parallel stream是基于fork/join框架的,简单点说就是使用多线程来完成的,使用parallel stream时要考虑初始化fork/join框架的时间,如果要执行的任务很简单,那么初始化fork/join框架的时间会远多于执行任务所需时间,也就导致了效率的降低.根据附录doug Lee的说明,任务数量*执行方法的行数>=10000或者执行的是消耗大量时间操作(如io/数据库)才有必要使用。
- 在spring框架中,假设有一组主键id,使用这组id去数据库获取记录。
//DB.fetchRecord(long id)使用当前线程session连接数据库 ids.parallelStream().map(DB::fetchRecord).collect(Collections.toList());
//这里使用parallel stream是正确的,但是运行会报错,类似于 can't obtain session from current thread.原因就是多线程运行,对应的线程没有绑定的session,要完成上面的功能需要提供一个特殊版本的DB方法
- session问题已经解决,如果获取到的记录需要和ids顺序相同,那么使用parallel获取到的结果就是不正确的,原因还是因为多线程。
如何正确使用:
- 确保要执行的任务对线程环境没有依赖
- 任务消耗时间长/数据量大到不用思考是否要用parallel
- 结果没有顺序要求
流收集数据
通过Collectors类提供的工厂方法(例如groupingBy)创建的收集器来完成。
Collectors.toList:流对象返回List对象
List<String> strings = Arrays.asList("sad"," ","wqe","","cv","gj");
System.out.println(strings.stream().filter(s -> !s.isBlank()).collect(Collectors.toList()));
Collectors.joining:流对象toString后返回字符串
List<String> strings = Arrays.asList("sad"," ","wqe","","cv","gj");
System.out.println(strings.stream().filter(s -> !s.isBlank()).collect(Collectors.joining("-")));
Collectors.counting:流元素计数
List<String> strings = Arrays.asList("sad"," ","wqe","","cv","gj");
System.out.println(strings.stream().filter(s -> !s.isBlank()).collect(Collectors.counting()));
System.out.println(strings.stream().filter(s -> !s.isBlank()).count());
Collectors.maxBy 、 Collectors.minBy:计算流中的最大值或最小值
List<Integer> integers = Arrays.asList(5,50,20,1,100,253);
Comparator<Integer> comparator = Integer::compareTo;
System.out.println(integers.stream().collect(Collectors.maxBy(comparator)).get());
//System.out.println(integers.stream().max(Comparator.comparing(Integer::intValue)).get());
System.out.println(integers.stream().collect(Collectors.minBy(comparator)).get());
System.out.println(integers.stream().max(comparator).get());
System.out.println(integers.stream().min(comparator).get());
Collectors.summingInt / Double / Long:数值求和
List<Integer> integers = Arrays.asList(5,50,20,1,100,253);
System.out.println(integers.stream().collect(Collectors.summingInt(Integer::intValue)));
System.out.println((Integer) integers.stream().mapToInt(Integer::intValue).sum());
Collectors.averagingInt / Double / Long:数组平均数
List<Integer> integers = Arrays.asList(5,50,20,1,100,253);
System.out.println(integers.stream().collect(Collectors.averagingInt(Integer::intValue)));
Collectors.summarizingInt / Double / Long:多元素计数汇总
List<Integer> integers = Arrays.asList(5,50,20,1,100,253);
System.out.println(integers.stream().collect(Collectors.summarizingInt(Integer::intValue)));
//IntSummaryStatistics{count=6, sum=429, min=1, average=71.500000, max=253}
Collectors.reducing:流元素规约
//求和
List<Integer> integers = Arrays.asList(5,50,20,1,100,253);
System.out.println(integers.stream().collect(Collectors.reducing(0,Integer::intValue,(i,j) -> i+j)));
System.out.println(integers.stream().map(Integer::intValue).reduce(0, (i, j) -> i + j));
System.out.println(integers.stream().reduce(0, (i, j) -> i + j));
System.out.println(integers.stream().reduce(0, Integer::sum));
Collectors.groupingBy:流元素分组
List<Integer> list = Arrays.asList(15,20,24,18,50);
System.out.println(list.stream().collect(Collectors.groupingBy(item -> "分组")));
//{分组=[15, 20, 24, 18, 50]}
Collectors.partitioningBy:流元素分区
List<Integer> list = Arrays.asList(15,20,24,18,50);
System.out.println(list.stream().collect(Collectors.partitioningBy(item -> item.intValue() > 20)));
//{false=[15, 20, 18], true=[24, 50]}