java8流式编程
Stream API
介绍
流式编程定义
对于java来说,我们最常用的面向对象编程属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming),函数式编程(Functional Programming)。函数式编程java8也导入了,结合 Lambda 表达式,对于函数式接口的实现和使用变得灵活和简单了。流式编程是一个受到 函数式编程 和 多核时代影响而产生的东西。其实,流式编程就是基于JDK8 的Stream对于集合一系列的操作的流程定义。
Stream 简介
我们之前对于集合的操作,无论是找一个特定的对象,还是对集合类特定的对象进行处理,或者排序,更加麻烦的是,我们还会需要对集合进行处理后,返回一些符合要求的特定的集合,或者,多次操作,这些,JDK都没有提供任何的方法,我们都需要对集合进行遍历,写一段很冗余的代码。 JDK8加入了 java.util.stream包,实现了集合的流式操作,流式操作包括集合的过滤,排序,映射等功能。根据流的操作性,又可以分为 串行流 和 并行流。根据操作返回的结果不同,流式操作又分为中间操作(返回一特定类型的结果)和最终操作(返回流本身)。大大方便了我们对于集合的操作。
Stream 作为 Java 8 的一大亮点,它与 java.io包里的 InputStream 和 OutputStream 是完全不同的概念。
Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作 。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性.
以前我们处理复杂的数据只能通过各种for循环,不仅不美观,而且时间长了以后可能自己都看不太明白以前的代码了,但有Stream以后,通过filter,map,limit等等方法就可以使代码更加简洁并且更加语义化。
Stream中间操作,多个中间操作可以连接起来形成一个流水线,除非流水线上触发了终止操作,否则中间不会执行任何处理!而终止操作时会一次性全部处理,称为惰性处理。要进行流操作首先要获取流。
常用的stream三种创建方式
- 集合
Collection.stream()
Stream<Student> stream = stus.stream();
Stream<Student> stream2 = stus.parallelStream();
- 静态方法
Stream.of
//2.静态方法*
Stream stream2 = Stream.of(“a”, “b”, “c”);
- 数组
Arrays.stream
String[] arr = {“a”,“b”,“c”};
Stream stream3 = Arrays.stream(arr);
数据准备
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
@Slf4j
public class StreamTest {
public static List stus = new ArrayList();
static {
Student s1 = new Student("2015134001", "小3", 15, "1501");
Student s2 = new Student("2015134003", "小4", 14, "1503");
Student s3 = new Student("2015134006", "小5", 15, "1501");
Student s4 = new Student("2015134008", "小6", 17, "1505");
Student s5 = new Student("2015134008", "小7", 19, "1505");
Student s6 = new Student("2015134008", "小8", 76, "1505");
Student s7 = new Student("2015134008", "小9", 7, "1505");
stus.add(s1);
stus.add(s2);
stus.add(s3);
stus.add(s4);
stus.add(s5);
stus.add(s6);
stus.add(s7);
stus.add(new Student("2015134012", "小c", 13, "1503"));
stus.add(new Student("2015134013", "小s", 14, "1503"));
stus.add(new Student("2015134015", "小d", 15, "1504"));
stus.add(new Student("2015134018", "小y", 16, "1505"));
}
public static void main (String[] args){
log.error("长度 {}",stus.size());
Stream<Student> stream = stus.stream();
Stream<Student> stream2 = stus.parallelStream();
}
}
@Data
@AllArgsConstructor
class Student {
private String id ;
private String name ;
private int age ;
private String grade ;
}
@Data
@AllArgsConstructor
class Pepole {
private String id ;
private String name ;
private int age ;
}
所有的对流的操作可以分为4种,分别为筛选与分片,映射,排序,终结(归约,收集)
常用的流操作
stream是一个接口,最后都是在ReferencePipeline这个类中实现的
常用的方法:
-
-
boolean
allMatch(Predicate<? super T> predicate)
返回此流的所有元素是否与提供的谓词匹配。boolean
anyMatch(Predicate<? super T> predicate)
返回此流的任何元素是否与提供的谓词匹配。static <T> Stream.Builder<T>
builder()
返回一个Stream
的构建器。<R,A> R
collect(Collector<? super T,A,R> collector)
使用 Collector对此流的元素执行 mutable reductionCollector
。<R> R
collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
对此流的元素执行 mutable reduction操作。static <T> Stream<T>
concat(Stream<? extends T> a, Stream<? extends T> b)
创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。long
count()
返回此流中的元素数。Stream<T>
distinct()
返回由该流的不同元素(根据Object.equals(Object)
)组成的流。static <T> Stream<T>
empty()
返回一个空的顺序Stream
。Stream<T>
filter(Predicate<? super T> predicate)
返回由与此给定谓词匹配的此流的元素组成的流。Optional<T>
findAny()
返回描述流的一些元素的Optional
如果流为空,则返回一个空的Optional
。Optional<T>
findFirst()
返回描述此流的第一个元素的Optional
如果流为空,则返回一个空的Optional
。<R> Stream<R>
flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
返回由通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流。DoubleStream
flatMapToDouble(Function<? super T,? extends DoubleStream> mapper)
返回一个DoubleStream
,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。IntStream
flatMapToInt(Function<? super T,? extends IntStream> mapper)
返回一个IntStream
,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。LongStream
flatMapToLong(Function<? super T,? extends LongStream> mapper)
返回一个LongStream
,其中包含将该流的每个元素替换为通过将提供的映射函数应用于每个元素而产生的映射流的内容的结果。void
forEach(Consumer<? super T> action)
对此流的每个元素执行操作。void
forEachOrdered(Consumer<? super T> action)
如果流具有定义的遇到顺序,则以流的遇到顺序对该流的每个元素执行操作。static <T> Stream<T>
generate(Supplier<T> s)
返回无限顺序无序流,其中每个元素由提供的Supplier
。static <T> Stream<T>
iterate(T seed, UnaryOperator<T> f)
返回有序无限连续Stream
由函数的迭代应用产生f
至初始元素seed
,产生Stream
包括seed
,f(seed)
,f(f(seed))
,等Stream<T>
limit(long maxSize)
返回由此流的元素组成的流,截短长度不能超过maxSize
。<R> Stream<R>
map(Function<? super T,? extends R> mapper)
返回由给定函数应用于此流的元素的结果组成的流。DoubleStream
mapToDouble(ToDoubleFunction<? super T> mapper)
返回一个DoubleStream
,其中包含将给定函数应用于此流的元素的结果。IntStream
mapToInt(ToIntFunction<? super T> mapper)
返回一个IntStream
,其中包含将给定函数应用于此流的元素的结果。LongStream
mapToLong(ToLongFunction<? super T> mapper)
返回一个LongStream
,其中包含将给定函数应用于此流的元素的结果。Optional<T>
max(Comparator<? super T> comparator)
根据提供的Comparator
返回此流的最大元素。Optional<T>
min(Comparator<? super T> comparator)
根据提供的Comparator
返回此流的最小元素。boolean
noneMatch(Predicate<? super T> predicate)
返回此流的元素是否与提供的谓词匹配。static <T> Stream<T>
of(T... values)
返回其元素是指定值的顺序排序流。static <T> Stream<T>
of(T t)
返回包含单个元素的顺序Stream
。Stream<T>
peek(Consumer<? super T> action)
返回由该流的元素组成的流,另外在从生成的流中消耗元素时对每个元素执行提供的操作。Optional<T>
reduce(BinaryOperator<T> accumulator)
使用 associative累积函数对此流的元素执行 reduction ,并返回描述减小值的Optional
(如果有)。T
reduce(T identity, BinaryOperator<T> accumulator)
使用提供的身份值和 associative累积功能对此流的元素执行 reduction ,并返回减小的值。<U> U
reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
执行 reduction在此流中的元素,使用所提供的身份,积累和组合功能。Stream<T>
skip(long n)
在丢弃流的第一个n
元素后,返回由该流的n
元素组成的流。Stream<T>
sorted()
返回由此流的元素组成的流,根据自然顺序排序。Stream<T>
sorted(Comparator<? super T> comparator)
返回由该流的元素组成的流,根据提供的Comparator
进行排序。Object[]
toArray()
返回一个包含此流的元素的数组。<A> A[]
toArray(IntFunction<A[]> generator)
使用提供的generator
函数返回一个包含此流的元素的数组,以分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组。
-
-
最终操作:返回一特定类型的结果。
-
中间操作:返回流本身。通过这种方式可以将多个中间操作连接起来,形成一个调用链,从而转换为另外 一个流。除非调用链后存在一个终端操作,否则中间操作对流不会进行任何结果处理。
返回stream的就是中间操作,其他的,返回具体对象的就是最终操作:
中间操作:
filter(): 对元素进行过滤
sorted():对元素排序
map():元素映射
distinct():去除重复的元素
最终操作:
forEach():遍历每个元素。
findFirst():找第一个符合要求的元素。
reduce():把Stream 元素组合起来。例如,字符串拼接,数值的 sum,min,max ,average 都是特殊的 reduce。
collect():返回一个新的数据结构,基于Collectors有丰富的处理方法。
min():找到最小值。
max():找到最大值。
需要注意的是,一般中间操作之后,都是为了进行最终操作,得到我们需要的对象。
常用的方法
1、筛选 对于集合的操作,经常性的会涉及到对于集中符合条件的数据筛选,Stream中对于数据筛选两个常见的API: filter(过滤)、distinct(去重)
filter过滤操作
-
filter
Stream<T> filter(Predicate<? super T> predicate)
返回由与此给定谓词匹配的此流的元素组成的流。
这是一个intermediate operation 。
-
参数
predicate
-一个 non-interfering , stateless谓词应用到每个元素,以确定是否它应包含 -
结果
新的流
Stream<T> filter(Predicate<? super T> predicate);
这个是filter这个接口的定义,filter方法接收一个Predicate类型参数用于对目标集合进行过滤Predicate 是函数式接口,可以直接用Lambda表达式进行实现。Lambda表达式格式就是a -> b ,其中a表示入参,b表示函数体,而它返回的,还是一个Stream,我们可以继续对它进行相关操作
-
例子:
Stream<Student> stream = stus.stream();
List list2 = stream .filter(e -> e.getAge()>20).collect(Collectors.toList());
log.info("集合: {}" ,list2);
结果
[Student(id=2015134008, name=小8, age=76, grade=1505)]
同理,map的遍历也是一样,先把map转换为entry的集合,然后生成流,只是这时候元数据是一个entry,map.entrySet().stream().filter(a -> a.getKey()==1 && a.getValue() > 2).collect(Collectors.toMap((a) -> a.getKey(), (a) -> a.getValue()));. 这里a代表的是entry。
切片
limit()实现数据截取
该方法会返回一个不超过给定长度的流
案例:获取数组的前五位
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
integers.stream().limit(5);
基于skip()实现数据跳过
//从集合第三个开始截取5个数据
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
List<Integer> collect = integers.stream().skip(3).limit(5).collect(Collectors.toList());
collect.forEach(integer -> System.out.print(integer+" "));
//先从集合中截取5个元素,然后取后3个
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 4, 5, 5, 6, 7, 8, 2, 2, 2, 2);
List<Integer> collect = integers.stream().limit(5).skip(2).collect(Collectors.toList());
collect.forEach(integer -> System.out.print(integer+" "));
sorted排序
我们之前的排序,基本都是new一个Comparator,其重写compare方法,还是很麻烦的,Stream中提供了针对排序的方法,Stream sorted(Comparator<? super T> comparator);传入的是一个Comparator的实现类,大家可能也想到了,Comparator也是一个函数式接口,里面就一个抽象方法int compare(T o1, T o2);调用这个方法,一般都是比较对象的某个属性,这个时候可以用Lambda表达式的方法应用写法,比如:Student::getAge,也就是比较学生的年级进行排序,这里的排序都是自然顺序,也就是正序的,想要倒序,调用reversed方法就ok。
示例
//List list2 = stream.sorted((e1,e2)-> e2.getAge()-e1.getAge()).collect(Collectors.toList());
//List list3 = stream.sorted((e1,e2)-> e1.getAge()-e2.getAge()).collect(Collectors.toList());
//继续用thenComparing对排序后的stream再进行排序操作
List list4 = stream.sorted(Comparator.comparing(Student::getAge).thenComparing(Student::getName)).collect(Collectors.toList());
log.info("集合: {}" ,list4);
映射 map元素映射
-
map
<R> Stream<R> map(Function<? super T,? extends R> mapper)
返回由给定函数应用于此流的元素的结果组成的流。
这是一个intermediate operation 。
-
参数类型
R
- 新流的元素类型 -
参数
mapper
-一个 non-interfering , stateless函数应用到每个元件 -
结果
新的流
这个方法比较简单,一般是用于生成对象中某些属性的新的集合,比如,取学生年龄的集合
传入参数为Function 也是一个函数式接口
List<Integer> list4 = stream.map(Student::getAge).collect(Collectors.toList()); log.info("集合: {}" ,list4); //集合: [15, 14, 15, 17, 19, 76, 7, 13, 14, 15, 16]
-
匹配
判断集合中某些元素是否匹配对应的条件,如果有的话,在进行后续的操作。在 Stream API中也提供了相关方法供我们进行使用,如anyMatch、allMatch等。他们对应的就是&&和||运算符。
基于anyMatch()判断条件至少匹配一个元素
anyMatch()主要用于判断流中是否至少存在一个符合条件的元素,它会返回一个boolean值,并且对于它的操作, 一般叫做短路求值
if(students.stream().anyMatch(student -> student.getAge() < 20)){
System.out.println("集合中有年龄小于20的学生");
}else {
System.out.println("集合中没有年龄小于20的学生");
}
基于allMatch()判断条件是否匹配所有元素
allMatch()的工作原理与anyMatch()类似,但是anyMatch执行时,只要流中有一个元素符合条件就会返回true, 而allMatch会判断流中是否所有条件都符合条件,全部符合才会返回true
if(students.stream().allMatch(student -> student.getAge() < 20)){
System.out.println("集合所有学生的年龄都小于20");
}else {
System.out.println("集合中有年龄大于20的学生");
}
distinct去重
去掉集合中的重复元素
-
-
-
Stream<T> distinct()
返回由该流的不同元素(根据
Object.equals(Object)
)组成的流。对于有序流,选择不同的元素是稳定的(对于重复的元素,首先在遇到顺序中出现的元素被保留。)对于无序流,不能保证稳定性。
这是一个stateful intermediate operation 。
-
API Note:
保存稳定性为
distinct()
在并行管线是相对昂贵的(要求操作充当一个完整屏障,具有大量缓冲的开销)通常不需要的,和稳定性。 使用无序流源(如generate(Supplier)
)或具有除去排序约束BaseStream.unordered()
可导致显著更高效的执行为distinct()
在并行管线,如果情况许可的语义。 如果需要与遇到顺序一致,distinct()
在并行流水线中使用distinct()
您的性能或内存利用率不佳,则使用BaseStream.sequential()
切换到顺序执行可能会提高性能。 -
结果
新的流
-
-
-
List<Integer> list4 = stream.map(Student::getAge).distinct().collect(Collectors.toList());
log.info("集合: {}" ,list4);
//集合: [15, 14, 17, 19, 76, 7, 13, 16]
forEach遍历处理
-
-
-
void forEach(Consumer<? super T> action)
对此流的每个元素执行操作。
这是一个terminal operation 。
这个操作的行为显然是不确定的。 对于并行流管道,此操作不保证遵守流的遇到顺序,因为这样做将牺牲并行性的好处。 对于任何给定的元素,动作可以在图书馆选择的任何时间和任何线索中执行。 如果操作访问共享状态,则负责提供所需的同步。
-
参数
action
- 一个 non-interfering对元素执行的动作
-
-
涉及到对集合中元素的操作的,都会使用这个方法。void forEach(Consumer<? super T> action);入参是一个Consumer接口,这个接口也是一个函数式接口,他有两个方法,一个是void accept(T t);,一个是andThen的方法,可以理解为入参是流中的数据元,然后调用覆盖的方法,覆盖数据元。因为他是直接更改了六种的数据,也是最终操作,所以,集合的元素是直接改变的。
-
stream.map(Student::getAge).distinct().collect(Collectors.toList()).forEach(e-> System.out.println(e.toString()));
长度 11
15
14
17
19
76
7
13
16
查找
对于集合操作,有时需要从集合中查找中符合条件的元素,Stream中也提供了相关的API,findAny()和 findFirst(),他俩可以与其他流操作组合使用。findAny用于获取流中随机的某一个元素,findFirst用于获取流中的 第一个元素。至于一些特别的定制化需求,则需要自行实现。
findFirst,findAny
判断一个集合中是否有某一个对象的方法,Optional<T> findAny();
没有入参,返回的是一个Optional的对象。我们确定是否包含,可以调用Optional.isPresent()方法。
Optional any = test.stream().filter(student -> student.getAge() > 10).findAny();
if(any.isPresent()){
//表示包含
}
分组 聚合groupingBy方法
将一个List集合中的对象按照某个特定的属性去整合成一个Map<key, Lsit>d的操作,这个时候使用流式编程的聚合方法,就很方便。
List<MetadataPropertyDO> list = metadataPropertyMapper.getList(new HashMap<>());
if (CollectionUtils.isNotEmpty(list)) {
Map<Long, List<MetadataPropertyDO>> collect = list
.stream()
.filter(metadataPropertyDO -> metadataPropertyDO.getKaId() != null)
.collect(Collectors.groupingBy(MetadataPropertyDO::getKaId));
}
归约
到现在截止,对于流的终端操作,我们返回的有boolean、Optional和List。但是在集合操作中,我们经常会涉及 对元素进行统计计算之类的操作,如求和、求大值、小值等,从而返回不同的数据结果。
reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)
归约是将集合中的所有元素经过指定运算,折叠成一个元素输出,如:求最值、平均数等,这些操作都是将一个集合的元素折叠成一个元素输出。
在流中,reduce函数能实现归约。
reduce函数接收两个参数:
初始值
进行归约操作的Lambda表达式
用于组合流中的元素,如求和,求积,求最大值等
int age = list.stream().reduce(0, (person1,person2)->person1.getAge()+person2.getAge());
//计算年龄总和:
int sum = list.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
//与之相同:
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
其中,reduce 第一个参数 0 代表起始值为 0,lambda (a, b) -> a + b 即将两值相加产生一个新值
同样地:
//计算年龄总乘积:
int sum = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);
当然也可以
Optional sum = list.stream().map(Person::getAge).reduce(Integer::sum);
即不接受任何起始值,但因为没有初始值,需要考虑结果可能不存在的情况,因此返回的是 Optional 类型
收集器
通过使用收集器,可以让代码更加方便的进行简化与重用。其内部主要核心是通过Collectors完成更加复杂的计算 转换,从而获取到终结果。并且Collectors内部提供了非常多的常用静态方法,直接拿来就可以了。比方说: toList。
性能
1.对于简单操作,比如最简单的遍历,Stream串行API性能明显差于显示迭代,但并行的Stream API能够发挥多核特性。
2.对于复杂操作,Stream串行API性能可以和手动实现的效果匹敌,在并行执行时Stream API效果远超手动实现。
所以,如果出于性能考虑,1. 对于简单操作推荐使用外部迭代手动实现,2. 对于复杂操作,推荐使用Stream API, 3. 在多核情况下,推荐使用并行Stream API来发挥多核优势,4.单核情况下不建议使用并行Stream API。
如果出于代码简洁性考虑,使用Stream API能够写出更短的代码。即使是从性能方面说,尽可能的使用Stream API也另外一个优势,那就是只要Java Stream类库做了升级优化,代码不用做任何修改就能享受到升级带来的好处。
流式编程性能不是最好的,但我们还是会选择流式编程,因为函数式代码中使用 Stream
有 3 个好处:
Stream
简洁、富于表达、非常优雅,而且代码读起来就像是问题陈述。- Stream 采用了惰性计算,这使得它在您的程序中非常高效。
- 它可以并行使用。