函数式编程与Lambda表达式
1 函数式编程的优势
1.1 函数式编程思想
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什 么事情”。编程中的函数,也有类似的概念,你调用我的时候,给我实参为形参赋值,然后通过运行方法体,给你返回一个结果。对于调用者来做,关注这个方法具备什么样的功能。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
面向对象的思想:
做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.
函数式编程思想:
只要能获取到结果,谁去做的不重要,重视的是结果,不重视过程
Java8引入了Lambda表达式之后,Java也开始支持函数式编程。
Lambda表达式不是Java最早使用的,很多语言就支持Lambda表达式,例如:C++,C#,Python,Scala等。如果有Python或者Javascript的语言基础,对理解Lambda表达式有很大帮助,可以这么说lambda表达式其实就是实现SAM接口的语法糖,使得Java也算是支持函数式编程的语言。Lambda写的好可以极大的减少代码冗余,同时可读性也好过冗长的匿名内部类。
备注:“语法糖”是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的for-each语法,其实
底层的实现原理仍然是迭代器,这便是“语法糖”。从应用层面来讲,Java中的Lambda可以被当做是匿名内部
类的“语法糖”,但是二者在原理上是不同的。
1,2 冗余的匿名内部类
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。代码如下:
public class Demo01 { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() {// 覆盖重写抽象方法 System.out.println("使用匿名内部类重写的run方法"); } }; new Thread(runnable).start(); // 启动线程 new Thread(new Runnable() { @Override public void run() { System.out.println("使用匿名内部类不创建对象重写的run方法"); } }).start(); } }
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
代码分析:
对于Runnable的匿名内部类用法,可以分析出几点内容:
Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
为了指定run的方法体,不得不需要Runnable接口的实现类;
为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
而实际上,似乎只有方法体才是关键所在。
1.3 编程思想转换
做什么,而不是谁来做,怎么做
我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。
传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
生活举例:
当我们需要从北京到上海时,可以选择高铁、汽车、骑行或是徒步。我们的真正目的是到达上海,而如何才能到达上海的形式并不重要,所以我们一直在探索有没有比高铁更好的方式——搭乘飞机。
而现在这种飞机(甚至是飞船)已经诞生:2014年3月Oracle所发布的Java 8(JDK 1.8)中,加入了Lambda表达式的重量级新特性,为我们打开了新世界的大门。
1.4 体验Lambda的更优写法
借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
public class Demo01 { public static void main(String[] args) { new Thread(() -> { System.out.println("使用Lambda表达式重写的run方法"); }).start(); new Thread(() -> System.out.println("使用简化的Lambda表达式重写的run方法")).start(); } }
这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!
2 函数式接口
lambda表达式其实就是实现SAM接口的语句,所谓SAM接口就是Single Abstract Method,即该接口中只有一个抽象方法需要实现,当然该接口可以包含其他非抽象方法。
其实只要满足“SAM”特征的接口都可以称为函数式接口,都可以使用Lambda表达式,但是如果要更明确一点,最好在声明接口时,加上@FunctionalInterface。一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。
之前的SAM接口中,标记了@FunctionalInterface的函数式接口的有:Runnable,Comparator,FileFilter。
Java8在java.util.function新增了很多函数式接口:主要分为四大类,消费型、供给型、判断型、功能型。基本可以满足我们的开发需求。当然你也可以定义自己的函数式接口。
2.1 自定义函数式接口
只要确保接口中有且仅有一个抽象方法即可:
修饰符 interface 接口名称 { public abstract 返回值类型 方法名称(可选参数信息); // 其他非抽象方法内容 }
接口当中抽象方法的 public abstract 是可以省略的
例如:声明一个计算器Calculator
接口,内含抽象方法calc
可以对两个int数字进行计算,并返回结果:
public interface Calculator { int calc(int a, int b); }
在测试类中,声明一个如下方法:
public static void invokeCalc(int a, int b, Calculator calculator) { int result = calculator.calc(a, b); System.out.println("结果是:" + result); }
下面进行测试:
public static void main(String[] args) { invokeCalc(1, 2, (int a,int b)-> {return a+b;}); invokeCalc(1, 2, (int a,int b)-> {return a-b;}); invokeCalc(1, 2, (int a,int b)-> {return a*b;}); invokeCalc(1, 2, (int a,int b)-> {return a/b;}); invokeCalc(1, 2, (int a,int b)-> {return a%b;}); invokeCalc(1, 2, (int a,int b)-> {return a>b?a:b;}); }
2.2 函数式接口的分类
除了我们可以自定义函数式接口之外,jdk也给我们内置了一些函数式接口,具体分类如下
2.2.1 消费型接口
消费型接口的抽象方法特点:有形参,但是返回值类型是void
2.2.2 供给型接口
这类接口的抽象方法特点:无参,但是有返回值
2.2.3 断言型接口
这里接口的抽象方法特点:有参,但是返回值类型是boolean结果。
2.2.4 功能型接口
这类接口的抽象方法特点:既有参数又有返回值
3 Lambda表达式语法
Lambda表达式是用来给【函数式接口】的变量或形参赋值用的。
其实本质上,Lambda表达式是用于实现【函数式接口】的“抽象方法”
Lambda表达式语法格式
(形参列表) -> {Lambda体} • 1
说明:
(形参列表)它就是你要赋值的函数式接口的抽象方法的(形参列表),照抄
{Lambda体}就是实现这个抽象方法的方法体
->称为Lambda操作符(减号和大于号中间不能有空格,而且必须是英文状态下半角输入方式)
优化:Lambda表达式可以精简
当{Lambda体}中只有一句语句时,可以省略{}和{;}
当{Lambda体}中只有一句语句时,并且这个语句还是一个return语句,那么return也可以省略,但是如果{;}没有省略的话,return是不能省略的
(形参列表)的类型可以省略
当(形参列表)的形参个数只有一个,那么可以把数据类型和()一起省略,但是形参名不能省略
当(形参列表)是空参时,()不能省略
示例代码:
ps:以下演示使用了Junit进行单元测试,不明白的可以先学习一下Junit
public class TestLambdaGrammer { @Test public void test1(){ //用Lambda表达式给Runnable接口的形参或变量赋值 /* * 确定两件事,才能写好lambda表达式 * (1)这个接口的抽象方法是否需要传入参数 * public void run() * (2)这个抽象方法的实现要干什么事 * 例如:我要打印“hello lambda" */ Runnable r = () -> {System.out.println("hello lambda");}; //简化,省略了{;} Runnable r = () -> System.out.println("hello lambda"); } @Test public void test2(){ String[] arr = {"hello","Hello","java","chai"}; //为arr数组排序,但是,想要不区分大小写 /* * public static <T> void sort(T[] a,Comparator<? super T> c) * 这里要用Lambda表达式为Comparator类型的形参赋值 * * 两件事: * (1)这个接口的抽象方法: int compare(T o1, T o2) * (2)这个抽象方法要做什么事? * 例如:这里要对String类型的元素,不区分大小写的比较大小 */ Arrays.sort(arr, (String o1, String o2) -> {return o1.compareToIgnoreCase(o2);}); //省略了{return ;} //Arrays.sort(arr, (String o1, String o2) -> o1.compareToIgnoreCase(o2)); //省略了两个String Arrays.sort(arr, (o1, o2) -> o1.compareToIgnoreCase(o2)); //打印arr for (String string : arr) { System.out.println(string); } } @Test public void test3(){ ArrayList<String> list = new ArrayList<>(); list.add("hello"); list.add("java"); list.add("world"); /* * JDK1.8给Collection系列的集合,准确的讲是在Iterable接口中,增加了一个默认方法 * default void forEach(Consumer<? super T> action) * 这个方法是用来遍历集合等的。代替原来的foreach循环的。 * * 这个方法的形参是Consumer接口类型,它是函数式接口中消费型接口的代表 * 现在调用这个方法,想要用Lambda表达式为Consumer接口类型形参赋值 * * 两件事: * (1)它的抽象方法: void accept(T t) * (2)抽象方法的实现要完成的事是什么 * 例如:这里要打印这个t */ // list.forEach((String t) -> {System.out.println(t);}); //省略{;} // list.forEach((String t) -> System.out.println(t)); //省略String // list.forEach((t) -> System.out.println(t)); //省略形参的() list.forEach(t -> System.out.println(t)); } }
4 Lambda表达式练习使用
4.1 消费型接口
4.1.1 自定义消费型接口测试
定义接口:
package Jdk8.Lambda; import java.util.function.Consumer; public class LambdaManager { public static void userConsumer(String str, Consumer<String> consumer) { consumer.accept(str); } }
接口测试:
//自定义消费性接口 @Test public void testConsumer() { // LambdaManager.userConsumer("你好", new Consumer<String>() {//使用匿名内部类实现接口 // @Override // public void accept(String s) { // System.out.println(s); // } // });//和下边效果相同 LambdaManager.userConsumer("你好", (str) -> System.out.println(str)); }
4.1.2 jdk中消费型接口测试
代码示例:Consumer接口
在JDK1.8中Collection集合接口的父接口Iterable接口中增加了一个默认方法:
public default void forEach(Consumer<? super T> action)遍历Collection集合的每个元素,执行“xxx消费型”操作。
在JDK1.8中Map集合接口中增加了一个默认方法:
public default void forEach(BiConsumer<? super K,? super V> action)遍历Map集合的每对映射关系,执行“xxx消费型”操作。
案例:
(1)创建一个Collection系列的集合,添加你知道的编程语言,调用forEach方法遍历查看
(2)创建一个Map系列的集合,添加一些(key,value)键值对,例如,添加编程语言排名和语言名称,调用forEach方法遍历查看
示例代码:
@Test//jdk中消费型接口测试 public void testJdkConsumer() { // 创建一个List集合 List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); // 遍历出集合的每一个元素 // for (Integer integer : list) { // System.out.println(integer); // } // list.forEach(new Consumer<Integer>() { // @Override // public void accept(Integer integer) { integer就是遍历出的每一个元素,可以直接打印 // } // });Jdk里边已经定义了foreach,直接调用即可 list.forEach(i -> System.out.println(i)); HashMap<Integer, String> map = new HashMap<>(); map.put(1, "Java"); map.put(2, "C"); map.put(3, "C++"); map.put(4, "JavaScript"); map.put(5, "c#"); map.put(6, "Python"); map.put(7, "PHP"); map.put(8, "Matlab"); map.put(9, "Go"); // map.forEach(new BiConsumer<Integer, String>() { // @Override // public void accept(Integer integer, String s) { // System.out.println("第" + integer + "个,值为" + s); // } // }); map.forEach((i, s) -> System.out.println("第" + i + "个,值为" + s)); }
4.2 供给型接口
4.2.1 自定义供给型接口测试
定义接口:
package Jdk8.Lambda1; import java.util.function.Supplier; public class LambdaManager { public static void userSupplier(Supplier<String> supplier) { String res = supplier.get(); System.out.println("调用供给型接口的值为" + res); } }
接口测试:
//供给型接口 @Test public void testSupplier() { // LambdaManager.userSupplier(new Supplier<String>() { // @Override // public String get() { // return "xiaogao"; // } // });//和下边相同 LambdaManager.userSupplier(() -> "xiaogao"); }
4.2.2 jdk中供给型接口测试
代码示例:Supplier接口
在JDK1.8中增加了StreamAPI,java.util.stream.Stream是一个数据流。这个类型有一个静态方法:
public static <T> Stream<T> generate(Supplier<T> s)可以创建Stream的对象。而又包含一个forEach方法可以遍历流中的元素:public void forEach(Consumer<? super T> action)。
案例:
现在请调用Stream的generate方法,来产生一个流对象,并调用Math.random()方法来产生数据,为Supplier函数式接口的形参赋值。最后调用forEach方法遍历流中的数据查看结果。
@Test public void testJdkSupplier(){ Stream.generate(() -> Math.random()).forEach(num -> System.out.println(num)); }
4.3 功能型接口
4.3.1 自定义功能型接口测试
定义接口:
package Jdk8.Lambda; import java.util.function.Function; public class LambdaManager { public static void userFunction(String str, Function<String, String> function) { String apply = function.apply(str); System.out.println("返回的结果是" + apply); } }
接口测试:
// 功能型接口 @Test public void testFunction() { // LambdaManager.userFunction("ming", new Function<String, String>() { // @Override // public String apply(String s) { // return s += "你好"; // } // }); LambdaManager.userFunction("uzi", (s -> s += "你好啊")); //返回的结果是uzi你好啊 }
4.3.2 jdk中功能型接口测试
代码示例:Funtion<T,R>接口
在JDK1.8时Map接口增加了很多方法,例如:
public default void replaceAll(BiFunction<? super K,? super V,? extends V> function)按照function指定的操作替换map中的value。
public default void forEach(BiConsumer<? super K,? super V> action)遍历Map集合的每对映射关系,执行“xxx消费型”操作。
@Test//jdk中功能型接口测试 public void testJdkFunction() { HashMap<Integer, String> map = new HashMap<>(); map.put(1, "Java"); map.put(2, "C"); map.put(3, "C++"); map.put(4, "JavaScript"); map.put(5, "c#"); map.put(6, "Python"); map.put(7, "PHP"); map.put(8, "Matlab"); map.put(9, "Go"); // 目标:将java学科的名字替换成JavaWeb /* for (Map.Entry<Integer, String> entry : map.entrySet()) { Integer key=entry.getKey(); String value=entry.getValue(); if (value.equals("Java")) { map.put(key,"JavaWeb"); } }//常规写法*/ /*map.replaceAll(new BiFunction<Integer, String, String>() { @Override public String apply(Integer k, String v) { // 判断v是否为java if (v.equals("Java")) { return "JavaWeb"; } return v; } });//匿名内部类写法*/ map.replaceAll((k, v) -> { if (v.equals("Java")) { return "JavaWeb"; } return v; });//Lambda表达式写法 map.forEach((i, s) -> System.out.println("第" + i + "个,值为" + s));//使用foreach打印 }
4.4 断言型接口
4.4.1 自定义断言型接口测试
定义接口:
package Jdk8.Lambda; import java.util.function.Predicate; public class LambdaManager { public static void userPredicate(String name, Predicate<String> predicate) { boolean flag = predicate.test(name); System.out.println(flag); } }
接口测试:
//断言型接口 @Test public void testPredicate() { // LambdaManager.userPredicate("uzi", new Predicate<String>() { // @Override // public boolean test(String s) { // return s.equals("ming"); // } // }); LambdaManager.userPredicate("ming", (s -> s.equals("ming"))); }
4.4.2 jdk中断言型接口测试
代码示例:Predicate接口
JDK1.8时,Collecton接口增加了一下方法,其中一个如下:
public default boolean removeIf(Predicate<? super E> filter) 用于删除集合中满足filter指定的条件判断的。
public default void forEach(Consumer<? super T> action)遍历Collection集合的每个元素,执行“xxx消费型”操作。
@Test public void testJdkPredicate() { /* 1)添加一些字符串到一个Collection集合中 (2)调用forEach遍历集合 (3)调用removeIf方法,删除其中字符串的长度<5的 (4)再次调用forEach遍历集合*/ ArrayList<String> list = new ArrayList<>(); list.add("Java"); list.add("C"); list.add("Python"); list.add("PHP"); list.add("JavaScript"); //调用forEach遍历集合 list.forEach(s -> System.out.println(s)); //调用removeIf方法,删除其中字符串的长度<5的 list.removeIf(s -> { if (s.length() < 5) { return true; } return false; }); System.out.println("--------------"); //再次调用forEach遍历集合 list.forEach(s -> System.out.println(s)); }
4.5 案例
(1)声明一个Employee员工类型,包含编号、姓名、性别,年龄,薪资。
(2)声明一个EmployeeSerice员工管理类,包含一个ArrayList集合的属性all,在EmployeeSerice的构造器中,创建一些员工对象,为all集合初始化。
(3)在EmployeeSerice员工管理类中,声明一个方法:ArrayList get(Predicate p),即将满足p指定的条件的员工,添加到一个新的ArrayList 集合中返回。
(4)在测试类中创建EmployeeSerice员工管理类的对象,并调用get方法,分别获取:
所有员工对象
所有年龄超过35的员工
所有薪资高于15000的女员工
所有编号是偶数的员工
名字是“张三”的员工
年龄超过25,薪资低于10000的男员工
示例代码:
Employee类:
package com.itheima.pojo; public class Employee{ private int id; private String name; private char gender; private int age; private double salary; public Employee(int id, String name, char gender, int age, double salary) { super(); this.id = id; this.name = name; this.gender = gender; this.age = age; this.salary = salary; } public char getGender() { return gender; } public void setGender(char gender) { this.gender = gender; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Employee() { super(); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getSalary() { return salary; } public void setSalary(double salary) { this.salary = salary; } @Override public String toString() { return "Employee [id=" + id + ", name=" + name + ", gender=" + gender + ", age=" + age + ", salary=" + salary + "]"; } }
员工管理类:
class EmployeeService{ private ArrayList<Employee> all; public EmployeeService(){ all = new ArrayList<Employee>(); all.add(new Employee(1, "张三", '男', 33, 8000)); all.add(new Employee(2, "翠花", '女', 23, 18000)); all.add(new Employee(3, "无能", '男', 46, 8000)); all.add(new Employee(4, "李四", '女', 23, 9000)); all.add(new Employee(5, "老王", '男', 23, 15000)); all.add(new Employee(6, "大嘴", '男', 23, 11000)); } public ArrayList<Employee> get(Predicate<Employee> p){ ArrayList<Employee> result = new ArrayList<Employee>(); for (Employee emp : all) { if(p.test(emp)){ result.add(emp); } } return result; } }
测试类:
public class TestLambda { public static void main(String[] args) { //所有年龄超过35的员工 EmployeeManager employeeManager = new EmployeeManager(); List<Employee> employeeList1 = employeeManager.find(e -> e.getAge() > 35); employeeList1.forEach(e -> System.out.println(e)); //所有薪资高于15000的女员工 List<Employee> employeeList2 = employeeManager.find(e -> e.getSalary() > 15000 && e.getGender() == '女'); employeeList2.forEach(e -> System.out.println(e)); //所有编号是偶数的员工 List<Employee> employeeList3 = employeeManager.find(e -> e.getId() % 2 == 0); employeeList2.forEach(e -> System.out.println(e)); //名字是“张三”的员工 List<Employee> employeeList4 = employeeManager.find(e -> e.getName() == "张三"); employeeList4.forEach(e -> System.out.println(e)); //年龄超过25,薪资低于10000的男员工 List<Employee> employeeList5 = employeeManager.find(e -> e.getAge() > 25 && e.getSalary() < 10000 && e.getGender() == '男'); employeeList5.forEach(e -> System.out.println(e)); } }
5 方法引用与构造器引用
Lambda表达式是可以简化函数式接口的变量与形参赋值的语法。而方法引用和构造器引用是为了简化Lambda表达式的。当Lambda表达式满足一些特殊的情况时,还可以再简化:
(1)Lambda体只有一句语句,并且是通过调用一个对象的/类现有的方法来完成的
例如:System.out对象,调用println()方法来完成Lambda体
Math类,调用random()静态方法来完成Lambda体
(2)并且Lambda表达式的形参正好是给该方法的实参
例如:t->System.out.println(t)
() -> Math.random() 都是无参
5.1 方法引用
方法引用的语法格式:
(1)实例对象名::实例方法
(2)类名::静态方法
(3)类名::实例方法
说明:
::称为方法引用操作符(两个:中间不能有空格,而且必须英文状态下半角输入)
Lambda表达式的形参列表,全部在Lambda体中使用上了,要么是作为调用方法的对象,要么是作为方法的实参。
在整个Lambda体中没有额外的数据。
@Test public void test1(){ // Runnable r = () -> System.out.println("hello lambda"); Runnable r = System.out::println;//打印空行 //不能简化方法引用,因为"hello lambda"这个无法省略 } @Test public void test2(){ String[] arr = {"Hello","java","chai"}; // Arrays.sort(arr, (s1,s2) -> s1.compareToIgnoreCase(s2)); //用方法引用简化 /* * Lambda表达式的形参,第一个(例如:s1),正好是调用方法的对象,剩下的形参(例如:s2)正好是给这个方法的实参 */ Arrays.sort(arr, String::compareToIgnoreCase); } @Test public void test3(){ // Stream<Double> stream = Stream.generate(() -> Math.random()); //用方法引用简化 Stream<Double> stream = Stream.generate(Math::random); } @Test public void test4(){ List<Integer> list = Arrays.asList(1,3,4,8,9); //list.forEach(t -> System.out.println(t)); //用方法再简化 list.forEach(System.out::println); }
5.2 构造器引用
(1)当Lambda表达式是创建一个对象,并且满足Lambda表达式形参,正好是给创建这个对象的构造器的实参列表。
(2) 当Lambda表达式是创建一个数组对象,并且满足Lambda表达式形参,正好是给创建这个数组对象的长度
构造器引用的语法格式:
类名::new
数组类型名::new
示例代码:
public class TestMethodReference { @Test public void test1() { Stream<Integer> stream = Stream.of(1,2,3); Stream<int[]> stream2 = stream.map(int[]::new); } @Test public void test2() { Stream<String> stream = Stream.of("1.0","2.3","4.4"); // Stream<BigDecimal> stream2 = stream.map(num -> new BigDecimal(num)); Stream<BigDecimal> stream2 = stream.map(BigDecimal::new); } @Test public void test3(){ // Supplier<String> s = () -> new String();//通过供给型接口,提供一个空字符串对象 //构造器引用 Supplier<String> s = String::new;//通过供给型接口,提供一个空字符串对象 } }