函数式接口与Lambda表达式

1076

概念

函数式接口在Java中是指:有且仅有一个抽象方法的接口。

函数式接口,即适用于函数式变成场景的接口。而Java中函数式变成体现就是Lambda,所以函数式接口就是适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能被顺利的进行推导。

语法糖是指使用更加方便,但是原理不变的代码语法。例如在遍历集合时使用的forEach语法,其实底层实现原理仍然是迭代器。从应用层面来讲,Java中的Lambda也可以被当作是匿名内部类的语法糖,但是二者在原理上是不相同的。

格式

只要确保接口中有且仅有一个抽象方法即可:

修饰符 interface 接口名称 {
	public abstract 返回值类型 方法名称(可选参数);
    // 其他非抽象方法内容
}

Java中接口的特性:

  • 接口是隐式抽象的,当声明一个接口的时候,不必使用abstract关键字。
  • 接口中每一个方法也是隐式抽象的,声明时同样不需要abstract关键字。
  • 接口中的方法都是公有的。

所以我们可以省略public abstract的修饰,所以定义一个函数式接口就变得很简单:

public interface MyFunctionalInterface {
    void hello();
}

@FunctionalInterface注解

Java8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上

@FunctionalInterface
public interface MyFunctionalInterface {
    abstract void hello();
}

添加这个注解之后,如果接口中存在多个方法或者没有方法,就会报错,它的作用就是检测接口是否是一个函数式接口。(接口方法都是隐式抽象所以为了简化直接称为方法,而不再加抽象修饰)

函数式接口的使用

public class Demo1 {
    // 定义一个方法,参数使用函数式接口
    public static void show (MyFunctionalInterface myInterface) {
        myInterface.hello();
    }

    public static void main(String[] args) {
        // 调用show方法,方法的参数是一个接口,所以可以传递接口的实现类对象
        show(new MyFunctionalInterface() {
            // 使用匿名内部类重写方法
            @Override
            public void hello() {
                System.out.println("我是函数式接口中的方法hello...");
            }
        });

        // 调用show方法,方法的参数一个函数时接口,所以可以使用Lambda表达式
        show(()->{
            System.out.println("使用Lambda表达式重写接口中的抽象方法。。。。");
        });
        
        // 简化Lambda表达式
        show(()-> System.out.println("这是简化后的Lambda表达式写法。。"));
    }
}

函数式编程

Lambda的延迟执行

有些场景的代码执行后,结果不一定会被使用,从而造成性能浪费。而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。

性能浪费的日志案例

注:日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,以便项目的监控和优化。
一种典型的场景就是对参数进行有条件使用,例如对日志消息进行拼接后,在满足条件的情况下进行打印输出:

public class Demo01Logger {
    // 定义一个根据日志级别显示日志信息的方法
    public static void showLog(int level, String message) {
        // 对日志的等级进行判断,如果级别是1那么输出日志信息
        if (level == 1) {
            System.out.println(message);
        }
    }

    public static void main(String[] args) {
        // 定义三个日志信息
        String msg1 = "hello";
        String msg2 = "world";
        String msg3 = "Java";

        //调用showLog方法,传递日志级别和日志信息
        showLog(1, msg1 + msg2 + msg3);
        showLog(2, msg1 + msg2 + msg3);
        showLog(3, msg1 + msg2 + msg3);

        // 以上代码存在性能浪费问题,我们是拼接字符串再调用showLog
        // 如果level不是1,那么就不会输出拼接后的字符串,白拼接了字符串
    }
}

体验Lambda的延迟

@FunctionalInterface
public interface MessageBuilder {
    // 定义一个拼接消息的抽象方法,返回拼接消息
    String joinMessage();
}
/**
 * 使用Lambda优化日志案例
 * Lambda的特点1:延迟加载
 * Lambda的使用前提,必须存在函数式接口
 */
public class Demo2Lambda {
    // 定义一个写日志的方法
    public static void showLog(int level, MessageBuilder messageBuilder) {
        // 对日志的等级进行判断,如果是1,调用joinMessage方法
        if (level == 1) {
            System.out.println(messageBuilder.joinMessage());
        }
    }

    public static void main(String[] args) {
        // 定义三个日志信息
        String msg1 = "hello";
        String msg2 = "world";
        String msg3 = "Java";

        //调用showLog方法,参数MessageBuilder是函数式接口
        showLog(1, ()->{
            //返回拼接的字符串
            return msg1 + msg2 + msg1;
        });

        // 使用Lambda表达式作为参数传递,仅仅是把参数传到showLog方法中
        // 只有满足条件,才会调用joinMessage方法,而只有调用时才会拼接字符串
        // 所以拼接字符串的代码得到了延迟加载的功能,不会造成性能浪费
    }
}

使用Lambda作为参数和返回值

如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名

内部类的替代品。如果方法的参数是一个函数式接口类型,那么就可

以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,

其实就是使用函数式接口作为方法参数。

例如java.lang.Runnable接口就是一个函数式接口,假设有一个

startThread方法使用该接口作为参数,那么就可以使用Lambda

进行传参。这种情况其实和Thread类的构造方法参数为Runnable

有本质区别。

常用函数式接口

Supplier接口

java.util.function.Supplier<T>接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供“一个符合泛型类型的对象数据。

/**
 * 常用函数式接口
 * java.util.function.Supplier<T>接口仅包含一个无参方法
 * T get()。用于获取一个泛型参数指定类型的对象数据
 * Supplier<T>接口被称之为生产型接口
 */
public class SupplierDemo {
    // 定义一个方法,方法参数传递Supplier<T>接口
    public static String getString(Supplier<String> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        // 调用getString
        String result = getString(()->{
            return "Hello World";
        });

        System.out.println(result);
    }
}

Consumer接口

java.util.function.consumer<T>接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。

accept

/**
 * Consumer是一个消费型接口泛型是什么类型
 * 就可以使用accept方法消费什么类型的数据
 */
public class ConsumerDemo {
    public static void hello(String name, Consumer<String> consumer) {
        consumer.accept(name);
    }

    public static void main(String[] args) {
        // 调用hello方法,传递需要消费的字符串和Consumer函数式接口
        hello("张三", (String name)->{
            // 消费name
            System.out.println("Hello "+name);
        });

    }
}

默认方法:andThen

如果一个方法的参数和返回值全都是consumer类型,那么就可以实现效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是consumer 接口中的default方法andThen。下面是JDK的源代码:

default Consumer<T> andThen(Consumer<? super T> after){
    Objects. requireNonNu11(after);
    return(T t)->{
        accept(t);
        after.accept(t);
   	};
}

备注:java.util.objectsrequireNonNull静态方法将会在参数为null时主动抛出NullPointerException异常。这省去了重复编写if语句和抛出空指针异常的麻烦。

要想实现组合,需要两个或多个Lambda表达式即可,而andThen的语义正是“一步接一步”操作。例如两个步骤组合的情况:

/**
 * Consumer接口的默认方法andThen
 * 作用:需要两个Consumer接口,可以把两个Consumer接口组合到一起再进行消费
 */
public class AndThenDemo {
    public static void hello(String name, Consumer<String> consumer1, Consumer<String> consumer2) {
        // consumer1.accept(name);
        // consumer2.accept(name);

        // 等同上面使用andThen方法连接两个Consumer再进行消费
        consumer1.andThen(consumer2).accept(name);
    }

    public static void main(String[] args) {
        hello("张三",(name)->{
            System.out.println(name+"消费了一只鸡");
        },(name)->{
            System.out.println(name+"消费100元");
        });
    }
}

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.predicate<T>接口。

抽象方法:test

Predicate 接口中包含一个抽象方法:boolean test(T t)。用于条件判断的场景:

/**
 * Predicate接口中包含一个抽象方法:
 * boolean test(T t):用来对指定数据进行判断的方法,符合返回true
 */
public class PredicateDemo {
    /**
     * 定义一个方法,参数传递一个String类型的字符串
     * 传递一个Predicate接口,使用Predicate的test
     * 方法对字符串进行判断,返回判断结果
     */
    public static boolean checkString(String s, Predicate<String> predicate) {
        return predicate.test(s);
    }

    public static void main(String[] args) {
        String s= "abcde";

        boolean flag = checkString(s, (String str)->{
            //对参数传递的字符串进行判断
            return str.length() > 5;
        });
        System.out.println(false);
    }
}

默认方法:and

既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个Predicate条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法and。其JDK源码为:

default Predicate<T> and(Predicate<? super T> other) {
    objects.requireNonNull(other);
    return(t)->test(t) && other. test(t);
}

如果要判断一个字符围既要包含大写H,又要包含大写W,那么:

/**
 * 逻辑表达式&&: 可以连接多个判断的条件
 */
public class PredicateAndDemo {
    public static void hello(Predicate<String> predicate1, Predicate<String> predicate2){
        boolean isValid = predicate1.and(predicate2).test("Hello World");
        System.out.println("字符串是否符合要求:"+isValid);
    }

    public static void main(String[] args) {
        hello(s->s.contains("H"), s->s.contains("W"));
    }
}

如果希望实现逻辑字符串包含大写H或者包含大写W,那么代码只需要将and修改为or名称即可,其他都不

public class PredicateOrDemo {
    public static void hello(Predicate<String> predicate1, Predicate<String> predicate2){
        // 使用or
        boolean isValid = predicate1.or(predicate2).test("Hello World");
        System.out.println("字符串是否符合要求:"+isValid);
    }

    public static void main(String[] args) {
        hello(s->s.contains("H"), s->s.contains("W"));
    }
}

默认方法:negate

表示逻辑非(取反),默认方法negateJDK源代码为:

default Predicate<T> negate(){
    return (t) -> !test(t);
}

从实现中很容易看出,它是执行了test方法之后,对结果boolean值进行"”取反而已。一定要在test方法调用之前调用negate方法,正如andor方法一样:

public class PredicateNegateDemo {
    public static void hello(Predicate<String> predicate) {
        // 使用negate
        boolean isValid = predicate.negate().test("Hello World");
        System.out.println("字符串是否符合要求:"+isValid);
    }

    public static void main(String[] args) {
        hello(s->s.contains("H"));
    }
}

Function接口

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。

抽象方法:apply

Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。
使用的场景例如:将String 类型转换为Integer 类型。

public class FunctionDemo {
    // String转Integer
    public static void convertType(String s, Function<String, Integer> fun) {
        Integer number = fun.apply(s);
        System.out.println(number);
    }

    public static void main(String[] args) {
        String s = "12313";
        convertType(s, (str)->{
            // String转Integer
            return Integer.parseInt(str);
        });
    }
}

默认方法:andThen

Function 接口中有一个默认的andThen方法,用来进行组合操作。JDK源代码如:

default <V> Function<T,V> andThen(Function<? super R,? extends V) after){
    objects.requireNonNu11(after);
    return(T t)->after.apply(apply(t));
}

该方法同样用于“先做什么,再做什么”的场景,和consumer中的andThen差不多:

public class FunctionAndThenDemo {
    // 将String转为Integer在转为Long型
    public static void convertType(String s, Function<String,Integer> fun1, Function<Integer, Long> fun2) {
        Long number = fun1.andThen(fun2).apply(s);
        System.out.println(number.getClass()+",值:" + number);
    }

    public static void main(String[] args) {
        String s = "12313";
        convertType(s, str->{
            return Integer.parseInt(str);
        }, num->{
            return num.longValue();
        });
    }
}