函数式接口
一、概念
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
二、格式
只要确保接口中有且仅有一个抽象方法即可:
说明:函数式接口要求只有一个抽象方法。但是还可以有默认方法、静态方法,只要只有一个抽象方法就可以。
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
}
由于接口当中抽象方法的public abstract是可以省略的,所以定义一个函数式接口很简单:
public interface MyFunctionalInterface {
//抽象方法只有一个
void method();
//可以含有默认方法
public default void method_1() {
}
}
三、@FunctionalInterface注解
与@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void method();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
问题:为什么要使用该注解来验证一个接口是否是函数式接口呢?
答:因为如果一个接口是函数式接口,可以使用Lambda来简化代码的开发
四、自定义函数式接口(无参无返回值)
对于刚刚定义好的MyFunctionalInterface函数式接口,典型使用场景就是作为方法的参数:
public class Demo04 {
public static void main(String[] args) {
show(() -> System.out.println("lambda执行了。。。。"));
}
public static void show(MyFunctionalInterface mi) {
mi.method();// 调用自定义的函数式接口方法
}
}
四、练习:自定义函数式接口(有参有返回)
请定义一个函数式接口Sumable,内含抽象sum方法,可以将两个int数字相加返回int结果。使用该接口作为方法的参数,并进而通过Lambda来使用它。
@FunctionalInterface
public interface Sumable {
int sum(int a, int b);
}
public class Demo05 {
public static void main(String[] args) {
getSum(150,250,(a,b) -> a + b);
}
private static void getSum(int a,int b,Sumable sumable){
int sum = sumable.sum(a, b);
System.out.println(sum);
}
}
方法引用
一、方法引用代码示例
// 函数式接口
@FunctionalInterface
public interface Printable {
//打印字符串str
void print(String str);
}
public class Demo01 {
public static void main(String[] args) {
// printString(str -> System.out.println(str));
// 使用方法引用输出
printString(System.out::println);
}
public static void printString(Printable pt) {
//传递参数
pt.print("Hello World");
}
}
二、方法引用符
双冒号::为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。就是只要可以使用Lambda的地方都可以使用方法引用,没有使用Lambda表达式就不能使用方法引用。
下面这段代码将会调用println方法的不同重载形式,将函数式接口改为int类型的参数:
@FunctionalInterface
public interface PrintableInteger {
void print(int str);
}
由于上下文变了之后可以自动推导出唯一对应的匹配重载,所以方法引用没有任何变化:
public class Demo01 {
public static void main(String[] args) {
// printString(str -> System.out.println(str));
printString(System.out::println);
}
public static void printString(Printable pt) {
//传递参数
pt.print(1024);
}
}
这次方法引用将会自动匹配到println(int)的重载形式。
三、通过对象名引用成员方法
一个类中存在一个成员方法:
public class MethodRefObj {
public void printUpperCase(String s){
System.out.println(s.toUpperCase());
}
}
函数式接口:
@FunctionalInterface
public interface Printable {
//打印字符串str
void print(String str);
}
通过对象名引用成员方法
public class Demo01 {
public static void main(String[] args) {
MethodRefObj obj = new MethodRefObj();
// Lambda省略和推到形式
// show("helloworld",str -> obj.printUpperCase(str));
// 方法引用
show("helloworld",obj::printUpperCase);
}
//自定义方法
public static void show(String s, Printable pt) {
//调用Printable接口中的方法将s变为大写
pt.print(s);
}
}
说明:
Lambda表达式格式:(参数) -> {对象.方法名(参数)}
方法引用格式: 对象::方法名
下面两种写法是等效的:
Lambda表达式:s2 -> methodRefObject.printUpperCase(s2)
方法引用:methodRefObject::printUpperCase
(使用方法引用前提:使用对象引用成员方法,要求必须有该对象。)
四、通过类名称引用静态方法
函数式接口:
@FunctionalInterface
public interface Calcable {
int calc(int num);
}
public class Demo02 {
public static void main(String[] args) {
// 使用Lambda表达式
// method(-5,num -> Math.abs(num));
// 使用方法引用
method(-5,Math::abs);
}
private static void method(int num,Calcable calcable){
System.out.println(calcable.calc(num));
}
}
五、通过super引用成员方法
如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口:
@FunctionalInterface
public interface Greetable {
void greet();
}
父类:
public class Fu {
public void say() {
System.out.println("Fu 。。。Hello!");
}
}
子类:
public class Zi extends Fu{
@Override
public void say() {
// lambda写法
// method(() -> super.say());
// 方法引用写法
method(super::say);
}
private void method(Greetable lambda) {
lambda.greet();
System.out.println("zi.......");
}
}
测试:
public class Test01 {
public static void main(String[] args) {
Zi zi = new Zi();
zi.say();
}
}
六、通过this引用成员方法
this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用“this::成员方法”的格式来使用方法引用。首先是简单的函数式接口:
@FunctionalInterface
public interface Richable {
void buy();
}
定义一个类
public class Husband {
// 定义一个成员方法,供lambda引用
public void buyHouse(){
System.out.println("买房子");
}
// 函数式接口当参数传递
private void marry(Richable richable){
richable.buy();
}
// 方法引用
public void beHappy(){
// marry(() -> this.buyHouse());
marry(this::buyHouse);
}
}
测试
public class Test02 {
public static void main(String[] args) {
Husband husband = new Husband();
husband.beHappy();
}
}
七、类的构造方法引用
由于构造方法的名称与类名完全一样,并不固定。所以构造方法引用使用类名称::new的格式表示。
首先创建一个Person类:
1)定义一个私有成员变量name;
2)定义满参构造方法和get set 方法;
创建函数式接口:PersonBuilder
@FunctionalInterface
public interface PersonBuilder {
// 定义一个抽象方法用来创建Person对象
Person buildPerson(String name);
}
测试
public class Test03 {
public static void main(String[] args) {
// lambda省略版
// printName("王思聪",name -> new Person(name));
// 方法引用
printName("迪丽热巴",Person::new);
}
public static void printName(String name, PersonBuilder builder) {
Person p = builder.buildPerson(name);
System.out.println("姓名:" + p.getName());
}
}
八、数组的构造器引用
数组也是Object的子类对象,所以同样具有构造器,只是语法稍有不同。
格式:数据类型[]::new
需求:创建一个长度是10的int类型数组。
首先创建函数式接口:
@FunctionalInterface
public interface ArrayBuilder {
int[] buildArray(int length);// length表示数组的长度
}
测试
public class Test04 {
public static void main(String[] args) {
// lambda简化版
// int[] arr = initArray(10, length -> new int[length]);
// 方法引用
int[] arr = initArray(10,int[]::new);
System.out.println(arr.length);
}
public static int[] initArray(int length,ArrayBuilder builder) {
int[] arr = builder.buildArray(length);
return arr;
}
}
九、方法引用的总结
方法引用是lambda另外一种格式,只是这种格式有一些特点,在特定的情况下才可以使用。
在lambda表达式中,{}里面的内容如果是使用到了其他对象或者是其他类中的功能,这个时候可以采用方法引用简化格式
如果lambda表达式{}中,功能中使用到的方法是某个类的成员方法,这个时候可以改写为 该类对象::方法名
如果lambda表达式{}中,功能中使用到的方法是某个类的静态方法,这个时候可以改写为 类名::静态方法名
如果lambda表达式{}中,功能中使用到的方法是其父类的成员方法,(…)-> super.方法名(…)简写为 super::方法名
如果lambda表达式{}中,功能中使用到的方法是其类的成员方法,(…)-> this.方法名(…)简写为 this::方法名
函数式编程
在兼顾面向对象特性的基础上,Java语言通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。
一、Lambda的延迟执行
有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。
性能浪费示例:
public class Demo01 {
public static void main(String[] args) {
String msgA = "Hello ";
String msgB = "World ";
String msgC = "Java";
method(1, msgA + msgB + msgC);
}
private static void method(int level, String msg) {
if (level == 1) {
System.out.println(msg);
}
}
}
说明:无论级别是否满足要求,作为method方法的第二个参数,三个字符串一定会首先被拼接并传入method方法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。
Lambda的写法:首先定义一个函数式接口
@FunctionalInterface
public interface MessageBuilder {
String buildMessage();
}
测试
public class Test01 {
public static void main(String[] args) {
String strA = "Hello ";
String strB = "World ";
String strC = "Java";
method(1,() -> strA+strB+strC);
}
private static void method(int level, MessageBuilder builder) {
//判断
if (level == 1) {
//只有level等于1的时候才会执行这句话,才会执行lambda,然后才能进行拼接
System.out.println(builder.buildMessage());
}
}
}
说明:由于上述使用了Lambda表达式,Lambda表达式是在执行代码:builder.buildMessage() 时才会执行msgA + msgB + msgC。
二、使用Lambda作为参数
Java中的Lambda表达式可以被当作是匿名内部类的另一种体现。如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。
代码示例:函数式接口作为参数
首先定义一个函数式接口
@FunctionalInterface
public interface MySupplier {
Object get();
}
测试
public class Demo {
public static void main(String[] args) {
//使用lambda
// printObject(()->{return "Hello";});
//lambda省略格式
printObject(()->"Hello");
}
private static void printObject(MySupplier supplier) {
System.out.println(supplier.get());
}
}
三、使用Lambda作为返回值
如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。
当需要通过一个方法来获取一个java.util.Comparator接口类型的对象作为排序器时:
需求:定义一个字符串数组按照字符串的长度降序排序。
public class Demo02 {
public static void main(String[] args) {
//定义一个字符串数组
String[] arr={"abc","ab","abcds","adef"};
Arrays.sort(arr,getComparator());
System.out.println(Arrays.toString(arr));
}
//定义一个方法来提供自定义比较器Comparator的对象
public static Comparator getComparator() {
//使用Lambda表达式返回Comparator的对象
return (o1,o2)-> o2.length() - o1.length(); // 因为是降序,所以是o2在前面
}
}
Lambda的总结:可以理解为1.8之后Lambda的诞生的思想是write less do more(写的少,做的多)。间接可以将Lambda理解为是对匿名内部类的简化,Lambda可以更节省空间和代码量并完成相同的功能。
常用函数式接口
我们以前都是自己定义函数式接口,其实JDK本身提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。比如:java.util.function.Supplier
一、Supplier接口
java.util.function.Supplier接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
说明:
1、这里的泛型T表示当我们使用该接口时,传递什么数据类型就是什么数据类型。然后我们通过这个接口中的get方法就可以获取到指定泛型的类型的对象。
2、由于Supplier属于函数式接口,我们可以使用Lambda来实现。在Lambda中可以完成Supplier接口中的get()方法体中的代码。主要完成生产某个类的对象数据的功能。即我们需要使用Lambda返回指定泛型类的对象。
3、其实Supplier接口就是用来生产某个类的对象的。
需求:指定泛型是String类型,获取String类的对象。
public class Demo03 {
public static void main(String[] args) {
String s = getString(() -> "helloworld");
System.out.println("s: " + s);
}
public static String getString(Supplier lambda){ //Supplier 已经确定泛型T是String类型
return lambda.get();
}
}
说明:上述只是生产String类的对象,那么我们要生产任何类的对象,我们可以修改代码如下所示:
public class Demo03 {
public static void main(String[] args) {
String s = getString(() -> "helloworld");
System.out.println("s: " + s);
Integer i = getString(() -> 123);
System.out.println("i: " + i);
}
public static T getString(Supplier lambda){
return lambda.get();
}
}
为什么不直接定义:Integer i =123或者int i = 123;
原因:
使用Integer i =123;或者int i=123;这样定义,就把获取对象数据的方式写死了。很不灵活。而使用Supplier接口的方式,我们可以在getObject方法中,除了执行return lambda.get();我们还可以书写其他的代码,完成其他的功能。比如再生产某个对象时,有可能我们还会结合其他的类和对象完成更多的功能。只是我们这里书写的比较简单,简化了业务代码。
二、Consumer接口
java.util.function.Consumer
抽象方法:accept
Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据类型的对象。
由于accept方法有参数,没有返回值,所以该方法只是用来消费接收到的对象数据。
在使用该接口的时候会确定接口上的泛型,由于这个接口是函数式接口,可以使用Lambda方式来实现。在使用Lambda实现方法体的时候,传递什么数据就对什么数据进行操作,想怎么操作就怎么操作。
代码示例:需求:使用Consumer接口消费String类型的对象数据。
public class ConsumerDemo {
public static void main(String[] args) {
String s = "Hello World";
// 可以消费名称
consumerString(s, System.out::println);
// 可以消费长度
consumerString(s,len -> System.out.println(len.length()));
}
//定义一个方法使用接口Consumer来消费字符串对象
public static void consumerString(String s, Consumer lambda) {
// 使用lambda调用accept方法来消费字符串s
lambda.accept(s);
}
}
通过查看Consumer接口的源代码我们发现,该接口中还有一个默认方法andThen(表示然后的意思)方法。
默认方法:andThen
default Consumer
如果一个方法的参数和返回值全都是Consumer类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口中的default方法andThen。下面是JDK的源代码:
default Consumer andThen(Consumer super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
说明:
1、java.util.Objects的requireNonNull静态方法将会在参数after为null时主动抛出NullPointerException异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
2、return (T t) -> { accept(t); after.accept(t); };表示先执行第一个对象的accept(t)方法,然后在执行第二个对象after.accept(t);方法
要想实现组合,需要两个或多个Lambda表达式即可,而andThen的语义正是“一步接一步”操作。谁写在前面先执行谁。
代码示例:使用andThen()方法依次求出字符串"hello"的长度,然后将所有小写字母转换为大写字母。
public class AndThenDemo {
public static void main(String[] args) {
method("hello",s -> System.out.println(s.length()),
s -> System.out.println(s.toUpperCase()));
}
public static void method(String s, Consumer one, Consumer two){
one.andThen(two).accept(s);
}
}
练习:格式化打印信息
下面的字符串数组当中存有多条信息,请按照格式“姓名:XX。性别:XX。”的格式将信息打印出来。要求将打印姓名的动作作为第一个Consumer接口的Lambda实例,将打印性别的动作作为第二个Consumer接口的Lambda实例,将两个Consumer接口按照顺序“拼接”到一起。
public class DemoConsumer {
public static void main(String[] args) {
String[] arr = {"迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男"};
printInfo(s -> System.out.print("姓名:" + s.split(",")[0]),
s -> System.out.println("。性别:" + s.split(",")[1] + "。"),arr);
}
private static void printInfo(Consumer one, Consumer two, String[] array) {
for (String s : array) { // s:迪丽热巴,女 古力娜扎,女 马尔扎哈,男
one.andThen(two).accept(s); // 姓名:迪丽热巴。性别:女。
}
}
}
三、Predicate接口
有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate接口。
抽象方法:test
Predicate接口中包含一个抽象方法:boolean test(T t)。
说明:这个接口就是一个条件判断接口。判断传递的泛型类型的数据是否符合判断条件,而条件的具体代码由Lambda表达式来实现。
需求:判断指定的字符串 “ajahsgs” 中是否含有a字符。
public class PredicateDemo {
public static void main(String[] args) {
String name="ajahsgs";
boolean boo = checkString(name, s -> s.contains("a"));
System.out.println("boo = " + boo);
}
public static boolean checkString(String s, Predicate lambda) {
return lambda.test(s);
}
}
默认方法:and
既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法and。其JDK源码为:
default Predicate and(Predicate super T> other) {
Objects.requireNonNull(other);//判断接口对象是否为空
return (t) -> test(t) && other.test(t);
}
需求:如果要判断一个字符串既要包含字母“a”,并且长度大于5。
public class PredicateDemo02 {
public static void main(String[] args) {
String name = "ajahsgs";
boolean boo = checkString(name, s -> s.contains("a"), s -> s.length() > 5);
System.out.println("boo : " + boo);
}
public static boolean checkString(String s, Predicate one, Predicate two) {
return one.and(two).test(s);
}
}
默认方法:or
与and类似,默认方法or实现逻辑关系中的“或”。JDK源码为:
default Predicate or(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
如果希望实现逻辑“一个字符串包含字母a或者长度大于5”,那么代码只需要将“and”修改为“or”名称即可,其他都不变:
public class PredicateDemo02 {
public static void main(String[] args) {
String name = "aja";
boolean boo = checkString(name, s -> s.contains("a"), s -> s.length() > 5);
System.out.println("boo : " + boo);
}
public static boolean checkString(String s, Predicate one, Predicate two) {
return one.or(two).test(s);
}
}
默认方法:negate
“与”、“或”已经了解了,剩下的“非”(取反)也很简单。默认方法negate的JDK源代码为:
default Predicate negate() {
return (t) -> !test(t);
}
从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行“!”取反而已。一定要在test方法调用之前调用negate方法,就是先调用negate方法,在调用test方法。正如and和or方法一样:
negate()方法演示如下:
public class PredicateDemo02 {
public static void main(String[] args) {
String name = "bjb";
boolean boo = checkString(name, s -> s.contains("a"));
System.out.println("boo : " + boo);
}
public static boolean checkString(String s, Predicate one) {
return one.negate().test(s);
}
}
练习:集合信息筛选
/*
数组当中有多条“姓名+性别”的信息如下,请通过Predicate接口的拼装将符合要求的字符串筛选到集合ArrayList中,
需要同时满足两个条件:
1. 必须为女生;
2. 姓名为4个字。
*/
public class PredicateDemo03 {
public static void main(String[] args) {
String[] array = {"迪丽热巴,女","古力娜扎,女","马尔扎哈,男","赵丽颖,女"};
List list = getList(array, s -> s.split(",")[1].equals("女"), s -> s.split(",")[0].length() == 4);
System.out.println(list);
}
//自定义方法
public static List getList(String[] arr, Predicate one, Predicate two)
{
//创建一个空的集合
ArrayList list = new ArrayList<>();
//遍历数组
for (String s : arr) {
//s表示取出的每一个数据
boolean boo = one.and(two).test(s);//如果boo为true,说明满足两个条件
//判断,满足两个条件,就将s添加到list集合中
if (boo) {
//说明满足两个条件
list.add(s);
}
}
//将list返回给调用者
return list;
}
}
四、Function接口
java.util.function.Function
抽象方法:apply
Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。
说明:
Function接口其实是一个转换接口,具有转换功能,可以将传入的T类型数据转换为R类型数据。转换的操作由Lambda表达式来处理,就是根据类型T作为参数的数据获取R类型的数据。
示例:将String类型转换为Integer类型。“123”------>转换为123。
public class FunctionDemo {
public static void main(String[] args) {
String s = "123";
int i = method(s, Integer::parseInt);
System.out.println(i);
}
//定义方法通过接口Function将字符串转换为整数
public static int method(String s,Function lambda) {
return lambda.apply(s);
}
}
默认方法:andThen
Function接口中也有一个默认的andThen方法,用来进行组合操作。JDK源代码如:
default Function andThen(Function super R, ? extends V> after) {
Objects.requireNonNull(after);//判断是否为空
return (T t) -> after.apply(apply(t));//先执行apply(t),在执行after.apply()
}
该方法同样用于“先做什么,再做什么”的场景,和Consumer中的andThen差不多:
需求:将一个字符串"10"先转换位整数10,然后在将转换后的数字10乘以10,并输出结果。
public class Demo011 {
public static void main(String[] args) {
//调用方法
method("10", Integer::parseInt, i-> i = i*10);
}
//定义方法
public static void method(String s, Function one, Function two) {
//调用方法
int result = one.andThen(two).apply(s);
//输出结果
System.out.println("result = " + result);
}
}
请注意,Function的前置条件泛型和后置条件泛型可以相同。
练习:自定义函数模型拼接
请使用Function进行函数模型的拼接,按照顺序需要执行的多个函数操作为:
给定字符串:String str = “赵丽颖,20”;
将字符串截取数字年龄部分,得到字符串;Function
将上一步的字符串转换成为int类型的数字;Function
将上一步的int数字累加100,得到结果int数字。Function
public class DemoFunction {
public static void main(String[] args) {
String str = "赵丽颖,20";
int age = getAgeNum(str, s -> s.split(",")[1],
Integer::parseInt,
n -> n += 100);
System.out.println(age);
}
private static int getAgeNum(String str, Function one,
Function two,
Function three) {
//这里先执行one.apply() ,然后是two.apply() 最后是three.apply()
return one.andThen(two).andThen(three).apply(str);
}
}
五、总结:延迟方法与终结方法
在上述学习到的多个常用函数式接口当中,方法可以分成两种:
延迟方法:只是在拼接Lambda函数模型的方法,并不立即执行得到结果。
终结方法:根据拼好的Lambda函数模型,立即执行得到结果值的方法。
通常情况下,这些常用的函数式接口中唯一的抽象方法为终结方法,而默认方法为延迟方法。但这并不是绝对的。下面的表格中进行了方法分类的整理: