JAVA

详解Java中的Lambda表达式(推荐)

Java 目前已经出到13的版本,但是国内大部分公司应该都停留在 Java 8 的版本(不敢承担升级带来的风险)。在Java8中给我们带来了 Lambda表达式和Stream流式操作,提供了函数式编程和简化批处理操作。可能大家日常使用很多,但是很少去关注Lambda 和 Stream实现。本篇就带着大家一起关注这两块知识,搬好小板凳我们一起看电视。

在java中我们很容易将一个变量赋值,比如int a =0;int b=a;

但是我们如何将一段代码和一个函数赋值给一个变量?这个变量应该是什么的类型?

在javascript中,可以用一个对象来存储。

    var t=function()
    {
      int a=1;
      a=a+1;
      alert(a);
    } 

在java中,直到java8的lambda的特性问世,才有办法解决这个问题

什么是Lambda


Lambda表达式是JDK 8开始后的一种新语法形式。简单来说,编程中提到的 lambda 表达式,通常是指需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。

可以说 lambda的表达式其实是接口的实现的“另一种方式”。这种方式更加简洁,更容易阅读。除了代码层面的简洁外,在编译的结果时候lambda也不会产生一个多余的匿名类。

一般我们使用变量来进行赋值操作:

但是在 Java8 之前是没有提供将一个方法赋值给一个变量的操作:

在 JavaScript 中有 闭包的概念,所以 Java不甘落后,也去学习了人家,搞出了匿名函数的概念。

在Java 8里面,所有的Lambda的类型都是一个接口,而Lambda表达式本身,也就是”那段代码“,需要是这个接口的实现。这是我认为理解Lambda的一个关键所在,简而言之就是,Lambda表达式本身就是一个接口的实现。

基本语法:

    (parameters) -> { expression or statements }
(匿名内部类被重写方法的形参列表) -> {
	重写方法
}

示例:

// 无参数, 返回1+2的结果
() -> 1+2;
 
// 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x;
 
// 接收2个参数(数字),返回表达式运算的结果
(x, y) -> x + y;
 
// 多个语句要用大括号包裹, 并且返回值要用return指明
(x, y) -> {
    int result = x + y;
    System.out.print(result);
    return result;
};

Lambda表达式有如下约定:

  • 一个 Lambda 表达式可以有零个或多个参数;
  • 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)与(a)效果相同;
  • 所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b) 或 (int a, int b) 或 (String a, int b, float c);
  • 空圆括号代表参数集为空。例如:() -> 42;
  • 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a;
  • Lambda 表达式的主体可包含零条或多条语句;
  • 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略。匿名函数的返回类型与该主体表达式一致;
  • 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空。

@FunctionalInterface 是 Java 8 新加入的一种接口,用于指明该接口类型声明是根据 Java 语言规范定义的函数式接口。Java 8 还声明了一些 Lambda 表达式可以使用的函数式接口,当你注释的接口不是有效的函数式接口时,可以使用 @FunctionalInterface 解决编译层面的错误。

什么是函数式接口?

  • 首先必须是接口、其次接口中有且仅有一个抽象方法的形式
  • 通常会在接口上加上一个@FunctionalInterface注解,标记该接口必须是满足函数式接口

根据定义,函数式接口只能有一个抽象方法,如果你尝试添加第二个抽象方法,将抛出编译时错误。例如:

@FunctionalInterface
public interface DoSomethingInterface {
 
  void doSomeThing();
 
  void doSomeThing1();
 
}

举一个简单使用的例子,首先定义一个接口:

@FunctionalInterface
public interface DoSomethingInterface<T> {
 
  String doSomeThing(T t);
 
}

这个接口里面只能有一个函数,接口里是返回值为String类型的方法,那么下面使用它的时候就需要有返回值:

class MainTest{
 
   public static void main(String[] args) {
 
     DoSomethingInterface<String> t = str -> {
       return str;
     };
     String print = t.doSomeThing("print");
     System.out.println(print);
 
   }
}

t就相当于是接口的具体实现,那么使用 t.doSomeThing(“print”)就会去调用 t 的匿名函数执行。从这段代码看,如果我们有一个抽象类的具体实现逻辑相对简单不用大段代码的时候,可以使用 Lambda 表达式去实现具体的逻辑,这样就不用new 多个类。

Java 8 内置4大核心函数式接口


大家日常使用的List,Map,肯定大量使用了Stream流式处理,在流式处理中大量使用了Lambda,比如List的foreach方法就使用了Lambda表达式。Java8中抽象出来4类基本的函数式接口,日常开发中我们其实也是可以使用的。

以上4种类型是Java默认提供的Lambda类,开箱即用。

Consumer使用:

public void test(){
 save(user, t->{ saveUser(t); });
}
 
public void save(User user, Consumer<User> consumer){
 consumer.accept(user);
}

Supplier使用:

public void test(){
 
 Config config = getConfig(t -> {
  return getAllConfig(t);
 });
 
}
 
public Config getConfig(Supplier<Config> supplier) {
 return supplier.get();
}

Function使用:

public void test(){
 User user = getUser(t -> { return getUserByName(t); });
}
 
public User getUser(Function<String, User> function) {
 return function.apply(t);
}

Predicate使用:

public void test(){
 boolean b = checkStatus(user, t -> {
  return checkUserExist(t);
 });
 
}
 
public boolean checkStatus(User user, Predicate<User> predicate) {
 return predicate.test(user);
}

如何使用Lambda?


我们将根据下面三个问题来帮助大家理解和使用Lambda

背景:我们自定义了一个man的类,创建了一个man的List。

class man {
    public int age;
    public char sex;
    public double socre;

    public man(int age, char sex, double score) {
        this.age = age;
        this.sex = sex;
        this.score = score;
    }
}

问题一:现需要对这个list根据人的年龄进行排序

要实现排序的功能,可以直接调用List对象自带的sort方法完成,但是需要man先实现Comparator的接口并重写compare方法,编译器才能比较两个不同man的大小。但是要更改原始类的代码,会比较麻烦,如果以后要对人的分数进行排序,那就又要更改的类的源码,这样操作很不方便。
sort(Comparator<? super E> c) 方法可以直接传入一个Comparator对象,我们可以直接改写compare方法就可以实现比较。

第一种写法:常规

public class lambdaTry {
    public static void main(String[] args) {
        List<man> humans = new ArrayList<>();
        humans.add(new man(19, 'g', 98.0));
        humans.add(new man(18, 'b', 95.0));
        humans.add(new man(20, 'b', 96.0));
        humans.add(new man(17, 'g', 97.0));

        humans.sort(new Comparator<man>() {
            @Override
            public int compare(man o1, man o2) {
                return o1.age - o2.age;
            }
        });
    }
}

第二种写法:Lambda

我们知道Lambda是用来简化函数式接口的匿名内部类,且Comparator满足函数式接口的两个条件:

  • 首先必须是接口、其次接口中有且仅有一个抽象方法的形式
  • @FunctionalInterface注解
@FunctionalInterface
public interface Comparator<T> {
	int compare(T o1, T o2);
    ...
}

因此我们可以对上述的源码进行改写成Lambda格式

public class lambdaTry {
    public static void main(String[] args) {
        List<man> humans = new ArrayList<>();
        humans.add(new man(19, 'g', 98.0));
        humans.add(new man(18, 'b', 95.0));
        humans.add(new man(20, 'b', 96.0));
        humans.add(new man(17, 'g', 97.0));

        humans.sort((man o1, man o2) -> {
                return o1.age - o2.age;
        });
    }
}

改写过后代码简洁了很多。但是还可以继续简写。

Lambda表达式的省略写法

  • 参数类型可以不写
  • 如果只有一个参数,参数类型可以省略,同时()也可以省略
  • 如果Lambda表达式的方法块中代码只有一行,可以省略大括号,同时省略分号。
  • 在条件三的基础上,如果这行代码是return语句,必须省略return。

第三种写法:Lambda简写

可以看到,此表达式满足省略写法的条件,可以继续简写成如下格式。只需要一行语句就能完成

public class lambdaTry {
    public static void main(String[] args) {
        List<man> humans = new ArrayList<>();
        humans.add(new man(19, 'g', 98.0));
        humans.add(new man(18, 'b', 95.0));
        humans.add(new man(20, 'b', 96.0));
        humans.add(new man(17, 'g', 97.0));

        humans.sort((o1, o2) -> o1.age - o2.age);
    }
}

问题二:将List转换为数组

我们知道List接口有一个方法toArray方法可以实现将其转换为数组。

JDK11之后,提供了这样的一个方法,提供了一个函数式接口来让我们转换

default <T> T[] toArray(IntFunction<T[]> generator) {
    return toArray(generator.apply(0));
}

IntFunction函数式接口是从JDK8之后实现的,内部只有一个apply抽象方法,是一个标准的函数式接口

@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}

我们可以直接用lambda,完成数组的转换

public class lambdaTry {
    public static void main(String[] args) {
        List<man> humans = new ArrayList<>();
        humans.add(new man(19, 'g', 98.0));
        humans.add(new man(18, 'b', 95.0));
        humans.add(new man(20, 'b', 96.0));
        humans.add(new man(17, 'g', 97.0));

        // 原本写法
        // man[] mans = humans.toArray(new IntFunction<man[]>() {
        //     @Override
        //     public man[] apply(int value) {
        //         return new man[value];
        //     }
        // });

        // lambda写法
        man[] mans = humans.toArray(value -> new man[value]);
        
        // 实际上用不上这样的写法,只是为了举例说明
        // man[] mans = humans.toArray(new man[0]);
        // man[] mans = humans.toArray(man[]::new);
        // 上面两种写法都可以,传值进去的size为0不影响实际的转换,具体可以看ArrayList的toArray重写方法
        
    }
}

问题三:输出年龄大于18的男同学的成绩

可以用forEach方法快捷实现,forEach方法来自于Iterable接口

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

再看Consumer接口,也是一个函数式接口

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    ...
}

具体实现

public class lambdaTry {
    public static void main(String[] args) {
        List<man> humans = new ArrayList<>();
        humans.add(new man(19, 'g', 98.0));
        humans.add(new man(18, 'b', 95.0));
        humans.add(new man(20, 'b', 96.0));
        humans.add(new man(17, 'g', 97.0));

        // humans.forEach(new Consumer<>() {
        //     @Override
        //     public void accept(man man) {
        //         if (man.age >= 18 && man.sex == 'g') {
        //             System.out.println(man.score);
        //         }
        //     }
        // });

        humans.forEach(man -> {
            if (man.age >= 18 && man.sex == 'g') {
                System.out.println(man.score);
            }
        });
    }
}

有时Lambda还可以继续简写成方法引用(method reference)

方法引用


方法引用通过方法的名字来指向一个方法。

方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

方法引用使用一对冒号 ::

主要分为四种:

  • 构造器引用 Class::newman[]
  • 静态方法引用 Class::static_method
  • 特定类的任意对象的方法引用 Class::method
  • 特定对象的方法引用 instance::method

以上关于Lambda就介绍到这里,大家应该明白了它是干啥的了吧。闭包是不是也没有这么难懂呢!联想到List的foreach使用,大家有没有冲动想自己实现一个Lambda在日常开发中炫一把(如果别人看不懂,会打你的)。