19—JAVA(进阶)—函数式接口

01函数式接口

1.1概念

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

1.2格式

  • 只要确保接口中有且仅有一个抽象方法即可:
修饰符 interface 接口名称 {
public abstract 返回值类型 方法名称(可选参数信息);
// 其他非抽象方法内容
}
  • 由于接口当中抽象方法的 public abstract 是可以省略的,所以定义一个函数式接口很简单:
public interface MyFunctionalInterface {
void myMethod();
}

1.3@FunctionalInterface注解

  • @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface 。该注解可用于一个接口的定义上:
@FunctionalInterface
public interface MyFunctionalInterface {
void myMethod();
}
  • 注意:一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样

1.4自定义函数式接口

  • 对于刚刚定义好的 MyFunctionalInterface 函数式接口,典型使用场景就是作为方法的参数
//定义函数式接口
@FunctionalInterface
interface MyFunctionalInterface {
    void myMethod();
}
//测试类
public class Test {
    // 使用自定义的函数式接口作为方法参数
    private static void doSomething(MyFunctionalInterface inter) {
        inter.myMethod(); // 调用自定义的函数式接口方法
    }
    public static void main(String[] args) {
        // 调用使用函数式接口的方法,并重写此抽象方法
        doSomething(()-> System.out.println("Lambda执行啦!"));
    }
}

02函数式编程

2.1Lambda的延迟执行

  • 有些时候的代码执行后,结果不一定会被使用,从而造成性能浪费。
  • 而Lambda表达式是延迟执行的,这正好可以作为解决方案,提升性能。
  • 例如,日志可以帮助我们快速的定位问题,记录程序运行过程中的情况,一种典型的场景就是对参数进行有条件使用,将日志消息进行拼接后,在满足条件的情况下进行打印输出
public class Test {
    //打印日志的方法
    public static void log(int level,String msg){
        //当等级为1时,信息被打印出来
        if(level==1){
            System.out.println(msg);
        }
    }

    public static void main(String[] args) {
        String msgA="Hello";
        String msgB="World";
        log(1,msgA+msgB);
        //我们故意把等级设置为2,看看信息拼接会不会执行
        log(2,msgA+msgB);
    }
}
  • 分析:我们在IDEA中使用debug的方式启动,运行后发现,当调用log方法时,在进入方法体之前,log方法的第二个参数,两个字符串已经被拼接好了,然后在进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费

2.1.1体验Lambda的延迟执行

  • 注意:使用Lambda必然需要一个函数式接口:
//定义一个函数式接口
@FunctionalInterface
interface Message{
    //定义一个抽象方法(public abstract 可以省略)
   public abstract String printMsg();
}

//主函数
public class Test {
    //打印日志的方法,第二个参数为抽象接口类型的
    //此处使用到了多态和匿名内部类,使用抽象接口类型的变量,操作匿名内部类中的方法
    public static void log(int level,Message msg){
        //当等级为1时,信息被打印出来
        if(level==1){
            System.out.println(msg.printMsg());
        }
    }

    public static void main(String[] args) {
        String msgA="Hello";
        String msgB="World";
        //重写抽方法
        log(1,()->{
            System.out.println("当等级为1时执行了吗?");
            return msgA+msgB;});
        //我们故意把等级设置为2,看看信息拼接会不会执行
        log(2,()->{
            System.out.println("当等级为2时执行了吗?");
            return msgA+msgB;});
    }
}
  • 分析:我们同样使用IDEA的debug功能启动,运行后发现,当调用log方法时,在第二参数拼接信息之前,先进行了等级判断,当等级不满足的时候,后面的拼接动作就不会继续执行

2.2 使用Lambda作为参数和返回值

2.2.1作为参数

  • 如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。如果方法的参数是一个函数式接口类型,那么就可以使用Lambda表达式进行替代。使用Lambda表达式作为方法参数,其实就是使用函数式接口作为方法参数。
  • 例如java.lang.Runnable接口就是一个函数式接口,假设有一个 startThread 方法使用该接口作为参数,那么就可以使用Lambda进行传参。这种情况其实和 Thread 类的构造方法参数为 Runnable 没有本质区别。
//此处我们使用的是JDK自带的Runnable接口
public class Test {
    //定义一个启动线程的方法
   public static void startThread(Runnable runnable){
       new Thread(runnable).start();
   }

   //测试方法
   public static void main(String[] args) {
       //调用启动线程的方法
       startThread(()-> System.out.println(Thread.currentThread().getName()+":线程启动了"));

       //对比:使用匿名内部类启动线程
       new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println(Thread.currentThread().getName()+":线程启动啦");
           }
       }).start();
   }
}

2.2.2作为返回值

  • 类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一个方法来获取一个 java.util.Comparator 接口类型的对象作为排序器时,就可以调该方法获取
public class Test {
    private static Comparator<String> newComparator() {
        return (a,b)-> b.length()-a.length();
    }
    public static void main(String[] args) {
        String[] array = { "abc", "ab", "abcd" };
        System.out.println(Arrays.toString(array));
        //使用sort方法对数组进行排序,排序方法由newComparator方法指定
        Arrays.sort(array, newComparator());
        System.out.println(Arrays.toString(array));
    }
}

03常用函数式接口

  • 在JDK-9文档中,java.util.function包下,存放的就是JDK提供的函数式接口,此处介绍几个常用的函数式接口,更多的可以查看帮助文档

3.1Supplier接口

  • java.util.function.Supplier 接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。
  • 由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
  • 例如:求数组元素最大值
public class Test {
    //定义一个方法,使用Supplier接口作为参数
    //指定泛型类型
  public static int getMax(Supplier<Integer> sup){
      return  sup.get();
  }

    public static void main(String[] args) {
        int[] arr={1,2,10,4,5,6};
        //调用maxNum方法
       int maxNum= getMax(()->{
           int max=arr[0];
           for(int i=1;i<arr.length;i++){
               if(max<arr[i]){
                   max=arr[i];
               }
           }
           return max;
        });
        System.out.println(maxNum);
    }
}

分析:代码执行流程

  • 1) 由于lambda作为方法参数的延时性,当调用getMax方法后,执行return中的语句,此时return中的语句并未真正执行
  • 2)程序会跳转到调用getMax方法的位置,调用lambda表达式,对抽象方法进行重写
  • 3)之后程序会跳转到getMax方法中的return语句,这是才真正执行此语句,然后将返回值返回并打印出来

3.2Consumer接口

  • java.util.function.Consumer<T>接口则正好与Supplier接口相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型决定。
  • Consumer 接口中包含抽象方法 void accept(T t) ,意为消费一个指定泛型的数据
public class Test {
    //创建一个消费字符串的方法
    private static void consumeString(Consumer<String> function) {
        //使用方法接收要消费的字符串
        function.accept("HelloWorld");
    }
    public static void main(String[] args) {
        //s为形参
        consumeString((s)-> System.out.println(s));
    }
}

3.3Predicate接口

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

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

  • 判断字符串长度是否大于5,大于就返回true

public class Test {
    //定义一个判断方法
    public static boolean checkString(String str, Predicate<String> pre){
        return pre.test(str);
    }

    public static void main(String[] args) {
        //定义一个字符串
        String str="AISMALL";
        //调用checkString方法对字符串进行检验
        //s为形参,str为实参
        boolean result=checkString(str,(s)->{
            return s.length()>5;
        });
        System.out.println(result);
    }
}

分析:代码执行流程

  • 1)当程序调用checkString方法时,由于Lambda表达是的延时性,此时只有str参数被传入到方法中
  • 2)然后程序就会执行到return语句,但是并不会真正的被执行,
  • 3)程序会跳转到到延时执行的Lambda表达式处,执行Lambda表达式,对抽象方法(test)进行重写,
  • 4)最后程序才会真正执行checkString方法的return语句,然后返回布尔值

对比使用匿名内部类的方式:

public class Test {
    //定义一个判断方法
    public static boolean checkString(String str, Predicate<String> pre){
        return pre.test(str);
    }

    public static void main(String[] args) {
        //定义一个字符串
        String str="AISMALL";
        //调用checkString方法对字符串进行检验
        boolean result=checkString(str, new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.length()>5;
            }
        });
        System.out.println(result);
    }
}

3.3.1默认方法:and

  • 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用default方法 and 。其JDK源码为:
default Predicate<T> and(Predicate<? super T> other) {
	Objects.requireNonNull(other);
	return (t)> test(t) && other.test(t);
}
  • and方法使用:判断字符串中是否包含W和H
public class Test {
    //定义一个判断方法
    public static boolean method(Predicate<String> pre1,Predicate<String> pre2){
        return pre1.and(pre2).test("HelloWorld");
    }

    public static void main(String[] args) {
        //判断字符串中是否既包含W有包含H
        boolean result=method((str1)->str1.contains("H"),(str2)->str2.contains("W"));
        System.out.println(result);
    }
}

3.3.2默认方法:or

  • 与 and 的“与”类似,默认方法 or 实现逻辑关系中的“或”。JDK源码为:
default Predicate<T> or(Predicate<? super T> other) {
	Objects.requireNonNull(other);
	return (t)> test(t) || other.test(t);
}
  • or方法使用:判断字符串中是否包含W或H
public class Test {
    //定义一个判断方法
    public static boolean method(Predicate<String> pre1,Predicate<String> pre2){
        return pre1.or(pre2).test("helloworld");
    }

    public static void main(String[] args) {
        boolean result=method((str1)->str1.contains("H"),(str2)->str2.contains("W"));
        System.out.println(result);
    }
}

3.3.3默认方法:negate

  • “与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法 negate 的JDK源代码为:
default Predicate<T> negate() {
	return (t)> !test(t);
}
  • negate方法使用:判断字符串中是否不包含W
public class Test {
    //定义一个判断方法
    public static boolean method(Predicate<String> pre1) {
        return pre1.negate().test("AISMALL");
    }

    public static void main(String[] args) {
        boolean result=method((str1)->str1.contains("H"));
        System.out.println(result);
    }
}

3.3.4应用

  • 把一个String类型的数组中包含AISMALL的成员提取出来
public class Test {
    //定义一个判断方法
    public static List<String > filter(String[] array,Predicate<String> pre1,Predicate<String> pre2 ) {
        //定义一个集合,用来存储满足条件的成员
        List<String> list=new ArrayList<>();
        //遍历传入的数组
        for (String info:array) {
            //判断条件
            if(pre1.and(pre2).test(info))list.add(info);
        }
        return list;
    }

    public static void main(String[] args) {
        //定义一个String类型数组
        String[] array={"AISMALL_01,女","AISMALL_02,男","AISMALL_03,女"};
        //调用filter方法,使用Lambda表达式写出过滤条件
        List<String> list=filter(array,s->"女".equals(s.split(",")[1]),s->s.split(",")[0].contains("AISMALL"));
        System.out.println(list);
    }
}
  • 运行结果:
[AISMALL_01,, AISMALL_03,]

3.4Function接口

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

3.4.1抽象方法:apply

  • Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。
  • 使用的场景例如:将 String 类型转换为 Integer 类型。
public class Test {
        private static void method(Function<String, Integer> function) {
            int num = function.apply("10");
            System.out.println(num + 20);
        }
        public static void main(String[] args) {
            method(s-> Integer.parseInt(s));
        }
    }

3.4.2默认方法:andThen

  • Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如下:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
	Objects.requireNonNull(after);
	return (T t)> after.apply(apply(t));
}
  • 该方法同样用于“先做什么,再做什么”的场景
public class Test {
    private static void method(Function<String, Integer> one, Function<Integer, Integer> two) {
        int num = one.andThen(two).apply("10");
        System.out.println(num + 20);
    }
    public static void main(String[] args) {
        method(str->Integer.parseInt(str)+10, i -> i *= 10);
    }
 }

  • 分析:第一个操作是将字符串解析成为int数字,第二个操作是乘以10。两个操作通过 andThen 按照前后顺序组合到了一起

猜你喜欢

转载自blog.csdn.net/weixin_45583303/article/details/105928554