Java函数式编程
笔记来源:
对于Java的函数式编程,做一些摘抄以及总结,我是一个快乐的搬运工~~~
Lambda表达式
一、简介
Lambda表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性,它允许把函数作为一个方法的参数,使代码变的更加简洁紧凑,它允许使用更简洁的代码来创建一个只有一个抽象方法的接口(这种接口被称为函数式接口)的实例。
二、Lambda入门
下面用一个小例子来简单说明
以Comparator
为例,我们想要调用Arrays.sort()
时,可以传入一个Comparator
实例,以匿名类方式编写如下
String[] strings=newString[]{"Apple","Orange","Banana","Lemon"};Arrays.sort(strings,newComparator<String>(){publicintcompare(String o1,String o2){return o1.compareTo(o2);}});
从Java 8开始,我们可以用Lambda表达式替换单方法 接口(注意是单方法!!!)如下
Arrays.sort(strings,(s1, s2)->{return s1.compareTo(s2);});
其中,参数是(s1, s2)
,参数类型可以省略,因为编译器可以自动推断出String
类型。-> { ... }
表示方法体,所有代码写在内部即可,返回值的类型也是由编译器自动推断的,这里推断出的返回值是int
,因此,只要返回int
,编译器就不会报错。
从上面的例子可以看出:Lambda的代码块会代替实现抽象方法的方法体,Lambda就相当于一个匿名的方法。
三、Lambda表达式与函数式接口
Lambda表达式的类型,也被称为目标类型
(target type),Lambda的目标类型必须是函数式接口
(functional interface)。所谓函数式接口就是只包含一个抽象方法的接口(但是可以包含多个默认方法,类方法,但是只能声明一个抽象方法)。例如Runnable
、ActionListener
都是函数式接口。
Java8专门为函数式接口提供了
@FunctionalInterface
注解,它用于告诉编译器严格检查–检查该接口必须是函数式接口,否则编译器报错。
由于Lambda表达式的结果就是被当成对象,因此可以直接进行赋值如下
Runnable runnable=()->{for(int i=0; i<100; i++)System.out.println(i);};
Runnable
接口只包含一个无参数的方法,Lambda实现了该接口中唯一的
、无参数
的方法,因此就实现了一个Runnable
对象
Runnable
是Java本身提供的一个函数式接口
从以上分析可知Lambda表达式实现的是匿名方法,它有以下两个限制:
- 目标类型必须是明确的函数式接口
- 只能为函数式接口创建对象,Lambda只能实现一个方法他只能为只有一个抽象方法的接口(函数式接口)创建对象
四、方法引用与构造器引用
如果Lambda代码块中只有一条代码,程序就可以省略Lambda中的花括号,甚至还能进行方法引用
与构造器引用
1.引用类方法
先直接上代码:
publicclassLambda{@FunctionalInterfaceinterfaceConvert{Integerconvert(String form);}publicstaticvoidmain(String[] args){//下面的代码使用Lambda创建了Convert对象Convert convert= form->Integer.valueOf(form);}}
函数式包含一个convert
的抽象方法,将String转成Integer,由于表达式所实现的convert()
需要返回值,所以Lambda把这条代码作为返回值。上面的代码块只有一行调用类方法的代码,因此可以使用如下方法进行替换:
Convert convert=Integer::valueOf;
调用Integer
类方法中的valueOf()
方法来实现Convert接口的唯一的抽象方法。
2.引用特定对象的实例方法
看看下面的示例:
Convert convert= form->"fkit.org".indexOf(form);
convert()
方法执行体就是Lambda表达式的代码块部分,下面请看方法引用代替Lambda,引用特定对象的实例方法:
Convert convert="fkit.org"::indexOf;
3.引用某类对象的实例方法
下面直接先看看代码:
publicclassLambda{@FunctionalInterfaceinterfaceMyTest{Stringtest(String a,int b,int c);}publicstaticvoidmain(String[] args){MyTest mt=(a,b,c)-> a.substring(b, c);String str= mt.test("abcdefghijk",2,9);System.out.println(str);}}
上面的Lambda的代码块只有a.substring(b, c)
,使用了a
这个String对象的substring()
方法,当然下面的代码也能实现:
MyTest myTest=String::substring;
对于上面这个实例的方法引用,方法引用代替Lambda表达式,引用某类的某类对象的实例方法,函数式接口中被实现方法的第一个参数作为调用者,后面的参数全部传给该方法作为参数。
4.引用构造器
下面看构造器引用:
publicclassLambda{@FunctionalInterfaceinterfaceMyTest{JFramewin(String title);}publicstaticvoidmain(String[] args){MyTest myTest=(String a)->newJFrame(a);JFrame jFrame= myTest.win("My Window");System.out.println(jFrame);}}
上面代码调用myTest
对象的win()
方法时,myTest
对象就是Lambda表达式创建的,因此代码块的执行体就是new JFrame(a)
并将这条语句的返回值作为方法的返回值。同样的,我们也可以用下面的代码去替换:
MyTest myTest=JFrame::new;
上面这种构造器引用代替Lambda的做法,函数式接口中被实现方法的全部参数传给该构造器作为参数。
五、Lambda表达式与匿名内部类的联系与区别
从上面的种种实例可以看出Lambda表达式
是匿名内部类
的一种简化。
共同点
- 都可以直接访问
effectively finally
的局部变量,以及外部类的成员变量(包括实例变量与类变量) - 都可以调用从接口中继承的默认方法
下面是喜闻乐见的演示环节:
publicclassLambda{@FunctionalInterfaceinterfaceDisplayable{voiddisplay();//抽象方法defaultintadd(int a,int b){//默认方法return a+b;}}privateint age=12;privatestaticString name="Java";publicvoidtest(){String blog="XTY' Blog";Displayable displayable=()->{//访问`effectively finally`的局部变量System.out.println(blog);//访问外部类的实例变量和类变量System.out.println(age);//注意!当我在执行函数中(即public static void main)添加这个语句时,会报错。System.out.println(name);};
displayable.display();
displayable.add(1,2);}}
不同点
- 匿名内部类可以为任意接口创建实例,不管有多少个抽象方法,但是Lambda表达式只能为函数接口创建实例。
- 匿名内部类可以为抽象类甚至普通类创建实例,但是Lambda表达式,依然只能是函数式接口。
- 匿名内部类允许在实现的抽象方法的方法体内调用接口中定义的默认方法,而Lambda依然不可以。
六、使用Lambda表达式调用Arrays的类方法
Arrays类有些方法需要实现Comparator、XxxOperation、XxxFunction等接口的实例,这些都是函数式接口,下面演示一个:
publicstaticvoidmain(String[] args){String[] language=newString[]{"java","c++","go","python","c"};Arrays.sort(language,(s1, s2)-> s1.compareTo(s2));System.out.println(String.join(",", language));}
至此,Lambda算是简单入了一个门啦!!!
Stream
一、简介
Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据,英语较好的同学建议直接看官方文档。
那么Stream究竟是啥呢?
我们来康康英文单词stream
,解释为溪流 小溪 数据流
,其实Java中的Stream也是类似于这个的东西。
官方文档的第一句话A sequence of elements supporting sequential and parallel aggregate operations.
,一个支持顺序和并行聚合的序列,这样说起来这个Stream和List有有啥区别呢?
List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。也就是说,如果我没有用到Stream中的元素,它就不存在,相当于Stream只是一个声明的作用。
接下来看一个例子:
int sum= widgets.stream().filter(w-> w.getColor()== RED).mapToInt(w-> w.getWeight()).sum();
这也是官方文档上的一个例子,widgets
是一个Widget
对象的一个Collection
,我们创建了一个包含Widget
对象的Stream;filter
就是一个过滤器,他只留下了red widgets
;然后mapToInt
将这条Stream变成了一个int
的Stream,最后求和。
怎么样?通过这个小例子是不是对Stream有了一点点感觉呢?接下来我们就开始学习使用它吧!
二、Stream的创建方式
- 1.使用Stream.of()静态方法
Stream<String> stream1=Stream.of("A","B","C","D");
stream1.forEach(System.out::println);//函数式接口
上面的例子传入了可变参数即创建了一个能输出确定元素的Stream。
- 2.基于数组或Collection
Stream<String> stream2=Arrays.stream(newString[]{"A","B","C"});Stream<String> stream3=List.of("X","Y").stream();List<String> list=newArrayList<String>();
list.add("hello");
list.add("world");Stream<String> stream4= list.stream();
stream2
使用Arrays.stream()
方法把传入的固定数组变成了Stream;
stream3
是使用List.of()
生成一个不可变数组,然后通过这个数组创建的流,需要注意的是,List.of()
是在jdk1.8以后才出现的,所以在jdk1.9版本及以后才能运行;
stream4
是list
直接调用stream()
方法获得的,Stream对于Collection(List、Set、Queue等)
,直接调用stream()
方法就可以获得Stream,这种创建Stream的方法都是把一个现有的序列变为Stream,它的元素是固定的。
- 基于Supplier
publicclassStreamTest{publicstaticvoidmain(String[] args){Stream<Integer> stream5=Stream.generate(newNatualSupplier());// 无限序列必须先变成有限序列再打印
stream5.limit(10).forEach(System.out::println);}staticclassNatualSupplierimplementsSupplier<Integer>{privateint n=0;@OverridepublicIntegerget(){
n++;return n;}}}
stream5
通过Stream.generate()
方法创建的Stream,它需要传入一个Supplier对象,基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种Stream
保存的不是元素,而是算法,它可以用来表示无限序列。
上述代码我们用一个Supplier<Integer>
模拟了一个无限序列(当然受int
范围限制不是真的无限大)。如果用List
表示,即便在int
范围内,也会占用巨大的内存,而Stream
几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。
- 其他方式
通过一些API提供的接口创建Stream
例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:
try(Stream<String> lines=Files.lines(Paths.get("/path/to/file.txt"))){}
另外,正则表达式的Pattern对象有一个splitAsStream()
方法,可以直接把一个长字符串分割成Stream序列而不是数组:
Pattern p=Pattern.compile("\\s+");Stream<String> s= p.splitAsStream("The quick brown fox jumps over the lazy dog");
s.forEach(System.out::println);
三、Stream的几种方法
1.Stream.map()
Stream.map()
是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream;所谓map操作,就是把一种操作运算,把一个序列映射到另外一个序列的每一个元素上。
例如下面的一个例子,对x计算它的平方,可以使用函数f(x) = x * x
,我们把这个函数映射到一个序列1,2,3,4,5
上,就得到了另一个序列1,4,9,16
:
Stream<Integer> s1=Stream.of(1,2,3,4);Stream<Integer> s2= s1.map(n-> n*n);
如果我们点进去查看Stream的源码:<R> Stream<R> map(Function<? super T, ? extends R> mapper);
发现map()方法接收的对象是Function接口对象,而Function接口对象呢,它定义了一个apply()
方法,负责把一个T
类型转换成R
类型:R apply(T t);
,由此可见Stream.map()
的功能。
下面我们看看他的简单应用:
publicclassMain{publicstaticvoidmain(String[] args){List.of(" Apple "," pear "," ORANGE"," BaNaNa ").stream().map(String::trim)// 去空格.map(String::toLowerCase)// 变小写.forEach(System.out::println);// 打印}}
2.Stream.filter()
Stream.filter()
从字面也能理解,即过滤器。filter()
方法原码:Stream<T> filter(Predicate<? super T> predicate);
它定义了一个test()方法,再深入:boolean test(T t);
可以看到这个方法负责判断元素是否符合条件。
publicclass mapTest{publicstaticvoidmain(String[] args){Stream.generate(newLocalDateSupplier()).limit(30).filter(Idt->Idt.getDayOfWeek()==DayOfWeek.SUNDAY||Idt.getDayOfWeek()==DayOfWeek.SATURDAY).forEach(System.out::println);}staticclassLocalDateSupplierimplementsSupplier<LocalDate>{LocalDate start=LocalDate.of(2020,1,1);int n=-1;publicLocalDateget(){
n++;return start.plusDays(n);}}}
可以看到,filter()
除了常用于数值外,也可应用于任何Java对象,上面的例子就是从一组给定的LocalDate
中过滤掉工作日,以便得到休息日。
3.Stream.reduce()
map()
和filter()
都是Stream的转换方法,而Stream.reduce()
则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果,下面看一个例子:
publicclassMain{publicstaticvoidmain(String[] args){int sum=Stream.of(1,2,3,4,5,6,7,8,9).reduce(0,(acc, n)-> acc+ n);System.out.println(sum);}}
咱们依然是来先看看reduce()
的源码:T reduce(T identity, BinaryOperator<T> accumulator);
可以看到reduce()
方法传入的一个对象是BinaryOperator
接口,继续前进,发现BinaryOperator
接口继承了BiFunction<T,T,T>
:public interface BinaryOperator<T> extends BiFunction<T,T,T>
最后终于在BiFunction
中找到了R apply(T t, U u);
这么一个方法,apply()
方负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果。
聊了这么多,最终的结果就是我们在reduce()
方法中传入的Lambda表达式(acc, n) -> acc + n
实现了R apply(T t, U u);
,并将聚合的结果给了acc返回,而传入的第一个参数0的作用是初始化结果为0。
除了求和,还有求积:
publicclassMain{publicstaticvoidmain(String[] args){int s=Stream.of(1,2,3,4,5,6,7,8,9).reduce(1,(acc, n)-> acc* n);System.out.println(s);}}
除了可以对数值进行累积计算外,灵活运用reduce()
也可以对Java对象进行操作。下面的代码演示了如何将配置文件的每一行配置通过map()
和reduce()
操作聚合成一个Map<String, String>
:
publicclassMain{publicstaticvoidmain(String[] args){// 按行读取配置文件:List<String> props=List.of("profile=native","debug=true","logging=warn","interval=500");Map<String,String> map= props.stream()// 把k=v转换为Map[k]=v:.map(kv->{String[] ss= kv.split("\\=",2);returnMap.of(ss[0], ss[1]);})// 把所有单个Map对象聚合到一个Map集合:.reduce(newHashMap<String,String>(),(m, kv)->{
m.putAll(kv);return m;});// 打印结果:
map.forEach((k, v)->{System.out.println(k+" = "+ v);});}}
我们来看看上面的程序,理一下思路:
首先,我们先创建一个String
类型的List
即props
,props.stream()
将props
变成一条Stream,然后在.map()
中将Stream中的每一个String对象都进行了分割,两段字符串分别存在ss[0], ss[1]
中,并调用Map.of(ss[0], ss[1])
将每一个对象都变成了一个Map对象,最后在.reduce()
方法中,初始值为一个HashMap<String, String>()
对象,将Stream中的每