什么是函数式编程
在聊函数式编程以前我们用一个煮菜的例子回忆一下以往我们编程的方法;
public class Demo1 {
public static void main(String[] args) {
/*
* 命令式编程,从上向一步一步执行代码
*/
CookingTask ct = new CookingTask();
ct.wash();
ct.cut();
ct.deepFry();
ct.fried();
}
}
/**
* 创建一个煮菜的类,包含了洗菜、切菜、热油、炒菜
* @author huangyifan
*
*/
class CookingTask{
public void wash() {
System.out.println("清洗蔬菜");
}
public void cut() {
System.out.println("切片蔬菜");
}
public void deepFry() {
System.out.println("烧热使用油");
}
public void fried() {
System.out.println("炒菜");
}
}
上面的案例就是以前我们使用的命令式编程,这种命令式编程更加专注如何实现,因为我们把所有煮菜的步骤都单独封装成了方法。当我们需要加上一个步骤时就需要再封装一个方法。下面再来看一下函数式编程同样是煮菜这个例子
public class Demo2 {
public static void main(String[] args) {
CookingDemo ck = new CookingDemo();
/*
* 以下步骤完成了上面案例的步骤
*/
ck.doTask("蔬菜", (material)->System.out.println("清洗"+material));
ck.doTask("蔬菜", (material)->System.out.println("切片"+material));
ck.doTask("食用油", (material)->System.out.println("烧热"+material));
ck.doTask("", (material)->System.out.println("炒菜"+material));
/*
* 如果这时我想在食用油烧热之前打一个鸡蛋,按照以前命令式编程没需要在CookingTask类中再添加一个方法
* 利用函数式编程我只需要告诉计算机“伙计,我要打个鸡蛋”,具体实现就不需要我管了
*/
ck.doTask("鸡蛋", (material)->System.out.println("打"+material));
}
}
/**
* 创建一个煮饭类,但是不用实现具体细节,只需要一个方法,这个方法只是告诉计算机我要干什么,具体实现由计算机完成
* @author huangyifan
*
*/
class CookingDemo{
/**
*
* @param material 原材料
* @param consumer 一个函数式接口的实现,文章后面会讨论
*/
public void doTask(String material,Consumer<String> consumer) {
consumer.accept(material);
}
}
在函数式编程中我们不需要把每个步骤都封装成方法,只需要封装一条方法。对比上面两个案例我们会发现,函数式编程更加容易理解代码的含义。比如说在函数式编程中洗菜这句代码
ck.doTask("蔬菜", (material)->System.out.println("清洗"+material));
一看就知道了我是要打印“清洗蔬菜”,而在命令式编程中我只看到对象调用了wash()方法,但从Demo1折个类中我无法知道wash()方法的具体实现,我必须到CookingTask类中菜能看到wash()方法的具体实现。
在来看一个例子,有一个Person类,该类有三个成员变量name、gender、age,业务要求是根据name和gender来找到符合要求的person
public class Person {
private String name;
private String gender;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
定义一个处理业务的类PersonService,用以往的命令式编程我们需要在该类里声明两个方法findByName和findByGender
public class PersonService {
private static List<Person> list = new ArrayList<>();
//业务需求,通过名字找到符合的人
public List<Person> findByName(String name) {
List<Person> person = new ArrayList<>();
for(Person p:list) {
if(name.equals(p.getName())) {
person.add(p);
}
}
return person;
}
//业务需求,通过性别找到符合的人
public List<Person> findByGender(String gender) {
List<Person> person = new ArrayList<>();
for(Person p:list) {
if(gender.equals(p.getGender())) {
person.add(p);
}
}
return person;
}
}
我们可以明显的看出上面两个方法复用性很差,有很多重复的代码,除了这两行不同其余都一样
if(name.equals(p.getName()))
if(gender.equals(p.getGender()))
这里name和gender是两个字符串,我们可以用一个String str代替,代码就变成如下形式
if(str.equals(p.getName()))
if(str.equals(p.getGender()))
这下就是有getName和getGender不同了,我们考虑是否可以将这两个方法抽象为一个find方法,再将find方法里面的具体实现(比较是否有符合要求的person)封装到一个接口内。
//函数式接口的注释
@FunctionalInterface
public interface Criteria {
//对findByName和findBygender的具体实现进行抽象
boolean matches(Person p);
}
那么PersonService就可以改为以下这样
public class PersonService {
private static List<Person> list = new ArrayList<>();
public List<Person> find(Criteria criteria){
List<Person> person = new ArrayList<>();
for(Person p:list) {
if(criteria.matches(p)) {
person.add(p);
}
}
return person;
}
//利用匿名内部类创建Criteria并对matches重写
public List<Person> findByName(String name) {
return find(new Criteria() {
@Override
public boolean matches(Person p) {
return name.equals(p.getName());
}
});
}
//利用匿名内部类创建Criteria并对matches重写
public List<Person> findByGender(String gender) {
return find(new Criteria() {
@Override
public boolean matches(Person p) {
return gender.equals(p.getGender());
}
});
}
}
再将匿名内部类改为用lambda表达式
public class PersonService {
private static List<Person> list = new ArrayList<>();
public List<Person> find(Criteria criteria){
List<Person> person = new ArrayList<>();
for(Person p:list) {
if(criteria.matches(p)) {
person.add(p);
}
}
return person;
}
public List<Person> findByName(String name){
return find((p)->name.equals(p.getName()));
}
public List<Person> findByGneder(String gender){
return find((p)->gender.equals(p.getGender()));
}
}
如果现在需要添加一个业务,根据年龄找到符合的person只需要添加一个下面这样的方法。
public List<Person> findByAge(int age){
return find((p)->age == p.getAge());
}
那么到底什么是函数式编程:对于这个问题最简单的回答就是“他是一种使用函数进行编程的方法”,其核心就是:在思考问题时,使用不可变值和函数,利用函数将不可变值映射为另一个值。个人的理解就是将方法用函数式接口封装后当作参数传递。
lambda表达式简介
Lambda表达式是一种紧凑的,传递行为的方式,也是Java 8引入的。首先看一下下面的例子
public class Demo3 {
public static void main(String[] args) {
//利用匿名内部类创建线程,匿名内部类的作用就是将代码当作参数传递,但是匿名内部类语法很繁琐
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程0在执行");
}
});
t.start();
//使用lambda表达式简化匿名内部类,和匿名内部类不同,我们传入的参数是段代码,这段代码可以看成一个没有方法名的方法。
Thread t1 = new Thread(()->System.out.println("线程1在执行"));
t1.start();
}
}
public class Demo4 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("小明");
list.add("明");
list.add("小小明");
Collections.sort(list, new Comparator<String>() {
public int compare(String o1,String o2) {
return o1.length()-o2.length();
}
});
System.out.println(list);
//
Collections.sort(list, (o1,o2)->o2.length()-o1.length());
System.out.println(list);
}
}
lambda表达式是编译器认可的,其内容其实会被编译器还原为匿名内部类的形式。从上面例子说明,编译器会根据sort的方法签名来判断lambda表达式实现的是什么接口,并且会根据该接口中的抽象方法,判断参数的类型,所以有时候无需生命参数类型。
lambada表达式的语法为(参数列表)->{方法体}
若方法体只有一行则可以省略花括号,但是若是方法体有return要么同时省略花括号和return,要么不省略花括号并加上return。
Collections.sort(list, (o1,o2)->{return o2.length()-o1.length();});
lambda表达式的5种不同形式
Runnable r = ()->System.out.println("线程开始执行");//1
Collections.sort(list, (o1,o2)->{return o2.length()-o1.length();});//2
Collections.sort(list, (String o1,String o2)->o2.length()-o1.length());//3
Runnable r2 = ()->{//4
System.out.println("线程开始执行");
System.out.println("线程执行结束");
};
ActionListener al = event->System.out.println("button clicked");//5
}
1)参数为空,
2)两个参数,编译器会根据上下文判断出参数类型。至于返回值,编译器会根据void sort (list<T> list,Comparator<super T> c)这个方法得出lambda表达式的方法体实际是重写Comparator接口里的抽象方法(int compare(T o1,To2)),然后就可以得到返回值是int类型。
3)又是编译器无法得出参数类型,就需要加上参数类型。此处可以同时省略花括号和return。
4)当方法体不止一行代码时,不能省略花括号。
5)当参数只有一个时可以省略括号
函数接口
函数是只有一个抽象方法得接口,但是该接口可以有多个默认方法或者静态方法。但是也有特殊情况比如说Compator接口,他有两个抽象方法,Compare()和equales()其中equals()方法是和Object中的equals方法具有相同的方法签名,也只有这种情况可以存在多种抽象方法,也不必重写equals方法。
看这个方法int show(String str),调用方法时可以将字符串传入方法内,我们就说参数类型是String。那么Lambda表达式的类型是什么呢,就是编译器根据上下文得到的函数接口。
java 8的核心函数式接口
java 8新增了一个函数式接口包java.util.function包,该包内全是函数式接口,包括下面几种核心接口和基本类型函数接口还有几本类型转换的函数接口
接口 | 参数 | 返回类型 | 描述 |
---|---|---|---|
Predicate<T> | T | boolean | 用于判别一个对象。比如求一个人是否为男性 |
Consumer<T> | T | void | 用于接收一个对象进行处理但没有返回,比如接收一个人并打印他的名字 |
Function<T, R> | T | R | 转换一个对象为不同类型的对象 |
Supplier<T> | None | T | 提供一个对象 |
UnaryOperator<T> | T | T | 接收对象并返回同类型的对象 |
BinaryOperator<T> | (T, T) | T | 接收两个同类型的对象,并返回一个原类型对象 |
Consumer<T>消费型接口 ,用于接收对像并处理对象,没有返回值,抽象方法为void accept(T t)
public class ConsumerDemo {
/**
* 定义一个方法,用来表示花钱这件事,是一个没有具体实现的方法
* @param money
* @param con
*/
public void doSome(int money,Consumer<Integer> con) {
con.accept(money);
}
/**
* 定义具体实现的方法
*/
public void buyCar() {
doSome(50000,(m)->System.out.println("我花了"+m+"元买了一辆车"));
}
public static void main(String[] args) {
ConsumerDemo cd = new ConsumerDemo();
cd.buyCar();
}
}
Supplier<T>供给型接口,只提供一个对象,抽象方法为 T get()
/**
* 业务随机产生10个随机数放到集合中取
* @author huangyifan
*
*/
public class SupplierDemo {
/**
* 创建一个没有具体实现的方法
* @param num 表示需要多少数
* @param sup 函数式接口参数,传递的是一段代码
* @return
*/
public List<Integer> getNumList(int num,Supplier<Integer> sup){
List<Integer> list=new ArrayList<>();
for(int i=0;i<num;i++) {
/*
* 创建数的方法不确定,有可能是随机的,也有可能是直接赋值的,也有可能是从别的集合或数组中取的
* Supplier接口中的get方法只是提供一个返回值
*/
Integer k = sup.get();
list.add(k);
}
return list;
}
/**
* 随机创建10个数,这是具体实现的方法。lambda表达式是get()方法的具体实现。
*/
public void test() {
List<Integer> list=getNumList(10,() ->(int)(Math.random()*100));
System.out.println(list);
}
public static void main(String[] args) {
SupplierDemo sd = new SupplierDemo();
sd.test();
}
}
Function<T R>函数式接口,转换为不同类型的对象,参数为T类型,返回值为R类型,抽象方法为R apply(T t)
public class FunctionDemo {
/**
* 创建没有具体实现的方法,具体需要是什么现在还不知道,
* @param str 需要处理的字符串
* @param ft 函数式接口参数,传递的是一段代码
* @return
*/
public String strHandle(String str,Function<String,String> ft) {
return ft.apply(str);
}
//实现具体的方法,将字符串两边的空白去掉、取子字符串
public void test() {
String str1 = strHandle("\t\t\t\t西门庆 ",(str) ->str.trim());
System.out.println(str1);
String subStr = strHandle("鲁智深到拔垂杨柳",str -> str.substring(2, 5));
System.out.println(subStr);
}
public static void main(String[] args) {
FunctionDemo f = new FunctionDemo();
f.test();
}
}
类型可以随便转换
public class FunctionDemo {
/**
* 创建没有具体实现的方法,具体需要是什么现在还不知道,
* @param str 需要处理的字符串
* @param ft 函数式接口参数,传递的是一段代码
* @return
*/
public Object strHandle(String str,Function<Object,Object> ft) {
return ft.apply(str);
}
//实现具体的方法,将字符串两边的空白去掉、取子字符串、算取字符串的长度
public void test() {
String str1 = (String)strHandle("\t\t\t\t西门庆 ",(str) ->((String)str).trim());
System.out.println(str1);
String subStr =(String) strHandle("鲁智深到拔垂杨柳",str -> ((String)str).substring(2, 5));
System.out.println(subStr);
int len = (Integer)strHandle("只去生辰纲",str ->((String)str).length());
System.out.println(len);
}
public static void main(String[] args) {
FunctionDemo f = new FunctionDemo();
f.test();
}
}
Predicate<T>断言型接口,用于判断操作,抽象方法为boolean test(T t)
public class PredicateDemo {
/**
* 创建没有具体实现的方法,该方法将一个集合的元素经过筛选返回另一个集合
* @param list
* @param pi 函数式接口参数,传递的是一段代码
* @return
*/
public List<String> stringFilter(List<String> list,Predicate<String> pi){
List<String> list1 = new ArrayList<>();
//如何筛选没有实现
for(String s:list) {
if(pi.test(s)) {
list1.add(s);
}
}
return list1;
}
/**
* 创建具体实现的方法,筛选出结合中长度大于3的字符串
*/
public void test() {
List<String> list = Arrays.asList("dkafjsdf","dkfs","eiurowerv","da");
list = stringFilter(list,str->str.length()>3);
System.out.println(list);
}
public static void main(String[] args) {
PredicateDemo pd = new PredicateDemo();
pd.test();
}
}
方法引用
什么是方法引用:当Lambda体中的代码已经有方法实现了,我们就可以用方法引用。具体语法有以下三种:
1)对象::实例方法名
public class TextMethodRef {
public static void main(String[] args) {
//用Lambda表达式创建Consumer接口的匿名内部类实例,
Consumer<String> con = (x)->System.out.println(x);
con.accept("abcde");
//利用Lambda表达式的方法引用对象::实例方法。out()方法是System类的静态方法,返回值是一个PrintStream实例
PrintStream ps = System.out;
Consumer<String> con1 = ps::println;
con1.accept("abcde");
//简化上面的代码
Consumer<String> con2 = System.out::println;
con2.accept("abcde");
}
}
public class TestMethodRef {
public static void main(String[] args) {
Person p = new Person();
/*
* getNmae方法后面不能跟(),因为如果Lambda表达式需要使用方法引用这种形式
* getName方法中的参数类型必须和Supplier接口的抽象方法的参数类型一致.
* 既然编译器可以从上下文推断出该抽象方法的参数类型所以就不需要加上(),如果加上()
* 编译器会报错,
*/
Supplier<String> s =p::getName;
String name = s.get();
System.out.println(name);
}
}
2)类::静态方法名
public class TestMethodRef1 {
public static void main(String[] args) {
//业务需求比较两个int值的大小
Comparator<Integer> c = (x,y) -> Integer.compare(x,y);
int fig = c.compare(5, 3);
System.out.println(fig);
//用Lambda表达式的引用方法形式简化
Comparator<Integer> c1 = Integer::compare;
fig = c.compare(3, 5);
System.out.println(fig);
}
}
3)类::实例方法名(比较特殊)
public class TestMethodRef2 {
public static void main(String[] args) {
BiPredicate<String,String> bp = (x,y) -> x.equals(y);
boolean fig=bp.test("abc", "abcd");
System.out.println(fig);
//简化
BiPredicate<Integer,Integer> bp1 = Integer::equals;
fig = bp1.test(5, 3);
System.out.println(fig);
}
}
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
注意点:1)lambda体中调用的方法的参数类型和返回值类型要与函数式接口中的抽象方法的参数和返回值保持一致。
2)若Lambda表达式中的两个参数,第一个参数是实例方法的调用者,第二个参数是实例方法的参数,就可以使用类::实例方法名
构造器引用
用于创建对象
格式:Classname::new
注意:需要调用的构造器的参数列表要与抽象方法中的参数列表保持一致
public class TestConstr {
public static void main(String[] args) {
Supplier<Person> sl = () -> new Person();
Person p=sl.get();
//简写
Supplier<Person> sl1 = Person::new;
Person p1 = sl1.get();
//带一个参数的构造器
Function<String, Person> sl2 = Person::new;
Person p2=sl2.apply("zhangsan");
//带二个参数的构造器
BiFunction<String,String,Person> sl3 = Person::new;
Person p3 = sl3.apply("lisi", "nv");
}
}
public class Person {
private String name;
private String gender;
private int age;
public Person() {
}
public Person(String name) {
this.name = name;
}
public Person(String name,String gender) {
this.name = name;
this.gender = gender;
}
}
数组引用
格式:Type[] ::new
public class TestArray {
public static void main(String[] args) {
Function<Integer,String[]> f = (x)->new String[x];
String[] arr = f.apply(20);
System.out.println(arr.length);
//简化
Function<Integer,String[]> f1 = String[]::new;
String[] arr1= f1.apply(30);
System.out.println(arr1.length);
}
}