Java8新特性—四大内置函数式接口

💣 Java8新特性——四大内置函数式接口


预备知识

背景

Lambda 的设计者们为了让现有的功能与 Lambda 表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。

什么是函数式接口?

函数式接口指的是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,这样的接口可以隐式转换为 Lambda 表达式。

但是在实践中,函数式接口非常脆弱,只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface,举个简单的函数式接口的定义:

@FunctionalInterface  public interface GreetingService {      void sayMessage(String message);  }  

Java7 只能通过匿名内部类进行编程,例如:

GreetingService greetService = new GreetingService() {        @Override      public void sayMessage(String message) {          System.out.println("Hello " + message);      }  };  greetService.sayMessage("world");  

Java8 可以采用 Lambda 表达方进行编程,例如:

GreetingService greetService = message -> System.out.println("Hello " + message);  greetService.sayMessage("world");  

目前 Java 库中的所有相关接口都已经带有这个注解了,实践上java.lang.Runnablejava.util.concurrent.Callable是函数式接口的最佳例子!

@FunctionalInterface注解

Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。

正确例子,没有报错:

/**  * @Description FunctionalInterface  * @Author vchicken  * @Date 2022/9/24 14:46  */ @FunctionalInterface public interface TestFunctionalInterface {      void sayMessage(String message);      } 

错误例子,接口中包含了两个抽象方法,违反了函数式接口的定义,Eclipse报错提示其不是函数式接口。

Java8新特性—四大内置函数式接口

提醒:加不加@FunctionalInterface对于接口是不是函数式接口没有影响,该注解知识提醒编译器去检查该接口是否仅包含一个抽象方法。

四大函数式接口

1.Function接口

什么是Function接口?

Java8新特性—四大内置函数式接口

从Function接口的源代码,我们可以看出,JDK1.8之后才加入这个接口。Functional接口类中只有一个抽象方法待实现,符合函数式接口(指的是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口),因此Function接口可以用Lambda表达式——这个方法就是apply。

从源码可以看出,入参和出参类型,用泛型动态指定。apply的具体逻辑就相当于是入参转化为出参的具体逻辑。也就相当于是y = f(x)这个里面的,映射法则f。具体逻辑需要我们用匿名内部类或者Lambda,写方法体来实现。因此这个接口又叫函数型接口

下面我们来用代码举栗如何使用Function接口:

public class FunctionTest {      public static void main(String[] args) {          // 如果入参为null,则回参为0,否则返回入参的值作为出参         Function<Integer, Integer> function1 = s -> s == null ? 0 : s;          // 将入参的值+1后作为回参返回         Function<Integer, Integer> function2 = s -> s + 1;          // 如果入参为null,则回参为"",否则返回入参的值作为出参         Function<String, String> function3 = s -> s == null ? "空的" : s;          System.out.println(function1.apply(null));         System.out.println(function1.apply(100));          System.out.println(function2.apply(10));          System.out.println(function3.apply(null));         System.out.println(function3.apply("hello world!"));                  // andThen是先执行前面的操作,然后执行andThen之后的操作         Function<Integer, Integer> first = x -> x * x;         Function<Integer, Integer> after = y -> y * 2;          System.out.println(first.apply(3));         System.out.println(after.apply(3));         int res = first.andThen(after).apply(4);         System.out.println(res);     } } 

执行结果

0 100 11 空的 hello world! 9 6 32 

从上面的栗子我们可以看出:

Java把这些映射规则,也就是y = f(x)中的【f】抽象成了这个Function接口的apply逻辑。然后x和y,自变量和因变量,也就是入参出参,Java使用了扩展性更强的泛型参数类型,而不是固定Object入参出参。因为固定Object的话还要涉及到类型转换,还有可能报ClassCast异常,很麻烦

再看一个栗子:

/**  * @author vchicken  * @version 1.0  * @description FunctionTest  * @date 2022/11/10 12:10:06  */ public class FunctionTest<T,R> {      public static void main(String[] args) {         FunctionTest<Integer, Integer> functionTest1 = new FunctionTest<>();         System.out.println(functionTest1.functionTest(null, s -> s == null ? 0 : s));         System.out.println(functionTest1.functionTest(100, s -> s == null ? 0 : s));          System.out.println(functionTest1.functionTest(10, s -> s + 1));          FunctionTest<String, String> functionTest2 = new FunctionTest<>();         System.out.println(functionTest2.functionTest(null, s -> s == null ? "空的" : s));         System.out.println(functionTest2.functionTest("hello world!", s -> s == null ? "空的" : s));     }      public R functionTest(T in,Function<T,R> function){         return function.apply(in);     } } 

执行结果:

0 100 11 空的 hello world! 

结合两个栗子我们可以看出来:

我们可以使用Function接口来将同一个方法的处理逻辑抽象出来,在调用方法的时候,将处理逻辑以Lambda表达式的形式传入,实现同一个方法可以处理不同的代码逻辑,而且使用泛型来表示方法的出入参,可以避免不必要的类型转换和异常发生。@vchicken

Function接口在JDK中的应用

在JDK1.8中的新属性Stream中,就使用到了Function接口,看下面的源码:

Java8新特性—四大内置函数式接口

通过一个例子来看看怎样使用map这个接口

public static void main(String[] args) {         Stream<Integer> stream = Stream.of(-1,0,1,2,3,4);         Stream<Integer> stream1 = stream.map(integer -> integer + 1);         stream1.forEach(System.out::println);     } 

执行结果:

0 1 2 3 4 5 

案例:将数组中的数,依次平方,等到一个一个新的数组

List<Integer> myList = new ArrayList<>();         myList.add(-2);         myList.add(0);         myList.add(1);         myList.add(3);         myList.add(5);         myList.add(7);         List<Integer> collect = myList.stream().map(integer -> integer*integer).collect(Collectors.toList());         collect.forEach(System.out::println); 

执行结果:

4 0 1 9 25 49 

2.Consumer接口

什么是Consumer接口?

Java8新特性—四大内置函数式接口

从源码可以看出,Consumer与Function类似,只有一个accept抽象方法待实现,只不过唯一的区别是,Consumer的accept使用void修饰,没有返回值,而Function有返回值。

Consumer接口又称为消费型接口,顾名思义,入参传入后,被accept方法消费掉了,什么都没得到。

举个栗子:

    public static void main(String[] args) {         Consumer<Object> consumer1 = new Consumer<Object>() {             @Override             public void accept(Object o) {                 System.out.println("这次消费了:" + o.toString());             }         };          consumer1.accept("100w元在双十一!这下穷死了!");          Consumer<String> consumer2 = s -> System.out.println("这次消费了:" + s);          consumer2.accept("120w元在双十二!又穷死了!");     } 

执行结果:

这次消费了:100w元在双十一!这下穷死了! 这次消费了:120w元在双十二!又穷死了! 

同样的,我们可以提取公共方法为:

/**  * @author vchicken  * @version 1.0  * @description ComsumerTest  * @date 2022/11/10 17:14:11  */ public class ConsumerTest<T> {      public static void main(String[] args) {          ConsumerTest<String> consumerTest = new ConsumerTest<>();         consumerTest.accept("100w元在双十一!这下穷死了!", s -> System.out.println("这次消费了:" + s));         consumerTest.accept("120w元在双十二!又穷死了!", s -> System.out.println("这次消费了:" + s));      }      public void accept(T in, Consumer<? super T> consumer) {         consumer.accept(in);     } }  

执行结果:

这次消费了:100w元在双十一!这下穷死了! 这次消费了:120w元在双十二!又穷死了! 

Consumer接口在JDK中的应用

Java8新特性—四大内置函数式接口

同样的,我们以Stream中的forEach为例,看下面的例子:

 Stream<Integer> stream = Stream.of(-1,0,1,2,3,4);  stream.forEach(s-> System.out.println(s)); 

执行结果:

-1 0 1 2 3 4 

同样的我们还可以使用方法引用来打印,效果一致

stream.forEach(System.out::println); 

对于方法引用不了解的亲,也可以阅读这篇文章Java8 新特性 - 方法引用 @vchicken

3.Suppiler接口

什么是Suppiler接口?

既然我们上面说到了Consumer为消费型接口,按照惯例,那肯定有生产型接口或者也可以成为供给型接口——Supplier接口,看下图源码:

Java8新特性—四大内置函数式接口

由源码我们可以看出,Supplier接口只有一个待实的get方法,属于无参有返回值的抽象方法。下面看一个例子:

 public static void main(String[] args) {          // 生成一个字符串         Supplier<String> supplier1 = () -> "abcde";          // 生成一个随机数         Supplier<Integer> supplier2 = () -> new Random().nextInt(10);          // 产生一个运行时异常         Supplier<RuntimeException> supplier3 = () -> new RuntimeException();          System.out.println(supplier1.get());         System.out.println(supplier2.get().intValue());         System.out.println(supplier3.get());     } 

执行结果:

abcde 2 java.lang.RuntimeException 

Supplier接口在JDK中的应用

Java8新特性—四大内置函数式接口

generate方法返回一个无限连续的无序流,其中每个元素由提供的供应商(Supplier)生成。generate方法用于生成常量流和随机元素流。看下面例子:

public static void main(String[] args) {          // 生成随机数         Stream.generate(() -> new Random().nextInt(10));         stream.forEach(System.out::println);          // 生成随机布尔流         Stream.generate(() -> new Random().nextBoolean())                 .forEach(System.out::println);          // 生成常量流         Stream.generate(() -> "Hello World!")                 .forEach(System.out::println);      } 

执行结果:

2 5 1 --- #略  true false true --- #略  Hello World! Hello World! Hello World! --- #略 

由于generate返回无限连续流,为了限制流中元素的数量,我们可以使用Stream.limit方法

 public static void main(String[] args) { 	Stream.generate(() -> new Random().nextInt(10)).limit(3) 	   .forEach(e -> System.out.println(e)); 	 	Stream.generate(() -> new Random().nextBoolean()).limit(3) 	   .forEach(e -> System.out.println(e)); 	 	Stream.generate(() -> "Hello World!").limit(3) 	   .forEach(e -> System.out.println(e));   } 

执行结果:

3 6 3 true false false Hello World! Hello World! Hello World! 

4.Predicate接口

什么是Predicate接口?

Java8新特性—四大内置函数式接口

Predicate接口又称为断言型接口,test()方法有参但是返回值类型是固定的boolean,看下面例子:

public static void main(String[] args) {         Predicate<String> predicate = (s) -> s.length() > 0;                  // 测试字符串的长度是否>0         System.out.println(predicate.test("hello"));          // 结果取反         System.out.println(predicate.negate().test("hello"));           System.out.println("=====or / and======");         System.out.println(predicate.test(""));         // 增加或判断,二者满足其一则为true         System.out.println(predicate.or(s -> s.equals("")).test(""));         // 增加与判断,二者都满足则为true         System.out.println(predicate.and(s -> s.equals("hello")).test(""));         System.out.println(predicate.and(s -> s.equals("hello")).test("hello"));          System.out.println("=====isEqual======");         // 判断是否相等         System.out.println(Predicate.isEqual("hello").test(""));         System.out.println(Predicate.isEqual("hello").test("hello"));          Predicate<Boolean> nonNull = Objects::nonNull;         Predicate<Boolean> isNull = Objects::isNull;          Predicate<String> isEmpty = String::isEmpty;         Predicate<String> isNotEmpty = isEmpty.negate();      } 

执行结果:

true false =====or / and====== false true false true =====isEqual====== false true 

Predicate接口在JDK中的应用

Java8新特性—四大内置函数式接口

Stream中的filter方法,用来过滤不满足条件的元素,使用Predicate传入过滤条件。看下面例子:

Stream<Integer> stream = Stream.of(-1, 0, 1, 2, 3, 4); stream.filter(s -> s > 0).forEach(System.out::println); 

执行结果:

1 2 3 4 

总结

关于JDK1.8为什么要新增这四大内置函数式接口,其实就是Java的开发者将常用于代码的一些普遍场景抽象出来成为接口,而我们可以根据实际业务需求,实现这些接口的具体逻辑。通过lambda表达式的方式,也可以使得代码更加简洁。JDK中的函数式接口还有很多,但基本都是在四大函数式接口的基础之上加以拓展,有兴趣的童鞋可以自行研究。

四大函数式接口的比较

函数式接口 对应程序逻辑的抽象 具体场景
Function 程序中映射逻辑的抽象 比如我们写得很多的函数:接收入参,返回出参,方法代码块就是一个映射的具体逻辑。
Predicate 程序中判断逻辑的抽象 比如各种if判断,对于一个参数进行各种具体逻辑的判定,最后返回一个if else能使用的布尔值
Consumer 程序中的消费型逻辑的抽象 就比如Collection体系的ForEach方法,将每一个元素取出,交给Consumer指定的消费逻辑进行消费
Suppiler 程序中的生产逻辑的抽象 就比如最常用的,new对象,这就是一个很经典的生产者逻辑,至于new什么,怎么new,这就是Suppiler中具体逻辑的写法了

参考资料

发表评论

相关文章