lambda表达式与函数式接口

lambda表达式与函数式接口

写着一篇的起因是学函数式接口那几个函数式接口的实现真把我整糊涂了,看了半天啥也看不懂,又忽然想起来也没有系统的研究过lambda表达式,索性一起做一个总结,方便以后查阅

为什么要有lambda表达式

首先我们要搞明白一个问题:lambda表达式存在的意义是什么?简化代码,提高代码可读性(虽然在不少时候,这玩意的一些奇奇怪怪的写法反倒让代码看起来更奇怪)

对于大部分人来说使用lambda表达式最多的地方就是匿名类了吧,那么我们就从匿名类聊起

匿名内部类与lambda表达式

首先我还是解释一下什么是匿名内部类吧,我们都知道对于一个抽象类或者接口,我们是不能直接将其实例化的,但是这件事并不是绝对的,我们可以通过匿名内部类来创建一个抽象类或接口的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Student {
void hello(String name);
}//一个很典型的接口
public class Main {
public static void main(String[] args) {
Student student=new Student() {
@Override
void hello() {
System.out.println("hello"+name);
}
}
}
}//我们可以通过上面的方法创建一个匿名内部类,在其后直接添加抽象类中的方法的实现

在上面的代码中,我们实际上并不是真的将一个接口实例化,而是创建了这个抽象类的一个子类,在子类内将其方法进行了实现,只不过这个类连类名都没有,所以我们将其称之为匿名内部类

但是,如果就像上面的示例一样,如果接口(注意只能是接口,抽象类是不行的)中只有一个方法没有实现,那么久可以将上面的代码简化为

1
2
3
public static void main(String[] args) {
Student student=(String name)-> System.out.println("hello"+name);
}

看起来是不是简洁的多了呢,当然,你也可选择多实现一点逻辑

1
2
3
4
Student student=(String name)-> {
System.out.println("hello"+name);
System.out.println("随便加点内容");
};

或者你也可选择直接使用一些现成的方法

1
2
3
4
5
6
7
8
public interface Student {
int grade(int chinese ,int math);
}
public class Main {
public static void main(String[] args) {
Student student=(chinese, math)->chinese+math;
}
}这样grade方法返回的就是数学和语文成绩之和了

当然,如果愿意,路子还可以更野一些

1
2
3
public static void main(String[] args) {
Student student=(chinese, math)->Integer.compare(chinese,math);
}

直接引用integer类中的方法,不过,既然都这样了,再看一种形式,当然这已经不是lambda表达式的范围了

1
Student student=Integer::compare;

怎么样,纯纯C++味,但Java中就是支持这样做.

lambda为什么可以用

lambda表达式编译器到底是如何被编译器处理的呢?从本质上讲,lambda表达式其实在编译的过程中被编译器替换为了正常的匿名内部类限的写法,编译器在信息足够的情况下可以通过上下文推断出lambda表达式具体的含义并将其替换。

那么,所谓的信息足够又是指什么呢?有对应的函数式接口,也就是我们今天要说的另一个主题,那么什么是函数是接口呢,一个接口中只有一个方法没有实现(允许有其他的方法,但必须提供对应的实现),此时,我们就可以直接使用lambda表达式给这个接口内为实现的方法提供具体的实现。

总结一下,就是lambda表达式只允许在存在函数式接口的地方使用

认识函数式接口

刚才不是说了函数式接口的定义吗,为什么又要解释,因为函数式接口还有不少内容

自己写函数式接口

在写自己需要使用的函数式接口的时候,为规范最好添加@FunctionalInterface注解,虽然这个注解不加也能通过编译,但这个注解会提醒编译器检查该接口内只有一个未实现的接口,确保不会出现错误,就像这样

1
2
3
4
@FunctionalInterface
public interface Student {
void hello(String name);
}

Java所提供的几个函数式接口

好的,比较难以理解的部分来了,Java本身提供了一些提前写好的函数式接口以供使用,这里我们介绍几个比较常用的

  1. supplier供给接口

    这个接口是专门的供给接口,具体的原型如下

    1
    2
    3
    4
    @FunctionalInterface   
    public interface Supplier<T> {
    T get(); //实现此方法,实现供给功能
    }

    是不是有点抽象,这个玩意的意义是什么,我这里给出一种用法

    1
    2
    3
    4
    5
    6
    //专门供给Student对象的Supplier
    private static final Supplier<Student> STUDENT_SUPPLIER = Student::new;//这里的new指的是student的构造方法
    public static void main(String[] args) {
    Student student = STUDENT_SUPPLIER.get();
    student.hello();
    }

    你或许会问,我直接用构造函数不好吗,当然可以,但是如果你要用的不是构造函数呢,你所需要供给的是一个复杂的对象或者需要经过一系列计算呢,当然,直接在类内部写一个方法来实现是可以的,但是这玩意简单啊,直接用supplier就可以迅速的写出一个返回某个特定类型的对象的函数,可以使代码更简洁

  2. consume消费接口

    这个接口是用来消费的,不会返回任何东西,原型如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @FunctionalInterface
    public interface Consumer<T> {
    void accept(T t); //这个方法就是用于消费的,没有返回值

    default Consumer<T> andThen(Consumer<? super T> after) { //这个方法便于我们连续使用此消费接口
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
    }
    }

    骚年,还看得懂吗这个andThen方法到底是什么情况,我先给出一个使用的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Student {
    void hello(String name){
    System.out.println("Hello "+name);
    }
    }
    public class Main {
    private static final Consumer<Student> consumer=(student) -> System.out.println(student+"你好");
    public static void main(String[] args) {
    Student student=new Student();
    consumer.andThen (stu-> System.out.println("hello"))
    .andThen(stu-> System.out.println("再见"))
    .accept(student);
    }
    }

    你可以猜猜运行的结果

    1
    2
    3
    4
    -classpath E:\test\out\production\test Main
    Student@34c45dca你好
    hello
    再见

    是不是有点奇怪,为什么会有这样的运行结果呢,我们慢慢分析一下.首先在创建consumer时accept内被写入了你好,andthen方法要求的参数是一个Consumer对象,而stu-> System.out.println("hello")实际上构成了一个Consumer对象,也就是对应的andThen中的after,而andThen也会返回一个Consumer对象,这个对象又是用lambda表达式创建的,这个对象的accept内容为先调用原来的accept方法,再调用after的accept,于是就有了先打印你好再打印hello的结果了,用这样的方式可以实现对对象的复杂处理

  3. function函数接口

    这个接口的默认方法有点多,让我们一个个来看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @FunctionalInterface
    public interface Function<T, R> {
    R apply(T t); //这里一共有两个类型参数,其中一个是接受的参数类型,还有一个是返回的结果类型

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
    }

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

    static <T> Function<T, T> identity() {
    return t -> t;
    }
    }

    首先是apply

    1
    R apply(T t);

    这个函数式允许lambda表达式进行实现的函数,可以看到允许有两个不同种类的参数

    1
    2
    3
    4
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
    }

    稍微解释一下这个方法的含义<V>是一个方法签名,说明该方法支持泛型,这个泛型以V指代,返回类型为Function<V, R>,形参为Function<? super V, ? extends T> before也就是一个类型为Function<? super V, ? extends T> 的参数,如果看不懂这个参数的含义,请自行了解泛型相关内容,再来看看返回值,即将before的返回值作为现在的apply的参数,也就是说,这个方法的作用实际上是将两个方法组合起来,也正如它的名称compose

    andThen方法的效果同上,不做过多介绍

    1
    2
    3
    static <T> Function<T, T> identity() {
    return t -> t;
    }

    这个方法有点抽象,它会将输入的东西原样返回,我着实想不到存在的意义是什么

  4. predicate断言接口

    先看原型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @FunctionalInterface
    public interface Predicate<T> {
    boolean test(T t); //这个方法就是我们要实现的

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

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

    default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
    ? Objects::isNull
    : object -> targetRef.equals(object);
    }
    }

    显然这个接口是用来做做判断的,test就不用多说了,直接看

    1
    2
    3
    4
    5
    6
    7
    8
     default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
    }
    default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
    }

    这个就比较简单了,就是同时判断两个,一个或一个与

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

    翻转结果

    1
    2
    3
    4
    5
    6
     static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
    ? Objects::isNull
    : object -> targetRef.equals(object);
    }
    }

    专门用来判断两个对象是否相等

很好,能看到这里是不容易的,让我们做个总结吧,所有的这些接口中的默认方法都是在改写我们用lambda表达式实现的那个方法,所以我们只有真正的调用我们自己实现的方法的那一刻,方法才会真的被执行

真正的你会用到的

好的,上面那一堆东西如果你没有看懂,问题不大,慢慢来即可,但是Java库中提供的可远不止这些函数式接口

  1. List类

    List类在实际编程中被大量使用,其中一些方法就使用了函数式接口

    • forEach

      1
      2
      3
      4
      5
      6
      default void forEach(Consumer<? super T> action) {
      Objects.requireNonNull(action);
      for (T t : this) {
      action.accept(t);
      }
      }

      怎么样,这个accept是不是有点熟悉,对,这就是上面的消费型函数式接口,所以我们才能够在forEach中使用lambda表达式,比如说我们想要打印一个List中的所有长度大于三的元素,就可以用lambda表达式这么写

      1
      2
      3
      4
      5
      6
      ist<String>list = new  ArrayList<>(Arrays.asList("s","sss","asd"));
      list.forEach(s->{
      if(s.length()>3){
      System.out.println(s);
      }
      });

      当然,如果你一定要用匿名内部类,也不是不行

      1
      2
      3
      4
      5
      6
      7
      8
      9
      List<String>list = new  ArrayList<>(Arrays.asList("s","sss","asd"));
      list.forEach(new Consumer<String>() {
      @Override
      public void accept(String s) {
      if(s.length()>3){
      System.out.println(s);
      }
      }
      });

      先不说那个版本的代码更加简洁的问题,lambda表达式帮你省去了记忆Consumer类的具体内容的问题,而交由编译器推断这应该是一个什么样的类

    • removeIf

      这个方法的原型如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      default boolean removeIf(Predicate<? super E> filter) {
      Objects.requireNonNull(filter);
      boolean removed = false;
      final Iterator<E> each = iterator();
      while (each.hasNext()) {
      if (filter.test(each.next())) {
      each.remove();
      removed = true;
      }
      }
      return removed;
      }

      具体的方法实现过程还是很好理解的,还是上面的List,如果我们希望移除长度大于三的字符串

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      list.removeIf(s->s.length()>3);
      list.removeIf(new Predicate<String>() {
      @Override
      public boolean test(String s) {
      if(s.length()>3){
      return true;
      }
      return false;
      }
      });

      两种写法的差异一下子就体现了出来

    • replaceALL

      用于根据条件替换某些元素,只需要用lambda表达式完成条件判断与替换即可,不再做过多的解释

    • sort,这个就更不用多说什么了吧

  2. Map类

    • forEach,无需多言
    • getOrDefault,用于查找Map中有对应的key的value
    • putIfAbsent,如果对应的key不存在,则将提供的数据按照提供的key放入Map

    等等等等,有很多你所常用的方法都运用了函数式接口,这为我们的编程带来了极大的便利

  3. Stream类

    这个就更不用多说了吧,几乎你常用的每一个方法都会允许使用lambda表达式

总结

总而言之,所谓的lambda表达式就是一种创建函数式接口的实现的简易方法,它极大的减少了我们记忆各种函数式接口内容的压力,且能够在一定程度上让代码变得更简洁,更易读.上面对List,Map和Stream的介绍实际上有些过于简答,过段时间我应该会再专门的对这些类做一个详细的介绍,最近属实是有点忙

放图


lambda表达式与函数式接口
http://soulmate.org.cn/2024/12/20/lambda表达式与函数式接口/
作者
Soul
发布于
2024年12月20日
许可协议