还在写臃肿的匿名内部类实现接口?快试试Lambda表达式!

365 阅读10分钟

前言

​ 在平时开发中,我们有时会用实现Runnable或Callable接口的方式来创建一个线程来做一些事情,但这些代码是一次性的,如果单独写一个Runnable或Callable的实现类就显得有些冗余,那可能有同学就想到可以用匿名内部类的方式来解决这个问题,但是匿名内部类的方式代码量显得又多又臃肿,虽然可以用开发工具自动生成这些代码但是给人的感觉代码并不简洁美观!

​ 为了解决这一问题,Oracle公司在2014年3月发布的Java8中带来了简洁的Lambda表达式!

1) Lambda表达式

  • Java8是Java语言开发的一个主要版本。Java8是oracle公司于2014年3月发布,可以看成是自Java5以来最具革命性的版本。java8为Java语言、编译器、类库、开发工具与JVM带来了大量新特性。Lambda表达式就是Java8的一大新特性!

  • 为什么要使用Lambda表达式?

    • Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
  • 我们先写两个例子来体验一下Lambda表达式的简洁:

    • 例一

      public static void main(String[] args) {
          //匿名内部类
          Runnable r1 = new Runnable() {
              @Override
              public void run() {
                  System.out.println("Hello World!");
              }
          }
          
          //Lambda表达式
          Runnable r2 = () -> System.out.println("Hello Lambda!");
      }
      
    • 例二

      public static void main(String[] args) {
          //原来使用匿名内部类作为参数传递
          TreeSet<String> ts1 = new TreeSet<>(new Comparator<String>() {
              @Override
              public int compare(String o1, String o2) {
                  return Integer.compare(o1.length(), o2.length());
              }
          });
              
          //Lambda表达式作为参数传递
          TreeSet<String> ts2 = new TreeSet<>(
          	(o1, o2) -> Integer.compare(o1.length(), o2.length())
          );
      }
      

    通过上面两个例子可以看出,相比于之前的匿名实现类的臃肿代码,Lambda表达式的方式真是简洁到飞起!

  • 那么就让我们一起学一下Lambda表达式的语法吧!

    • Lambda表达式:在Java8语言中引入的一种新的语法元素和操作符。这个操作符为“ ->”,该操作符被称为Lambda操作符箭头操作符。它将Lambda分为两个部分:

      • 左侧:指定了Lambda表达式需要的参数列表
      • 右侧:指定了Lambda体,是抽象方法的实现逻辑,也即Lambda表达式要执行的功能。
    • 语法格式一:无参,无返回值。

      Runnable r1 = () -> {System.out.println("Hello Lambda!");};
      
    • 语法格式二:Lambda需要一个参数,但是没有返回值。

      Consumer<String> con = (String str) -> {System.out.println(str);};
      
    • 语法格式三:数据类型可以省略,因为可由编译器推断得出,称为”类型推断“。

      Consumer<String> con = (str) -> {System.out.println(str);};
      
    • 语法格式四:Lambda若只需要一个参数时,参数的小括号可以省略。

      Consumer<String> con = str -> {System.out.println(str);};
      
    • 语法格式五:Lambda需要两个或以上的参数,多条执行语句,并且可以有返回值。

      Comparator<Integer> com = (x, y) -> {
          System.out.println("实现函数式接口方法!");
          return Integer.compare(x, y);
      };
      
    • 语法格式六:当Lambda体只有一条语句时,return与大括号若有,都可以省略。

      Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
      
  • 何为类型推断?

    上述Lambda表达式中的参数类型都是由编译器推断得出的。Lambda表达式中无需指定类型,程序依然可以编译,这是因为javac根据程序的上下文,在后台推断出了参数的类型。Lambda表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。

  • 学完了Lambda表达式,不知道你有没有发现一个问题,那就是上面Lambda的例子所实现的接口都只有一个抽象方法!这是什么原因呢?就让我们继续看一下函数式接口!

2) 函数式(Functional)接口

  • 什么是函数式(Functional)接口

    • 只包含一个抽象方法的接口,称为函数式接口。
    • 你可以通过Lambda表达式来创建该接口的对象。(若Lambda表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
    • 我们可以在一个接口上使用**@FuncationalInterface**注解,这样做可以检查它是否是一个函数式接口。同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。
    • 在java.util.function包下定义了Java8的丰富的函数式接口
  • 如何理解函数式接口

    • Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP)编程是一切。但是随着Python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也即Java不但可以支持OOP还可以支持OOF(面向函数编程)
    • 在函数式编程语言当中,函数被当作一等公民对待。在将函数作为一等公民的编程原因中,Lambda表达式的类型是函数。但是在Java8中,有所不同。在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口
    • 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
    • 所以以前用匿名实现类表示的现在都可以用Lambda表达式来写
  • 举个栗子

    • 我们常见的Runnable接口就是一个函数式接口
  • 当然,我们也可以自己定义一个函数式接口:

这里要说一下,@FunctionalInterface注解只是起一个校验的作用,如果没写该注解这个接口依然是函数式接口,函数式接口的关键点在于只包含一个抽象方法,而不是@FunctionalInterface注解。@FunctionalInterface注解的作用主要是校验并在javadoc中声明。

  • 如果想要将Lambda表达式作为参数传递,那么接收Lambda该表达式的参数类型必须是与该Lambda表达式兼容的函数式接口的类型。

  • Java在java.util.function包下定义了Java8的丰富的函数式接口

    • 四大核心函数式接口

      函数式接口参数类型返回类型用途
      Consumer<T> 消费型接口Tvoid对类型为T的对象应用操作,包含方法:
      void accept(T t)
      Supplier<T> 供给型接口T返回类型为T的对象,包含方法:
      T get()
      Function<T, R> 函数型接口TR对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t)
      Predicate<T> 断定型接口Tboolean确定类型为T的对象是否满足某约束,并返回boolean值。包含方法:boolean test(T t)
    • 其他接口

      函数式接口参数类型返回类型用途
      BiFunction<T, U, R>T, UR对类型为T,U参数应用操作,返回R类型的结果。包含方法为:R apply(T t, U u);
      UnaryOperator<T>(Function子接口)TT对类型为T的对象进行一元运算,并返回T类型的结果。包含方法为:T apply(T t);
      BinaryOperator<T>(BiFunction子接口)T, TT对类型为T的对象进行二元运算,并返回T类型的结果。包含方法为:T apply(T t1, T t2);
      BiConsumer<T, U>T, Uvoid对类型为T,U参数应用操作。包含方法为:
      void accept(T t, U u);
      BiPredicate<T, U>T, Uboolean包含方法:boolean test(T t, U u);
      ToIntFunction<T>
      ToLongFunction<T>
      ToDoubleFunction<T>
      Tint
      long
      double
      分别计算int、long、double值的函数
      IntFunction<R>
      LongFunction<R>
      DoubleFunction<R>
      int
      long
      double
      R参数分别为int、long、double类型的函数
  • 可以说函数式接口就是Lambda表达式的核心!Lambda表达式只能作用于函数式接口!

  • 那么Lambda表达式就是最简单的写法了吗?并非如此!还可以有更简单的写法!就让我们一起看一下方法引用吧!

3) 方法引用与构造器引用

  • 方法引用(Method References)

    • 当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!

    • 方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。

    • 要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致!

    • 格式:使用操作符“::”将类(或对象)与方法名分割开来。

    • 如下三种主要使用情况:

      • 对象 :: 实例方法名
      • 类 :: 静态方法名
      • 类 :: 实例方法名
    • 举个栗子:

      //例一 对象 :: 实例方法名
      //Lambda写法
      Consumer<String> con = (x) -> System.out.println(x);
      //方法引用
      Consumer<String> con2 = System.out :: println;
      
      //例二 类 :: 静态方法名
      //Lambda写法
      Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
      //方法引用
      Comparator<Integer> com1 = Integer :: compare;
      
      //例三 类 :: 实例方法名
      //Lambda写法
      BiPredicate<String, String> bp = (x, y) -> x.equlas(y);
      //方法引用
      BiPredicate<String, String> bp1 = String :: equals;
      
    • 注意:当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName :: methodName(上述例三)

  • 构造器引用

    • 格式:ClassName :: new

    • 与函数式接口相结合,自动与函数式接口中方法兼容。

    • 可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象方法的参数列表一致!且方法的返回值即为构造器对应类的对象。

    • 举例:

      //Lambda写法
      Function<Integer, MyClass> fun = (n) -> new MyClass(n);
      
      //构造器引用
      Function<Integer, MyClass> fun = MyClass :: new;
      
  • 数组引用

    • 格式:type[] :: new

    • 举例:

      //Lambda写法
      Function<Integer, Integer[]> fun = (n) -> Integer[n];
      
      //数组引用
      Function<Integer, Integer[]> fun = Integer[] :: new;
      

4) IDEA自动简化代码

以下操作仅针对使用IntelliJ IDEA开发的同学。

如果你看完了上面的文章还是不知道该怎么去用Lambda表达式和方法引用去简化函数式接口的匿名内部实现类,别慌,我们有强大的开发工具IntelliJ IDEA!

我们可以写一个最简单的Comparator接口匿名实现类:

Comparator<Integer> com = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return Integer.compare(o1, o2);
    }
};

如果你把上面的代码复制到IDEA中可以发现,部分代码是灰色的,而如果你把鼠标放在灰色部分,idea就会智能的提示你:Anonymous new Comparator<Integer>() can be replaced with lambda(匿名的Comparator()实现类可以替换为lambda),并且支持一键替换!

点击之后就会发现上面的代码就变成了下面的样子:

Comparator<Integer> com = (o1, o2) -> Integer.compare(o1, o2);

但是你会发现,替换为Lambda表达式之后代码依然是灰色的,那么就说明还可以进一步替换为方法引用!依然支持一键替换!

重复上面的操作idea就会帮你替换为最简便的代码写法!

Comparator<Integer> com = Integer :: compare;

所以就算你不懂得直接写最简写法,但是依然通过强大的开发工具将臃肿的实现方式替换为最简洁的写法!

ps:笔者使用的是IDEA的2019.3.4版本,其他版本的IDEA和其他的开发工具是否有该功能笔者并未测试过,了解的同学不妨在评论区讨论一下吧!

总结

​ 以上就是对于Java8新特性Lambda表达式及函数式接口和方法引用的讲解啦。

​ 觉得上面内容有帮助到你的掘友希望赏个 star ~(ps:不要吝啬哦)