📢📢📢本篇博文部分内容借鉴了以下二篇博文:
廖雪峰官方博客函数式编程:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943847278976
慕课教程Streamapi:https://www.imooc.com/wiki/javalesson/streamapi.html
只包含一个抽象方法的接口,称为函数式接口,也称为SAM接口,即Single Abstract Method interfaces,用@FunctionalInterface注解进行标注,常见的函数式接口如Comparator,Runnable,Callable。
例子:
@FunctionalInterface
interface A {
public void test(); // 只能包含一个抽象方法
}
并且还可以使用泛型,如下:
@FunctionalInterface
interface A<T> {
public void test(T t); // 只能包含一个抽象方法
}
函数式接口不仅仅只定义一个抽象方法,还可以定义
下面以 Comparator函数式接口为例:
这里的 equals(Object obj)方法来自于java.lang.Object里的public方法boolean equals(Object obj);
函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式,Java从JDK1.8开始支持这种风格 (
针对函数式接口的一种简单写法
)。
例子:
@FunctionalInterface
interface A {
public void test(int a); // 只能包含一个抽象方法
}
class TestB {
public static void Test(Integer num, A a) {
a.test(num);
}
}
// 使用匿名内部类
TestB.Test(1, new A() {
@Override
public void test(int a) {
System.out.println("【方式1】实现A接口的test方法,num="+a);
}
});
在以前对于这种
函数式接口参数
我们使用匿名内部类的方式传递变量过去,从Java 8开始,我们可以用Lambda表达式进行简化。
// 使用Lambda表达式
TestB.Test(2,(int a)->{
System.out.println("【方式2】实现A接口的test方法,num="+a);
});
运行效果:
Lambda表达式在特殊情况下还可以进一步简化,如下:
未简化前:
// 使用Lambda表达式
TestB.Test(2,(int a)->{
System.out.println("【方式2】实现A接口的test方法,num="+a);
});
【简化1】参数的数据类型可以省略
TestB.Test(2,(a)->{
System.out.println("【方式2】实现A接口的test方法,num="+a);
}
);
【简化2】只有一个参数的时候可以省略()
TestB.Test(2,a->{
System.out.println("【方式2】实现A接口的test方法,num="+a);
}
);
【简化3】执行代码块中只有一条语句/只有一个return语句的时候可以省略{}
TestB.Test(2,a->
System.out.println("【方式2】实现A接口的test方法,num="+a)
);
前文中的函数式接口都是我们自己定义的,其实Java官方给我提供了一些函数式接口,我们可以直接使用这些接口,而无需自己去定义函数式接口。
上文例子中的函数式接口对照上表来看的话,和消费性接口相对应,这里我们采用Consumer接口来试试看:
定义TestB_2类,采用 Consumer
函数式接口:
class TestB_2 {
public static void Test(Integer num, Consumer<Integer> c) {
c.accept(num);
}
}
使用Lambda表达式进行调用:
TestB_2.Test(3,(a)-> System.out.println("【方式3】实现A接口的test方法,num="+a));
运行效果:
其实还可以进一步简化:
BiConsumer<String,Integer> consumer = (a, b) -> System.out.println(a+""+b);
consumer.accept("【方式4】实现A接口的test方法,num=",4);
运行效果:
方法引用(Method References)是一种语法糖,它本质上就是 Lambda 表达式,我们知道Lambda表达式是函数式接口的实例,所以说方法引用也是函数式接口的实例,通过
类/对象::方法
进行引用。
例子:
@FunctionalInterface
interface A {
public void test(int a); // 只能包含一个抽象方法
}
class TestB {
public static void Test(String[] arrays, A a) {
for (int i = 0; i < arrays.length; i++) {
System.out.println(arrays[i]);
}
}
}
在前文中我们是通过Lambda表达式进行简化:
// 使用Lambda表达式
TestB.Test(2,(int a)->{
System.out.println("【方式2】实现A接口的test方法,num="+a);
});
当存在如下类时,C类下的静态方法testC(int a)和函数式接口方法 test(int a);的形参和返回值类型保持一致时,就可以通过方法引用进行简化,如下:
class C{
public static void testC(int a){
System.out.println("【方式5】实现A接口的test方法,num="+a);
}
}
使用方法引用进行简化:
// 使用引用
TestB.Test(5,C::testC);
运行效果:
对于方法引用的使用,通常可以分为以下 4 种情况:
分清楚实例方法,静态方法和构造方法
下面的equals(Object o)方法就是实例方法:
public final class Integer {
boolean equals(Object o) {
...
}
}
下面的parseInt(String s)方法就是静态方法:
public final class Integer {
public static int parseInt(String s) {
...
}
}
下面的方法就是构造方法:
public class A{
public A(){
}
}
情况1( 对象 :: 非静态方法
):
例子:
Consumer<String> consumer = System.out::println;
consumer.accept("【情况1】( 对象 :: 非静态方法 )方法引用");
对应的Lambda表达式如下:
Consumer<String> consumer2 = m -> System.out.println(m);
运行效果:
System.out为对象:
println为实例方法:
情况2( 类 :: 静态方法
):
例子:
Comparator<Integer> cm= Integer::compare;
System.out.println("【情况2】( 对象 :: 静态方法 )方法引用,100和99做比较:"+cm.compare(100, 99));
Lambda表达式如下:
Comparator<Integer> cm2 = (x, y) -> Integer.compare(x, y);
运行效果:
Integer为类:
compare为静态方法:
情况3( 类 :: 非静态方法
):
例子:
Comparator<String> cmStr = String::compareTo;
System.out.println("【情况3】( 对象 :: 非静态方法 )方法引用,字符串a和b做比较:"+cmStr.compare("a", "b"));
Lambda表达式如下:
Comparator<String> cmStr2 = (s1, s2) -> s1.compareTo(s2);
运行效果:
String为类:
compareTo为实例方法:
分清楚compareTo()方法和compare()方法的区别:
备注:compareTo()方法和compare()方法的区别更加详细解释,请参照这篇博文:java中compareTo和compare方法之比较
根据上面的理解我们来分析一下"a"和"b"是怎么进行比较的:
根据上文中二者的区别,可知使用 String::compareTo
表示采用String的排序操作,而 cmStr.compare("a", "b")
表示调用 "a"
的 compareTo("b")
进行比较。
如下图所示:
"a"
从哪里来的呢?,其实这个 "a"
就是this本身,而 this.value = 'a'
:
情况4( 类 :: new
):
例子:
class Student{
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
方法引用:
Function<String,Student> function = Student::new;
Student zs = function.apply("张三");
System.out.println("【情况4】( 类 :: new )方法引用,获取学生姓名:"+zs.getName());
Lambda表达式如下:
Function<String, Student> function2 = (name) -> new Student(name);
运行效果:
Student为类:
Function
这里的String对应Student的构造方法的参数类型
对上文的说法进行验证( 确实如此
):
Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中,Stream是数据渠道,用于操作数据源所生成的元素序列(数据源可以无限大),它可以实现对集合(Collection)的复杂操作,例如查找、替换、过滤和映射数据等操作。
这个 Stream
不同于java.io的 InputStream
和 OutputStream
,它代表的是任意Java对象的序列。两者对比如下:
java.io | java.util.stream | |
---|---|---|
存储 | 顺序读写的byte或char | 顺序输出的任意Java对象实例 |
用途 | 序列化至文件或网络 | 内存计算/业务逻辑 |
这个 Stream
和 List
也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。
换句话说,List的用途是操作一组已存在的Java对象,而Stream实现的是惰性计算,两者对比如下:
java.util.List | java.util.stream | |
---|---|---|
元素 | 已分配并存储在内存 | 可能未分配,实时计算 |
用途 | 操作一组已存在的Java对象 | 惰性计算 |
流式操作通常分为以下 3 个步骤:
通过Stream.Of()进行创建
第一种创建Stream的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream:
例子:
Stream<String> strStream = Stream.of("张三", "李四", "王五");
基于数组或Collection
第二种创建Stream的方法是基于一个数组或者Collection,这样该Stream输出的元素就是数组或者Collection持有的元素:
例子:
// 使用数组创建Stream
Stream<String> arrayStream = Arrays.stream(new String[]{"张三", "李四", "王五"});
// 使用集合创建Stream
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
Stream<String> listStream = list.stream();
基于Supplier
创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象,基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,
这种Stream保存的不是元素,而是算法,它可以用来表示无限序列
。
例子:
先创建Supplier对象
class NameSupplier implements Supplier<String> {
String name = "学生";
int count = 0;
@Override
public String get() {
count++;
return name+count;
}
}
基于Supplier对象创建Stream
Stream<String> supplierStream = Stream.generate(new NameSupplier());
其他方法
创建Stream的第三种方法是通过一些API提供的接口,直接获得Stream。
例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
} catch (IOException e) {
e.printStackTrace();
}
另外,正则表达式的Pattern对象有一个splitAsStream()方法,可以直接把一个长字符串分割成Stream序列而不是数组:
Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
IntStream、LongStream和DoubleStream
对于一些基本数据类型使用Stream( 在Stream中使用基本数据类型需要拆箱,装箱这样会影响Stream的执行效率
),可以使用Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率
例子:
IntStream intStream = IntStream.of(1, 2, 3, 4);
LongStream longStream = Arrays.stream(new long[]{14946161, 252525, 23135666});
DoubleStream doubleStream = Arrays.stream(new double[]{25366.33, 23256.22, 466.33});
Stream.forEach(Consumer super T> action)用于内部遍历Stream的数据
根据forEach(Consumer super T> action)的源码可知,是一个函数式接口(Java内置的消费性函数式接口),所以这里
我们可以使用Lambda进行简化:
Stream<String> strStream = Stream.of("张三", "李四", "王五");
System.out.println("---------------Stream.of()静态方法------------------");
strStream.forEach((m)-> System.out.println(m));
当然我们还可以再进一步简化,就是使用方法引用,System.out为对象,println为实例方法:
Stream<String> strStream = Stream.of("张三", "李四", "王五");
System.out.println("---------------Stream.of()静态方法------------------");
strStream.forEach(System.out::println);
Stream提供的常用操作有:
操作 | 方法 |
---|---|
转换操作: | map(),filter(),sorted(),distinct(); |
合并操作: | concat(),flatMap(); |
并行处理: | parallel(); |
聚合操作: | reduce(),collect(),count(),max(),min(),sum(),average(); |
其他操作: | allMatch(), anyMatch(), forEach()。 |
下面只是简单的介绍几种Stream常用的操作:
使用Stream.map()
Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。
所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如,对x计算它的平方,可以使用函数f(x) = x * x。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25:
上文例子的代码如下:
IntStream intStream = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
intStream.map(n -> n * n).forEach(System.out::println);
运行效果:
当Strem进行了中间操作后就不能再对Stream进行操作了:
Stream只能被消费一次,当其调用了终止操作后便说明其已被消费掉了。 如果还想重新使用,可考虑在原始数据中重新获得。
运行效果:
map的参数为函数式接口
根据 IntStream map(IntUnaryOperator mapper);的源码可知IntStream.map的参数为IntUnaryOperator :
根据IntUnaryOperator 源码可知IntUnaryOperator 为函数式接口,抽象方法为 int applyAsInt(int operand);
采用匿名内部类方式如下:
IntStream intStream = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
intStream.map(new IntUnaryOperator() {
@Override
public int applyAsInt(int operand) {
return operand *operand;
}
}).forEach(System.out::println);
通过Lambda表达式进行简化就是我们上文中的写法:
当然我们也可以使用方法引用( 由于使用方法引用需要创建静态方法比较麻烦,所以上文采用Lambda表达式即可
):
我们再来看看Stream.map的源码:
同样的Stream.map的参数也为函数式接口,并且为内置四大核心函数式接口中的Fuction接口
例子:
Stream<String> stream = Stream.of("A ","B ","C ","D ");
stream.map(String::toLowerCase).map(String::trim)
.forEach(System.out::println);
运行效果:
使用Stream.filter()
所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream。
例如,我们对1,2,3,4,5这个Stream调用filter(),传入的测试函数f(x) = x % 2 != 0用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5:
上文例子中的代码如下:
IntStream intStream = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
intStream.filter(x -> x % 2 != 0).forEach(System.out::println);
运行效果:
根据 IntStream filter(IntPredicate predicate); 源码可知,filter的参数为IntPredicate:
根据IntPredicate源码可知,IntPredicate为函数式接口,抽象方法为boolean test(int value);:
使用Stream.reduce()
map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。
例子:
IntStream intStream1 = IntStream.of(1, 2, 3, 4);
int reduce = intStream1.reduce(new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
}).getAsInt();
System.out.println(reduce);
运行效果:
根据IntStream.reduce(IntBinaryOperator op);源码可知,IntStream.reduce的参数为IntBinaryOperator:
根据IntBinaryOperator的源码可知IntBinaryOperator为函数式接口,抽象方法为 int applyAsInt(int left, int right);
所以我们可以使用Lambda表达式进行简化:
IntStream intStream1 = IntStream.of(1, 2, 3, 4);
int reduce = intStream1.reduce((left,right)->left+right).getAsInt();
上面例子中如果Stream中没有数据,会发生什么呢?
运行效果:
我们可以通过如下操作进行避免:
IntStream intStream1 = IntStream.of();
OptionalInt optionalInt = intStream1.reduce((left, right)->left+right);
int asInt = 0;
if (optionalInt.isPresent()) {
asInt = optionalInt.getAsInt();
}
System.out.println(asInt == 0? "Stream中没有任何数据":asInt);
运行效果:
聚合操作是真正需要从Stream请求数据的,对一个Stream做聚合计算后,结果就不是一个Stream,而是一个其他的Java对象
NumSupplier类:
class NumSupplier implements Supplier<Long> {
long num = 0;
@Override
public Long get() {
++num;
return num;
}
}
测试代码:
Stream<Long> s1 = Stream.generate(new NumSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
Long reduce = s4.reduce(0L, (left, right) -> left + right);
System.out.println(reduce);
运行效果:
我们对s4进行reduce()聚合计算,会不断请求s4输出它的每一个元素。因为s4的上游是s3,它又会向s3请求元素,导致s3向s2请求元素,s2向s1请求元素,最终,s1从Supplier实例中请求到真正的元素,并经过一系列转换,最终被reduce()聚合出结果。
使用Stream.limit(long maxSize)
Stream的limit方法可以对数据进行截断,使其元素不超过给定数量。
例子:
IntStream intStream = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
intStream.limit(5).forEach(System.out::println);
运行效果:
使用Stream.sorted()进行排序
关于排序中间操作,有下面几个常用方法:
例子:
List<Integer> integers = Arrays.asList(1, 69, 50, 700, 36, 92);
System.out.println("按自然排序【升序】");
integers.stream().sorted().forEach(System.out::println);
System.out.println("按降序排序【降序】");
integers.stream().sorted((num1,num2)-> - Integer.compare(num1,num2)).forEach(System.out::println);
运行效果:
使用Stream.distinct()去重
例子:
Arrays.asList("A", "B", "A", "C", "B", "D")
.stream()
.distinct().forEach(System.out::println);
运行效果:
使用Stream.skip()进行截取
例子:
Arrays.asList("A", "B", "C", "D", "E", "F")
.stream()
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.forEach(System.out::println); // [C, D, E]
运行效果:
使用Stream.concat()合并
例子:
Stream<String> s1 = Arrays.asList("A", "B", "C").stream();
Stream<String> s2 = Arrays.asList("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
s.forEach(System.out::println); // [A, B, C, D, E]
运行效果:
使用Stream.flatMap()
如果Stream的元素是集合:
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
而我们希望把上述Stream转换为Stream,就可以使用flatMap():
Stream<Integer> i = s.flatMap(list -> list.stream());
因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream:
使用Stream.parallel()并行
通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。
把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换:
Stream<String> s = Arrays.asList("A", "B", "C").stream();
s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.forEach(System.out::println);
运行效果:
经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。
输出为List
Stream<String> stream = Stream.of("Apple ", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null).map(String::trim).filter(k->!(k.isEmpty())).collect(Collectors.toList());
System.out.println(list);
运行效果:
输出为数组
Stream<String> stream = Stream.of("Apple ", "", null, "Pear", " ", "Orange");
String[] arrays = stream.filter(s -> s != null).map(String::trim).filter(k -> !(k.isEmpty())).toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
for (String str : arrays) {
System.out.println(str);
}
运行效果:
通过Lambda表达式进行简化:
Stream<String> stream = Stream.of("Apple ", "", null, "Pear", " ", "Orange");
String[] arrays = stream.filter(s -> s != null).map(String::trim).filter(k -> !(k.isEmpty())).toArray((value) ->new String[value]);
for (String str : arrays) {
System.out.println(str);
}
通过方法引用进行简化:
Stream<String> stream = Stream.of("Apple ", "", null, "Pear", " ", "Orange");
String[] arrays = stream.filter(s -> s != null).map(String::trim).filter(k -> !(k.isEmpty())).toArray(String[]::new);
for (String str : arrays) {
System.out.println(str);
}
输出为Map
toMap的二个参数分别为key和value,从字符串中映射的key和value。
Stream<String> stream = Stream.of("1:Apple ", "", null, "2:Pear", " ", "3:Orange");
Map<String,String> map = stream.filter(s -> s != null).map(String::trim).filter(k -> !(k.isEmpty())).collect(Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String s) {
return s.substring(0, s.indexOf(':'));
}
}, new Function<String, String>() {
@Override
public String apply(String s) {
return s.substring(s.indexOf(':') + 1);
}
}));
System.out.println(map);
运行效果:
通过Lambda表达式进行简化:
Stream<String> stream = Stream.of("1:Apple ", "", null, "2:Pear", " ", "3:Orange");
Map<String,String> map = stream.filter(s -> s != null).map(String::trim).filter(k -> !(k.isEmpty()))
.collect(
Collectors.toMap(
s->s.substring(0, s.indexOf(':')),
s->s.substring(s.indexOf(':') + 1 )
)
);
System.out.println(map);
分组输出
例子:
List<String> list = Arrays.asList("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(new Function<String, String>() {
@Override
public String apply(String s) {
return s.substring(0, 1);
}
}, Collectors.toList()));
System.out.println(groups);
运行效果:
使用Lambda简化:
分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List。
List<String> list = Arrays.asList("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s->s.substring(0, 1), Collectors.toList()));
System.out.println(groups);