《java8 的java.util.function包中函数式接口》
一、为什么需要 lambda
public static void test1() { String name = ""; String name1 = "12345"; System.out.println(validInput(name, inputStr -> inputStr.isEmpty() ? "名字不能为空" : inputStr)); System.out.println(validInput(name1, inputStr -> inputStr.length() > 3 ? "名字过长" : inputStr)); } public static String validInput(String name, Function<String, String> function) { return function.apply(name); }
// 统计年龄在25-35岁的男女人数、比例 public void boysAndGirls(List<Person> persons) { Map<Integer, Integer> result = persons.parallelStream().filter(p -> p.getAge()>=25 && p.getAge()<=35). collect( Collectors.groupingBy(p->p.getSex(), Collectors.summingInt(p->1)) ); System.out.print("boysAndGirls result is " + result); System.out.println(", ratio (male : female) is " + (float)result.get(Person.MALE)/result.get(Person.FEMALE)); }
二、lambda 语法
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
语法
lambda 表达式的语法格式如下:由三部分组成: 参数列表,箭头( ->),表达式或者语句块,如下:
箭头操作符将 Lambda 表达式拆分成两部分:
左侧:Lambda 表达式的参数列表
右侧:Lambda 表达式中所需执行的功能, 即 Lambda 体
(参数) -> {表达式;}
(parameters) -> expression 或 (parameters) ->{ statements; }
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
// 1. 不需要参数,返回值为 5 () -> 5 // 2. 接收一个参数(数字类型),返回其2倍的值 x -> 2 * x // 3. 接受2个参数(数字),并返回他们的差值 (x, y) -> x – y // 4. 接收2个int型整数,返回他们的和 (int x, int y) -> x + y // 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) (String s) -> System.out.print(s)
- 参数类型省略–绝大多数情况,编译器都可以从上下文环境中推断出lambda表达式的参数类型。例如上面的第二条 x 和第三条 (x,y) 都没声明类型。
- 当lambda表达式的参数个数只有一个,可以省略小括号。 如上面 第二条的 x
- 当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号。如上面的返回都没有 return 这东西。
- lambda 表达式 可以访问外部变量(外部变量不可变)。
示例2:
public class Java8Tester { public static void main(String args[]){ Java8Tester tester = new Java8Tester(); // 类型声明 MathOperation addition = (int a, int b) -> a + b; // 不用类型声明 MathOperation subtraction = (a, b) -> a - b; // 大括号中的返回语句 MathOperation multiplication = (int a, int b) -> { return a * b; }; // 没有大括号及返回语句 MathOperation division = (int a, int b) -> a / b; System.out.println("10 + 5 = " + tester.operate(10, 5, addition)); System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction)); System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication)); System.out.println("10 / 5 = " + tester.operate(10, 5, division)); // 不用括号 GreetingService greetService1 = message -> System.out.println("Hello " + message); // 用括号 GreetingService greetService2 = (message) -> System.out.println("Hello " + message); greetService1.sayMessage("Runoob"); greetService2.sayMessage("Google"); } interface MathOperation { int operation(int a, int b); } interface GreetingService { void sayMessage(String message); } private int operate(int a, int b, MathOperation mathOperation){ return mathOperation.operation(a, b); } }
运行结果:
10 + 5 = 15
10 - 5 = 5
10 x 5 = 50
10 / 5 = 2
Hello Runoob
Hello Google
使用 Lambda 表达式需要注意以下两点:
- Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
- Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
三、变量作用域
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误 。可以类似内部类和外部变量的关系。
示例5-1:在 lambda 表达式中访问外层的类的成员变量,通过new一个实例,通过实例引用成员变量的方式可以。
public class Java8Tester3 { //Java8Tester3的成员变量 String salutation = "Hello! "; public static void main(String args[]) { Java8Tester3 jt = new Java8Tester3(); GreetingService greetService1 = message -> System.out.println(jt.salutation + message); greetService1.sayMessage("Runoob"); } interface GreetingService { void sayMessage(String message); } }
Hello! Runoob
示例5-2: lambda 表达式也可以引用标记了 final 的外层局部变量:
public class Java8Tester3 { //Java8Tester3的成员变量,加final final String salutation = "Hello! "; public static void main(String args[]) { Java8Tester3 jt = new Java8Tester3(); jt.test(); } public void test() { GreetingService greetService1 = message -> System.out.println(salutation + message); greetService1.sayMessage("Runoob"); } interface GreetingService { void sayMessage(String message); } }
示例5-3: 我们也可以直接在 lambda 表达式中访问外层的局部变量:
public class Java8Tester4 { public static void main(String args[]) { //局部变量 int num = 1; Converter<Integer, String> s = (param) -> System.out.println(param + num); s.convert(2); // 输出结果为 3 } public interface Converter<T1, T2> { void convert(int i); } }
运行结果:3
示例5-4: 尝试在lambda表达式里修改外部变量的值,编译失败会报错“Local variable flag defined in an enclosing scope must be final or effectively final”
”
匿名内部类和局部内部类只能引用外部的fianl变量,因为局部变量在初始化后,又对这个变量进行了赋值。赋值后会认为这个变量不是final了,所以报错。把变量变成fianl即可不报错。
但是这样的话我们就不能实现关于最大年龄在排序时做的统计,解决方法很简单,将上面的maxAge换成数组类型存储最大年龄即可。
lambda表达式也有类似问题,其可以访问给它传递的变量,访问自己内部定义的变量,同时也能访问它外部的变量。但lambda表达式访问外部变量有一个非常重要的限制:变量不可变(只是引用不可变,而不是真正的不可变)。
四、方法引用
- 方法引用通过方法的名字来指向一个方法。
- 方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- 方法引用使用一对冒号 ::
五、函数式接口
5.1、函数式接口介绍
有且只有一个抽象方法的接口,称之为函数式接口。当然接口中可以有其他方法(默认,静态,私有)
@FunctionInterface注解:可以检测接口是否是函数式接口
是:编译成功, 否:编译失败
5.2、函数式接口的使用
一般可以作为方法的参数和返回值类型
例子:
1.定义一个函数式接口MyFunctionInterface中定义一个没有返回值的方法public void method();
2.定义一个接口的实现类MyFunctionInterFaceImpl实现函数式接口
3.定义一个测试类Demo中有一个静态方法show参数是这个函数式接口,调用接口中的方法public static void show(MyFunctionInterface myface){myface.method();}
4.在main方法中调用show方法的3中方式
a.调用show方法,参数是接口,所以可以传接口的实现类
show(new MyFunctionInterfaceImpl());
b.方法参数是一个接口,所以可以传递接口的匿名内部类
show(new MyFunctionInterface(){
@override
public void method(){System.out.print(“使用匿名内部类重写接口中的抽象方法”);}
});
c.调用show方法,方法参数是一个函数式接口所以可以使用Lambda表达式
show(()->{System.out.print(“使用lambda重写接口中的抽象方法”)});
5.3、函数式编程
5.3.1、函数式编程优点
- 函数式编程:使用Lambda表达式和方法引用简化程序。
- Lambda表达式的延迟执行:有些场景的代码执行后,结果不一定会被使用,从而造成性能的浪费。而Lambda是延迟执行的,这正好可以作为解决方法,提升性能。
- 函数式接口作为方法的参数和返回值。
存在函数式接口就可以使用Lambda表达式。即可以使用Lambda进行传参。
使用Lambda优化日志案例:
1.Lambda的特点:延迟加载 lambda使用的前提:必须存在函数式接口(定义拼接字符串的接口) 2.使用Lambda表达式作为参数传递,仅仅是把参数传递showLogger方法中,只有满足条件(只有满足其条件,才会执行,否则不会执行。因此,不会存在性能浪费。),日志的等级是1级才会调用接口MessageBuilder中的方法builderMessage才会进行拼接字符串。
示例5.3:
定义一个函数式接口,即可以适用于lambda表达式的接口:
@FunctionalInterface public interface Message { public abstract String messageBuilder(); }
定义一个类,在类中定义一个有两个参数的方法,其中一个是接口的对象,如下:
public class Test01 { public static void showMsg(int level, Message msg){ if(level == 1){ System.out.println(msg.messageBuilder()); } } public static void main(String[] args) { String msg1 = "Hello "; String msg2 = "Wrold!!! "; showMsg(1,()->{ System.out.println("拼接字符串"); return msg1+msg2; }); } }
只有当level=1满足条件时,才会执行lambda表达式的内容,拼接字符串;否则就没有必要进行拼接。
5.3.2、常见的函数式接口(java.util.function包中)
Supplier接口
Java.util.function.Supplier<T> 接口中仅包含一个无参的方法:T get()。 用来获取一个泛型参数指定类型的接口数据。
由于是一个函数式接口,这就意味着对应的Lambda表达式对外提供一个符合泛型类型的对象数据。 Supplier<T>接口被称为生产型接口,指定接口的泛型是什么类型,那么接口中的get方法就会生产什么类型的数据。
Consumer接口
Java.util.function.Consumer<T>接口正好与Supplier接口相反,他不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。 Consumer中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。 Consumer接口是一个消费型接口,泛型执行什么类型,就可以使用accept方法消费什么类型的数据
.foreach(Consumer t)
Consumer中的默认方法andThen:组合消费
Predicate接口 (判断)
Java.util.function.Predicate: Predicate接口对某种数据类型进行判断,从而得到一个boolean值结果。 Predicate接口中的抽象方法:boolean Test(T t)用来对指定类型的数据进行判断方法 结果:符合条件:返回true 不符合条件:返回flase 默认方法:and or !
Function接口
Java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。 抽象方法:R apply(T t) 根据T的类型参数获取R类型的结果
.map(Function<Object, Object> f)
5.3.3、函数式接口与lambda结合使用示例
package com.dxz.jdk; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; public class lambdaDemo { public static void main(String[] args) throws InterruptedException { System.out.println("functionTest--------"); functionTest(); System.out.println("consumerTest--------"); consumerTest(); System.out.println("predicateTest--------"); predicateTest(); } // 第二个参数是Function接口 public static String validInput(String name, Function<String, String> function) { return function.apply(name); } /** * Function接口的使用示例 */ public static void functionTest() { String name = ""; String name1 = "12345"; System.out.println(validInput(name, inputStr -> inputStr.isEmpty() ? "名字不能为空" : inputStr)); System.out.println(validInput(name1, inputStr -> inputStr.length() > 3 ? "名字过长" : inputStr)); } // 第二个参数是Consumer接口 public static void validInput2(String name, Consumer<String> function) { function.accept(name); } /** * Consumer接口的使用示例 */ public static void consumerTest() { String name = ""; String name1 = "12345"; validInput2(name, inputStr -> System.out.println(inputStr.isEmpty() ? "名字不能为空" : "名字正常")); validInput2(name1, inputStr -> System.out.println(inputStr.isEmpty() ? "名字不能为空" : "名字正常")); } // 第二个参数是Predicate接口 public static boolean validInput3(String name, Predicate<String> function) { return function.test(name); } /** * Predicate接口使用示例 */ public static void predicateTest() { String name = ""; String name1 = "12"; String name2 = "12345"; System.out.println(validInput3(name, inputStr -> !inputStr.isEmpty() && inputStr.length() <= 3)); System.out.println(validInput3(name1, inputStr -> !inputStr.isEmpty() && inputStr.length() <= 3)); System.out.println(validInput3(name2, inputStr -> !inputStr.isEmpty() && inputStr.length() <= 3)); } // 给出一个String类型的数组,求其中所有不重复素数的和 public void distinctPrimarySum(String... numbers) { List<String> l = Arrays.asList(numbers); int sum = l.stream() .map(e -> new Integer(e)) .filter(e -> Primes.isPrime(e)) .distinct() .reduce(0,(x, y) -> x + y); // equivalent to .sum() System.out.println("distinctPrimarySum result is: " + sum); } }
运行结果:
functionTest-------- 名字不能为空 名字过长 consumerTest-------- 名字不能为空 名字正常 predicateTest-------- false true false
六、lambda 配合 集合的使用
// 给出一个String类型的数组,求其中所有不重复素数的和 public void distinctPrimarySum(String... numbers) { List<String> l = Arrays.asList(numbers); int sum = l.stream() .map(e -> new Integer(e)) .filter(e -> Primes.isPrime(e)) .distinct() .reduce(0,(x, y) -> x + y); // equivalent to .sum() System.out.println("distinctPrimarySum result is: " + sum); }
- 引入lambda:代码更简单,配合其他使用效率更高。
- lambda语法:基本组成由 参数列表,箭头,表达式组成。如果参数列表和表达式简单,还可以把括号什么的进一步省略。
- Lambda的使用:一般配合 Function,Consumer,Predicate或者自定义的函数式接口使用,使得代码更加方便;同时也配合集合使用,提升效率,提高阅读性。