Java基础丨Lambda 表达式

前言

λ ,形似汉字的“入”,希腊字母表中排序第十一位的字母,英语名称为 Lambda
Lambda 表达式,也可称为闭包,是 JDK 8 的一个新特性,它可以取代大部分的匿名内部类,取消了模板,允许用函数式风格编写代码。这样写出更优雅的 Java 代码,可读性更好,表达更清晰,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。

使用前提

并不是所有的接口都可以使用 Lambda 表达式来实现,Lambda 规定函数式接口才能用。如果接口中只有一个抽象方法(可以包含多个默认方法或多个 static 方法),那么该接口就是函数式接口。Java 中内置了不少的函数式接口。


Runnable接口
Callable接口
Comparator接口

JDK 8 又新增了一个包:java.util.function,在这个包里有很多类,不多介绍。
Function接口

@FunctionalInterface

@FunctionalInterface 注解就是用来指定某个接口必须是函数式接口,它只能修饰接口,不能修饰其它程序元素;作用是让编译器强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。
虽然 @FunctionalInterface 注解不是必须的,但自定义的函数式接口最好还是加上,一是按照规范养成良好的编程习惯,二是帮助他人一看到这个注解就知道是函数式接口,避免一些不必要的低级错误。

基础语法

Lambda 表达式由三部分组成:参数列表、箭头、主体。语法格式如下:

// 参数列表 + 箭头 + 表达式
(parameters) -> expression

// 参数列表 + 箭头 + 大括号和返回语句
(parameters) -> { statements; }

说明:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号,但是没有参数不能省略空括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

    练习用例

    自定义 6 个函数式接口,用于说明。

    /**
     * 无参数,无返回值
     */
    @FunctionalInterface
    public interface NoParamAndNoReturn {
      void sayHello();
    }
    
    /**
     * 无参数,有返回值
     */
    @FunctionalInterface
    public interface NoParamButReturn {
      String sayHello();
    }
    
    /**
     * 一个参数,无返回值
     */
    @FunctionalInterface
    public interface OneParamButNoReturn {
      void sayHello(String name);
    }
    
    /**
     * 一个参数,有返回值
     */
    @FunctionalInterface
    public interface OneParamAndReturn {
      String sayHello(String name);
    }
    
    /**
     * 多个参数,无返回值
     */
    @FunctionalInterface
    public interface MultiParamButNoReturn {
      void sayHello(String name, int age);
    }
    
    /**
     * 多个参数,有返回值
     */
    @FunctionalInterface
    public interface MultiParamAndReturn {
     String sayHello(String name, int age);
    }

    然后再新建一个测试类,用于实现以上 6 个接口并测试运行。在实现接口时,IDEA 会智能提示:Anonymous new NoParamAndNoReturn() can be replaced with lambda,因为 Lambda 表达式能简化某些匿名内部类的写法。
    IDEA提示
    为了简单起见,直接使用 Lambda 表达式,按照规则,能不写的都不写。

  • 简化参数类型,但是必须所有参数都不写,要么都写要么都不写。
  • 简化参数小括号,如果只有一个参数则,可以省略参数小括号。
  • 简化方法体大括号,如果方法条只有一条语句,则可以省略方法体大括号。
  • 如果方法体只有一条语句,并且是 return 语句,则可以省略方法体大括号。
  • 参数的命名可以简短一些,可以使用 a、b、c,但是为了可读性,最好还是用原方法的参数名。

按照以上规则简化,代码如下:

public class FunctionTest {
    public static void main(String[] args) {
        // 1.无参数,无返回值
        NoParamAndNoReturn npnr = () -> System.out.println("Hello, World!");
        npnr.sayHello();

        // 2.无参数,有返回值
        NoParamButReturn npbr = () -> "Hello, 2021!";
        String msg1 = npbr.sayHello();
        System.out.println(msg1);

        // 3.一个参数,无返回值
        OneParamButNoReturn opbnr = name -> System.out.println("你好,你可以叫我" + name);
        opbnr.sayHello("靓仔");

        // 4.一个参数,有返回值
        OneParamAndReturn opar = name -> {
            System.out.println("执行了一个参数,有返回值的方法");
            return name + ",我爱你!";
        };
        String msg2 = opar.sayHello("中国");
        System.out.println(msg2);

        // 5.多个参数,无返回值
        MultiParamButNoReturn mpbnr = (name, age) -> System.out.println("我叫" + name + ",今年" + age + "岁了");
        mpbnr.sayHello("靓仔", 21);

        // 6.多个参数,有返回值。参数的命名更简短了,但无法见名知义
        MultiParamAndReturn mpar = (a, b) -> a + "现在" + (b - 1949) + "岁了";
        String msg3 = mpar.sayHello("新中国", 2021);
        System.out.println(msg3);
    }
}

方法引用

有时候不是必须要重写某个匿名内部类的方法,箭头“->”右边执行的表达式只是调用一个类已有的方法,那么利用 Lambda 表达式的方法引用Method Reference)。
方法引用的格式:方法的归属者 + 一对冒号 + 方法名。静态方法的归属者为类名,普通方法的归属者为对象,所以格式为:ClassName::methodNameObjectName::methodName

引用静态方法

如果表达式符合“([参数1, 参数2, ...]) -> 类名.静态方法名([参数1, 参数2, ...])”,那么可以简化为“类名::静态方法名”。示例代码:

public class Function2Test {
    public static void main(String[] args) {
        /**
         * 引用静态方法,格式为[类名::方法名]
         * 原写法:Example exam = a -> addTenOpreration(a);
         * 引用了一个已经实现的 addTenOperation() 方法
         */
        Example exam = Function2Test::addTenOperation;
        int result = exam.addTenOperation(10);
        System.out.println(result);

        /**
         * 一个参数,无返回值的情况
         * 原写法:OneParamButNoReturn opbnr = name -> System.out.println(name);
         */
        OneParamButNoReturn opbnr = System.out::println;
        opbnr.sayHello("666");
    }

    @FunctionalInterface
    interface Example {
        int addTenOperation(int num);
    }

    public static int addTenOperation(int num) {
        // 执行加 10 操作
        return num + 10;
    }
}

引用对象的方法

如果表达式符合“([参数1, 参数2, ...]) -> 对象名.静态方法名([参数1, 参数2, ...])”,那么可以简化为“对象名::方法名”。示例代码:

public class Function3Test {
    public static void main(String[] args) {
        Function3Test test3 = new Function3Test();
        /**
         * 在 Lambda 表达式中使用对象的方法时,[对象名::方法名]
         * 原写法:Example2 exam = (num1, num2) -> test3.compare(num1, num2);
         */
        Example2 exam = test3::compare;
        int result = exam.compareOperation(10, 20);
        System.out.println(result);
    }

    @FunctionalInterface
    interface Example2 {
        int compareOperation(int num1, int num2);
    }

    public int compare(Integer o1, Integer o2) {
        // 小于--负整数;等于--零;大于--正整数
        return o1.compareTo(o2);
    }
}

引用构造方法

如果表达式符合“([参数1, 参数2, ...]) -> new 类名([参数1, 参数2, ...])”,那么可以简化为“类名::new”。示例代码:

public class Function4Test {
    public static void main(String[] args) {
        /**
         * 通过[类名::new]的方式来实例化对象
         * 原写法:ProductCreator creator = (id, name, price) -> new Product(id, name, price);
         */
        ProductCreator creator = Product::new;
        Product p1 = creator.getProduct(1, "大棉袄", 365.00);
        System.out.println(p1.toString());
    }

    @FunctionalInterface
    interface ProductCreator {
        Product getProduct(int id, String name, double price);
    }
}

使用情景

以下举例说明一些常用的情景。

创建线程

可以使用 Lambda 表达式来简化线程的创建过程:

new Thread(() -> System.out.println("子线程启动了")).start();

遍历集合

传统遍历一个 List ,基本上都使用 for 循环来遍历,JDK 8 之后 List 有了 forEach() 方法,配合 Lambda 表达式写出更加简洁的方法:

public class Function5Test {
    public static void main(String[] args) {
        // 1.遍历列表
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list, 1,2,3,4,5,6,7);
        // 使用 Lambda 表达式遍历
        list.forEach(System.out::println);
    }
}

过滤元素

java.util.function 包下有一个断言型接口 Predicate<T>,其中包含了多种静态 / 默认方法,用于处理复杂的逻辑判断。示范代码:

public class Function5Test {
    public static void main(String[] args) {
        // 2.过滤元素
        List<String> lsit2 = Arrays.asList("Java", "PHP", "C++", "Golang", "Python", "Kotlin");
        Predicate<String> condition = (s) -> s.length() > 5;
        filter(lsit2, condition);
    }

    // 使用 Predicate 接口
    public static void filter(List<String> list, Predicate condition) {
        for (String content : list) {
            if (condition.test(content)) {
                System.out.println("符合条件的内容:" + content);
            }
        }
    }
}

使用 Lambda 表达式,还可以进一步简化:

public class Function5Test {
    public static void main(String[] args) {
        // 2.过滤元素
        List<String> lsit2 = Arrays.asList("Java", "PHP", "C++", "Golang", "Python", "Kotlin");
        // 使用 Lambda 表达式简化
        filter(lsit2, (s) -> (((String) s).length() > 5));
    }

    // 使用 Predicate 接口
    public static void filter(List<String> list, Predicate condition) {
        // 使用 Lambda 表达式简化
        list.stream().filter(condition).forEach((content) -> System.out.println("符合条件的内容:" + content));
    }
}

删除元素

ArrayList 类中有一个 removeIf(Predicate<E> filter) 方法可以删除所有满足特定条件的数组元素。

public class Function5Test {
    public static void main(String[] args) {
        // 3.删除满足条件的元素
        List<Product> proList = new ArrayList<Product>() {
            {
                add(new Product(1, "电动牙刷", 230.00));
                add(new Product(2, "品牌文化衫", 99.00));
                add(new Product(3, "无线鼠标", 177.00));
                add(new Product(4, "机械键盘", 418.00));
                add(new Product(5, "除螨仪", 199.00));
            }
        };
        // Predicate<E> filter,使用 Lambda 简化
        proList.removeIf(p -> p.getId() == 3);
        // 输出验证
        System.out.println("删除特定目标后的 ArrayList: " + proList.size());
    }
}

元素排序

在 Java 8 之前,通过实现 Comparator 接口,匿名内部类中重写 compare() 方法完成排序,我们现在可以使用 Lambda 表达式来简化代码。

public class Function5Test {
    public static void main(String[] args) {
        // 4.元素排序
        List<Product> proList2 = new ArrayList<Product>() {
            {
                add(new Product(1, "小米平板5", 2299.00));
                add(new Product(2, "米家电动剃须刀", 99.00));
                add(new Product(3, "Redmi路由器AX1800", 229.00));
                add(new Product(4, "小米中性笔", 24.90));
                add(new Product(5, "黑鲨4游戏手机", 2299.00));
                add(new Product(6, "米家加湿器", 99.00));
                add(new Product(7, "米家指甲刀", 9.90));
                add(new Product(8, "Redmi手表", 269.00));
                add(new Product(9, "RedmiK40游戏手机", 2299.00));
            }
        };
        // 自定义排序:先比较价格,再比较 id
        proList2.sort((s1, s2) -> {
            if (s1.getPrice() == s2.getPrice()) {
                return Integer.compare(s1.getId(), s2.getId());
            } else {
                return Double.compare(s1.getPrice(), s2.getPrice());
            }
        });
        // 输出验证
        proList2.forEach(System.out::println);
    }
}

从逻辑来看,多条件排序就是先判断第一个条件,如果相等,再判断第二个条件,依次类推。在 Java 8 中可以使用 comparing 和一系列 thenComparing 表示多级条件判断(thenComparing 方法是可以有多个的),上面的逻辑可以简化为:

// 使用 Lambda 表达式
proList2.sort(Comparator.comparing(Product::getPrice).thenComparing(Product::getId));

参考

菜鸟教程
极客教程
稀土掘金
打赏
评论区
头像
文章目录