Java基础-函数式编程接口篇

好久不见. 提交于 2019-12-04 23:29:23

1. 函数式接口

1.1 函数式接口介绍

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

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

从应用层面来讲,Java中的Lambda可以被当做是匿名内部类的“语法糖”,但是二者在原理上是不同的。

1.2 格式

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

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

1.3 @FunctionalInterface注解

与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注 解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。需要注 意的是,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

1.4 自定义函数式接口

对于刚刚定义好的 MyFunctionalInterface 函数式接口,典型使用场景就是作为方法的参数:

  public  static  void show(MyFunctionalInterface func){
    func.mythond();
  }
  public static void main(String[] args) {
    // 传入匿名内部类
    show(new MyFunctionalInterface() {
      @Override
      public void mythond() {
        System.out.println("执行了");
      }
    });
    // 简写Lambda表达式
    show(()-> System.out.println("执行了"));
  }

2. 函数式编程

​ 在兼顾面向对象特性的基础上,Java语言通过Lambda表达式与方法引用等,为开发者打开了函数式编程的大门。 下面我们做一个初探。

2.1 Lambda延迟执行

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

性能浪费的日志案例

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

public class Demo01Logger {
    private static void log(int level, String msg) {
        if (level == 1) {
            System.out.println(msg);
        }
    }
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        String msgC = "Java";
        log(1, msgA + msgB + msgC);
    }
}

这段代码存在问题:无论级别是否满足要求,作为 log 方法的第二个参数,三个字符串一定会首先被拼接并传入方 法内,然后才会进行级别判断。如果级别不符合要求,那么字符串的拼接操作就白做了,存在性能浪费。

备注:SLF4J是应用非常广泛的日志框架,它在记录日志时为了解决这种性能浪费的问题,并不推荐首先进行 字符串的拼接,而是将字符串的若干部分作为可变参数传入方法中,仅在日志级别满足要求的情况下才会进 行字符串拼接。例如: LOGGER.debug("变量{}的取值为{}。", "os", "macOS") ,其中的大括号 {} 为占位 符。如果满足日志级别要求,则会将“os”和“macOS”两个字符串依次拼接到大括号的位置;否则不会进行字 符串拼接。这也是一种可行解决方案,但Lambda可以做到更好。

体验Lambda的更优写法

使用Lambda必然需要一个函数式接口:

@FunctionalInterface
public interface MessageBuilder {
  String message();
}

然后对log方法进行改造

public class Test01 {
  private static  void  log(int level,MessageBuilder builder){
    if(level==1){
      String mes = builder.message();
      System.out.println(mes);
    }
  }
  public static void main(String[] args) {
    String msgA = "Hello";
    String msgB = "World";
    String msgC = "Java";
    log(1,()-> msgA+msgB  +msgC);
  }
}

这样一来,只有当级别满足要求的时候,才会进行三个字符串的拼接;否则三个字符串将不会进行拼接

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

如果抛开实现原理不说,Java中的Lambda表达式可以被当作是匿名内部类的替代品。

如果方法的参数是一个函数 式接口类型,那么就可以使用Lambda表达式进行替代。

使用Lambda表达式作为方法参数,其实就是使用函数式 接口作为方法参数。 例如 java.lang.Runnable 接口就是一个函数式接口,假设有一个 startThread 方法使用该接口作为参数,那么就 可以使用Lambda进行传参。这种情况其实和 Thread 类的构造方法参数为 Runnable 没有本质区别。

  public static void startThead(Runnable run){
    new Thread(run).start();
  }
  public static void main(String[] args) {
    startThead(()-> System.out.println(Thread.currentThread().getName()));
  }

类似地,如果一个方法的返回值类型是一个函数式接口,那么就可以直接返回一个Lambda表达式。当需要通过一 个方法来获取一个 java.util.Comparator 接口类型的对象作为排序器时,就可以调该方法获取。

  private static Comparator<String> newComparator() {
      return (o1, o2) -> o1.length()-o2.length();
  }
  public static void main(String[] args) {
    // 对字符串数组排序,按照字符长度排序
    String[]arr = {"ab","b","abc","abcde","abcd","aaaaaa"};
    //
    Arrays.sort(arr);
    Arrays.sort(arr,newComparator());
    System.out.println(Arrays.toString(arr)); // [b, ab, abc, abcd, abcde, aaaaaa]
  }

其中直接return一个Lambda表达式即可。

3. 常用的函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在 java.util.function 包中被提供。 下面是最简单的几个接口及使用示例。

3.1 Supplier接口

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

  public static String show(Supplier<String> sp){
    return sp.get();
  };
  public static void main(String[] args) {
    System.out.println(show(()->"你好Java"));
  }

练习:求数组最大值

  public static int getMax(Supplier<Integer> sp){
    return sp.get();
  }
  public static void main(String[] args) {
    int[]arr={11,2,3,6,4,66,22,19};
    int max = getMax(()->{
      int maxValue = arr[0];
      for (int i = 1; i < arr.length; i++) {
        if(maxValue<arr[i]){
          maxValue = arr[i];
        }
      }
      return maxValue;
    });
    System.out.println(max);
  }

3.2 Consumer接口

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

  • 抽象方法 :accept,Consumer 接口中包含抽象方法 void accept(T t) ,意为消费一个指定泛型的数据。基本使用如:

      public static void printList(ArrayList<String> list, Consumer<ArrayList> con){
        con.accept(list);
      }
      public static void main(String[] args) {
          ArrayList<String> list = new ArrayList<>();
          list.add("张三");
          list.add("李四");
          list.add("王五");
          printList(list,arrayList->{
            for (int i = 0; i < arrayList.size();i++){
              System.out.println("姓名:" + arrayList.get(i));
            }
          });
      }
  • 默认方法:如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费数据的时候,首先做一个操作, 然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:

    • 源码

      default Consumer<T> andThen(Consumer<? super T> after) {
          Objects.requireNonNull(after);
          return (T t) ‐> { accept(t); after.accept(t); };
      }
      /*
      备注: java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出
      NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
      */
    • 代码

      //要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况:
        public static void printList(ArrayList<String> list, Consumer<ArrayList> one,Consumer<ArrayList> two){
          one.andThen(two).accept(list);
        }
        public static void main(String[] args) {
          ArrayList<String> list = new ArrayList<>();
          list.add("张三");
          list.add("李四");
          list.add("王五");
          printList(list,arrayList->{
            System.out.println("======顺序打印=======");
            for (int i = 0; i < arrayList.size();i++){
              System.out.println("姓名:" + arrayList.get(i));
            }
          },arrayList -> {
            System.out.println("======倒序打印=======");
            for (int i = arrayList.size()-1; i >=0;i--){
              System.out.println("姓名:" + arrayList.get(i));
            }
          });
        }

3.3 Predicate接口

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

抽象方法test

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

  public static void main(String[] args) {
    // 判断一个字符串中是否包含大写的J和大写的H
    String str = "Hello Java";
    boolean isHas = checkStr(str,data->data.contains("J")&&data.contains("H"));
    System.out.println(str + "是否包含大写J和H:"+isHas);
  }
  public static boolean checkStr(String str , Predicate<String> predicate){
    return predicate.test(str);
  }

默认方法and、or、nagate

  • 方法
// and 且
default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) ‐> test(t) && other.test(t);
}
// or 或
default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) ‐> test(t) || other.test(t);
}
// nagate非
default Predicate<T> negate() {
  return (t) ‐> !test(t);
}
  • 代码

    // and
      public static void main(String[] args) {
        // 判断一个字符串中是否包含大写的J和大写的H
        String str = "Hello Java";
        boolean isHas = checkStr(str,data->data.contains("J"),data->data.contains("H"));
        System.out.println(str + "包含大写J和H:"+isHas);
      }
      public static boolean checkStr(String str , Predicate<String> one,Predicate<String> two){
        return one.and(two).test(str);
      }
    // or
      public static void main(String[] args) {
        // 判断一个字符串中是否包含大写的J和大写的H
        String str = "Hello Java";
        boolean isHas = checkStr(str,data->data.contains("J"),data->data.contains("H"));
        System.out.println(str + "包含大写J或H:"+isHas);
      }
      public static boolean checkStr(String str , Predicate<String> one,Predicate<String> two){
        return one.or(two).test(str);
      }
    }
    // nagate
      public static void main(String[] args) {
        // 判断一个字符串中是否包含大写的J和大写的H
        String str = "Hello Java";
        boolean isHas = checkStr(str,data->data.contains("J")&&data.contains("H"));
        System.out.println(str + "没有包含大写J和H:"+isHas);
      }
      public static boolean checkStr(String str , Predicate<String> predicate){
        return predicate.negate().test(str);
      }
    }

3.4 Function接口

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

抽象方法:apply

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

  public static void main(String[] args) {
    methond(str->{
      return Integer.parseInt(str);
    });
    // 打印结果30
  }
  public static void methond(Function<String,Integer> func){
    Integer num = func.apply("20");
    System.out.println(num + 10);
  }

默认方法: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));
}

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

  public static void main(String[] args) {
    // 将一个字符串变成数字后,再乘以10的结果
    test(str->Integer.parseInt(str),num->num * 10);
  }
  public static void test(Function<String,Integer> one,Function<Integer,Integer> two){
    int result = one.andThen(two).apply("20");
    System.out.println("结果是:" + result);

  }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!