3

Java8新特性—四大内置函数式接口 - 初出茅庐小菜鸡

 1 year ago
source link: https://www.cnblogs.com/wtao0730/p/16878131.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

💣 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报错提示其不是函数式接口。

img

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

四大函数式接口

1.Function接口

什么是Function接口?

image-20221110115452820

从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接口,看下面的源码:

a8569888-e829-4461-8c0e-7239637e0f35_Stream-function.png

通过一个例子来看看怎样使用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接口?

5a4efb87-740c-4d9c-85d7-def23ff56a4a_consumer.png

从源码可以看出,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中的应用

image-20221110174025534

同样的,我们以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接口,看下图源码:

a23b5092-1bdf-4e46-8d62-4b448c7aed81_supplier.png

由源码我们可以看出,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中的应用

3af23929-5890-4f28-848d-6aaaec88162b_Stream-supplier.png

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接口?

865c7007-2648-4c7e-850d-cee33a3324ea_Predicate.png

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中的应用

e5a1ee35-04e5-4273-81df-596db10d56e6_Stream-Predicate.png

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

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

执行结果:

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

四大函数式接口的比较

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK