java JDK8新特性 - 2.lambda表达式

1.lambda表达式 - 概念

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。

下面我们使用Lambda表达式看一下和以前的写法有什么不同

// 假设:我需要创建一个线程执行一些操作
// 1.不使用lambda表达式时:
new Thread(new Runnable() {
    @Override
    public void run() {
           System.out.println("当前时间戳:" + System.currentTimeMillis());
    }
}).start();

// 2.使用lambda表达式时:
new Thread(() -> System.out.println("当前时间戳:" + System.currentTimeMillis())).start();

结论:
lambda表达式可以简化代码,提升开发效率
在后面的学习中我们还会了解到lambda表达式可以作为函数的参数或返回值,这种特性在函数式编程中称为“高阶函数”
补充:

  1. lambda表达式每次创建的对象都是新的,见下面测试1
  2. lambda表达式不能用Object来声明,因为它没办法判断实现的是哪个接口,所以要么用函数式接口声明,要么强转类型,见下面测试2

// 测试1
Runnable r1 = () -> System.out.println("123");
Runnable r2 = () -> System.out.println("123");
System.out.println("r1 == r2, 结果:" + (r1 == r2)); // 结果是false

// 测试2,使用Object声明,编译报错:Target type of a lambda conversion must be an interface
// Object obj = () -> System.out.println("123");
Object obj2 = (Runnable) () -> System.out.println("123");

2.lambda表达式常见的4中写法

// 写法1:多参数、方法体内多行时这样使用
(i1, i2) -> {
    System.out.println(i1 + i2);
    return i1 + i2;
};

// 写法2:方法体内单行时省略大括号及return关键字
(i1, i2) -> i1 + i2;

// 写法3:在参数前可以写参数类型,但我们基本都会省略掉参数类型
(long i) -> i * 2;

// 写法4:单参数时省略小括号
i -> i * 2;

3.函数式接口

函数式接口(Functional Interface),也可以叫函数接口,就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

函数式接口是一种单一函数契约。属于单一责任制,一个接口只做一件事情。

@FunctionalInterface 注解作用:表示这是一个函数接口,接口内只能有一个待实现函数,如果有多个待实现函数时会编译错误

@FunctionalInterface
public interface Lambda03 {
    int doubleNum(int i);
//    定义多个接口时,注解位置会提示编译错误
//    int addNum(int i1, int i2);
}

4.接口的默认方法

Java 8 引入了新的语言特性 —— 默认方法(Default Methods)
1.默认方法允许您添加新的功能到现有的库的接口中,并能确保与采用旧版本的接口编写的代码的二进制兼容性.
2.默认方法是在接口中的方法签名前加上 default 关键字的实现方法.
3.默认方法就是可以在接口中定义一个已实现方法,且该接口的实现类不需要实现该方法;

补充:函数式接口可以有多个非抽象方法,非抽象方法指的就是默认方法和静态方法
Java8引入了新特性,接口中也可以写静态方法,并且要求必须有方法体

@FunctionalInterface
public interface Lambda04 {

    /**
     * 默认方法写法:
     * 使用关键字 default
     */
    default int add2(int i) {
        return i + 2;
    }
    default int add3(int i) {
        return i + 3;
    }

    // 抽象方法
    int doubleNum(int i);

}

5.接口的默认方法冲突

如果接口继承了2个含有默认方法的接口,并且两个接口的默认方法名冲突时,会编译报错,需要重写方法

public interface Lambda05 extends D123, D456 {
    // 首先要重写方法
    @Override
    default int add(int i1, int i2) {
        // 使用指定父类接口的默认方法来实现
        // <Interface>.super.<method>();
        return D123.super.add(i1, i2);
    }
}

interface D123 {
    default int add(int i1, int i2) {
        return i1 + i2;
    }
}
interface D456 {
    default int add(int i1, int i2) {
        return i1 * i2;
    }
}

6.函数接口与lambda表达式使用

public class Lambda06 {

    public static void main(String[] args) {
        // 现在测试使用下面的内部类,创建一个我的钱包,并打印金额
        MyMoney mm = new MyMoney(999999999);
        mm.printMyMoney(i -> new DecimalFormat("#,###").format(i));
        // 打印结果:我的存款是:999,999,999


        /**
         * Function<T, R> 函数接口还提供了几个默认方法
         * 其中一个是:default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
         * 用途是可以实现链式操作,在lambda表达式结果上进一步处理
         *
         * 参数:Function<? super R, ? extends V> after
         * 泛型R:Function<T, R>接口apply()函数的返回值类型
         * 泛型V:返回的函数接口(Function<T, V>)apply()函数的返回类型
         *
         * 返回:Function<T, V>
         * 泛型T:Function<T, R>接口的入参类型
         * 泛型V:返回的函数接口(Function<T, V>)apply()函数的返回类型
         */
        // 测试在金额前加(人民币)
        System.out.println("-------------------------------------------------------------");
        Function<Integer, String> moneyFormat = i -> {
            System.out.println("执行apply:" + System.currentTimeMillis());
            return new DecimalFormat("#,###").format(i);
        };
        mm.printMyMoney(moneyFormat.andThen(s -> {
            System.out.println("执行andThen:" + System.currentTimeMillis());
            return "(人民币)" + s;
        }));


        System.out.println("-------------------------------------------------------------");
        // function1的apply()实现
        Function<Integer, Integer> function1 = i -> i + 2;
        // 先执行function1的i + 2,再执行andThen的i * 3
        Integer apply = function1.andThen(i -> i * 3).apply(2);
        System.out.println("(2 + 2) * 3 = " + apply);
        // 分成2步操作,实际我们调用的是function2.apply(2),函数内部会调用function1的apply()
        Function<Integer, Integer> function2 = function1.andThen(i -> i * 3);
        Integer apply1 = function2.apply(2);
        System.out.println("分解:(2 + 2) * 3 = " + apply1);
    }

}

/**
 * 定义一个“我的钱包”实体类
 * 属性有金额
 * 方法有打印金额,参数是函数接口
 */
class MyMoney {

    private int money;

    public MyMoney(int money) {
        this.money = money;
    }

    /**
     * 因为lambda表达式并不关注接口是什么,方法是什么,只关传参和返回
     * 因此java提供了一些通用的函数接口,其中就有 Function<T, R>,T是传参类型,R是返回类型
     * 接口内待实现抽象方法为 R apply(T t);
     */
    public void printMyMoney(Function<Integer, String> moneyFormat) {
        System.out.println("我的存款是:" + moneyFormat.apply(this.money));
    }
}

7.java8提供的几种函数接口

public class Lambda07 {

    public static void main(String[] args) {
        /**
         * 断言:Predicate<T>
         * 抽象方法:boolean test(T t);
         *
         * 测试案例:生成一个通用编号,标准是不能为null,不能为空,长度必须大于4
         * 在实际项目中可以测试编号是否重复等问题
         * 这里使用了断言接口后可以保证我拿到的值一定是符合我标准的
         */
        Predicate<String> predicate = s -> s != null && !"".equals(s) && s.length() > 4;
        System.out.println("断言测试 - 生成通用编号:" + createCommonCode(predicate));

        /**
         * java提供了自带类型的函数接口,使用时建议优先使用自带类型的函数接口
         * 自带类型函数接口都在java.util.function包下面
         *
         * 这里用断言来举例:
         * Predicate<Integer>       使用 IntPredicate
         * Predicate<Long>          使用 LongPredicate
         * Predicate<Double>        使用 DoublePredicate
         */
        IntPredicate intPredicate = i -> i > 0;
        LongPredicate longPredicate = l -> l > 0;
        DoublePredicate doublePredicate = d -> d > 0;

        /**
         * 消费一个数据:Consumer<T>
         * 抽象方法:void accept(T t);
         *
         * 测试案例:我需要调用函数保存一个map到数据库,函数执行完我需要做一些操作
         * 简单来说可以作为回调函数使用
         */
        System.out.println("--------------------------------------------------------------------------------");
        Consumer<Map<String, Object>> consumer = m -> {
            System.out.println("我要保存的map保存完了,这是消费接口");
            System.out.println("我要保存的map保存完了,这是消费接口,map的id = " + m.get("id"));
        };
        Map<String, Object> map = new HashMap<>();
        map.put("name", "张三");
        map.put("gender", "男");
        saveMapToDatabase(map, consumer);

        /**
         * 输入T输出R的函数:Function<T, R>
         * 抽象方法:R apply(T t);
         *
         * 测试案例:这个在lambda06...中已经演示过了,属于用户自定义的函数接口
         */
        Function<Integer, String> function = i -> i.toString();

        /**
         * 提供一个数据:Supplier<T>
         * 抽象方法:T get();
         */
        Supplier<Date> supplier = () -> new Date();

        /**
         * 一元函数(输出输入类型相同):UnaryOperator<T>
         * 抽象方法:R apply(T t);   // 继承自Function<T, R>的抽象方法
         * 静态方法:identity()该方法返回一个UnaryOerator对象,并且apply()方法中直接返回范型对象。
         *  static <T> UnaryOperator<T> identity() {
         *      return t -> t;
         *  }
         *
         * Java8引入了新特性,接口中也可以写静态方法,并且要求必须有方法体(见下方定义的测试接口)
         */
        System.out.println("--------------------------------------------------------------------------------");
        UnaryOperator<String> unaryOperator = s -> "(前缀)" + s;
        System.out.println("一元函数 - 测试在参数前加前缀:" + unaryOperator.apply("123456"));
        // identity()该方法返回一个UnaryOerator对象,并且apply()方法中直接返回范型对象。
        UnaryOperator<Object> identity = UnaryOperator.identity();
        System.out.println("一元函数 - 测试静态方法提供的对象实例:" + identity.apply("456789"));

        /**
         * 2个输入的函数:BiFunction<T, U, R>
         * T:第一个参数类型;U:第二个参数类型;R:返回值类型
         * 抽象方法:R apply(T t, U u);
         */
        BiFunction<String, Integer, Boolean> biFunction = (s, i) -> Integer.parseInt(s) == i;

        /**
         * 二元函数(输出输入类型相同):BinaryOperator<T>
         * 抽象方法:R apply(T t, U u);  // 继承自BiFunction<T, U, R>的抽象方法
         */
        BinaryOperator<Integer> binaryOperator = (i1, i2) -> i1 + i2;
    }


    /**
     * 生成通用编号
     * @param predicate 断言接口,用于检测编号是否合格(合格返回true)
     */
    public static String createCommonCode(Predicate<String> predicate) {
        String code;
        boolean codeIsOk = true;
        do {
            // 生成编号
            code = "BH-" + (int) (Math.random() * 15);
            // 使用断言接口检测编号是否合格,不合格时重新生成编号
            codeIsOk = predicate.test(code);
            System.out.println("生成通用编号函数:code = " + code + ",codeIsOk = " + codeIsOk);
        } while (!codeIsOk);

        return code;
    }


    /**
     * 保存map到数据库(生成map记录的ID)
     * @param map 要保存的map
     * @param consumer 消费接口(这里相当于回调函数)
     */
    public static void saveMapToDatabase(Map<String, Object> map, Consumer<Map<String, Object>> consumer) {
        // 生成map记录的ID
        map.put("id", "ID-" + (int) (Math.random() * 100));
        // 这里假设保存到数据库了
        System.out.println("保存map到数据库 - 已成功保存map到数据库");

        // 使用消费接口
        consumer.accept(map);
    }

}

/**
 * Java8引入了新特性,接口中也可以写静态方法,并且要求必须有方法体
 */
interface TestDog {
    // 无方法体时报错
//    static void wang();
    // 有方法体时编译成功
    static void wang() {
        System.out.println("wang wang");
    }
}

8.方法引用

方法引用通过方法的名字来指向一个方法。
方法引用可以使语言的构造更简洁紧凑,减少冗余代码。
方法引用使用一对冒号 :: 。

public class Lambda08 {

    public static void main(String[] args) {
        // 正常lambda表达式写法
        Consumer<String> consumer = s -> System.out.println(s);
        consumer.accept("正常lambda表达式写法:s -> System.out.println(s)");

        /**
         * 当lambda表达式的方法体中只调用了一个函数,并且lambda表达式的参数与方法体内函数的参数相同时
         * 可以使用方法引用的写法:对象::方法名
         * 表示我要调用指定对象的指定方法,参数就是lambda表达式的参数
         *
         * 注意:我们在写lambda表达式尽量使用方法引用,如果我们自己写lambda表达式,java就会生成一个类似lambda$0这样的函数存放在堆栈里
         * 如果我们使用了方法引用,lambda表达式在执行时会根据方法引用调用其他类或对象的方法,可以节约堆栈的空间
         *
         * 一篇很好的文章:从Presto堆栈讲解含有lambda表达式堆栈分析方法
         * https://blog.csdn.net/zhanyuanlin/article/details/96743774
         * 文章中只提到了lambda表达式会在堆栈生成函数,但没有明确是堆是栈,函数理论上讲是放在栈中
         */
        Consumer<String> consumer1 = System.out::println;
        consumer1.accept("使用了方法引用的写法:System.out::println");

        /**
         * 静态方法的方法引用
         * 写法:类名::静态方法名
         */
        System.out.println("----------------------------------------------------------------------");
        Consumer<Dog> consumer2 = dog -> Dog.bark(dog);
        Consumer<Dog> consumer3 = Dog::bark;
        consumer2.accept(new Dog("哮天犬2"));
        consumer3.accept(new Dog("哮天犬3"));

        /**
         * 对象实例的方法引用
         * 写法:对象::方法名
         * 因为这里要调用的方法是public int eat(int num),参数和返回类型都是int
         * 函数接口要使用int类型的一元函数接口:IntUnaryOperator
         *
         * 表示调用指定对象的指定方法,参数就是lambda表达式的参数,方法返回值作为lambda表达式方法体内的返回值
         */
        System.out.println("----------------------------------------------------------------------");
        Dog dog = new Dog("哮天犬4");
        IntUnaryOperator unaryOperator4 = dog::eat;
        // 这里有个知识点:上面使用方法引用定义lambda表达式时使用了dog变量,我在这里将其设为null,在调用函数接口的函数是否会报错呢?
        // 答案时不会,dog变量虽然为null了,但不代表之前的dog在堆(java堆栈的堆)里消失了,函数接口会调用这个dog对象来完成函数调用。
        dog = null;
        System.out.println("吃了3斤狗粮,还剩" + unaryOperator4.applyAsInt(3) + "斤狗粮。");

        // 这里如果使用普通的lambda表达式,会编译报错,内部类访问外部变量时必须是final变量或未修改过的变量
//        IntUnaryOperator unaryOperator44 = i -> dog.eat(1);

        /**
         * 对象实例的方法引用还有一种写法
         * 写法:类名::方法名
         * 方法需要传入两个参数,第一个是对象实例,第二个是原本的参数
         *
         * 解释:JDK默认会把当前实例传入到非静态方法,参数名为this,位置是第一个参数
         * 当我们声明一个方法:public int eat(int num)
         * 其实java底层大概是这样的:public int eat(Dog this, int num)
         * 使用一些工具查看字节码文件时,可以清晰看到eat方法有2个参数
         * 无论我们使用哪种写法都是ok的(Dog的eat方法可以提供测试,无论哪种写法都对代码没有任何影响)
         */
        System.out.println("----------------------------------------------------------------------");
        dog = new Dog("哮天犬5");
        BiFunction<Dog, Integer, Integer> biFunction5 = Dog::eat;
        System.out.println(dog + "吃了5斤狗粮,还剩" + biFunction5.apply(dog, 5) + "斤狗粮。");
        // 虽然eat定义了2个参数,但使用对象调用时依然可以传一个参数
        dog.eat(2);

        /**
         * 构造方法的方法引用
         * 写法:类名::new
         */
        System.out.println("----------------------------------------------------------------------");
        // 无参构造的方法引用
        Supplier<Dog> supplier6 = Dog::new;
        System.out.println("我使用无参构造方法创建了新的狗:" + supplier6.get());
        // 有参构造的方法引用,JDK会自动匹配入参为String类型的构造方法
        Function<String, Dog> function7 = Dog::new;
        System.out.println("我使用有参构造方法创建了新的狗:" + function7.apply("酷狗"));
    }

}

// 用于测试 “静态方法”和“对象实例”的方法引用
class Dog {

    private String name;        // 狗名
    private int food = 10;      // 当前狗粮斤数

    public Dog() {
        this.name = "默认狗";
    }
    public Dog(String name) {
        this.name = name;
    }

    public static void bark(Dog dog) {
        System.out.println(dog + "叫了");
    }

    /**
     * 吃狗粮
     *
     * @param num 吃的斤数
     * @return 还剩下多少斤狗粮
     */
//    // 提供双参数测试
//    public int eat(Dog this, int num) {
    public int eat(int num) {
        System.out.println(this.name + "吃了" + num + "斤狗粮");
        this.food -= num;
        return this.food;
    }

    @Override
    public String toString() {
        return this.name;
    }

}

9.类型推断

public class Lambda09 {

    public static void main(String[] args) {
        /**
         * lambda表达式实际返回的是实现指定接口的对象实例
         * 那么lambda表达式实现了哪个接口呢?这就需要用到“类型推断”了
         * java有以下几种类型推断:
         */
        // 变量类型
        IMath lambda = (x, y) -> x + y;
        // 注:前面我们使用过很多次用lambda表达式作为函数参数,实际就是使用变量类型推断实现了哪个函数接口
        // 注:当使用lambda表达式作为重载方法的参数时,如果重载方法参数极其相似(java不能自动推断实现哪个接口),则必须强制转换类型,否则编译报错
//        test((x, y) -> x + y);        // 参数极其相似时,不强转会编译出错
        test((IMath) (x, y) -> x + y);  // 参数极其相似时,强制转换类型后编译成功
        test2((x, y) -> x + y);         // 参数相似,但java依然可以自动推断实现哪个接口

        // 数组类型
        IMath[] lambdas = { (x, y) -> x + y };

        // 强制转换类型
//        Object lambda2 = (x, y) -> x + y; // 不强转会编译出错
        Object lambda2 = (IMath) (x, y) -> x + y;

        // 通过函数返回类型
        IMath iMath = createIMath();
    }

    public static IMath createIMath() {
        return (x, y) -> x + y;
    }

    public static void test(IMath imath) {}
    public static void test(IMath2 imath2) {}

    public static void test2(IMath imath) {}
    public static void test2(IMath3 imath3) {}
}

@FunctionalInterface
interface IMath {
    int add(int x, int y);
}

@FunctionalInterface
interface IMath2 {
    int sub(int x, int y);
}

@FunctionalInterface
interface IMath3 {
    String print(int i);
}

10.变量引用

public class Lambda10 {

    public static void main(String[] args) {
        /**
         * lambda表达式实际返回的是实现指定接口的对象实例
         * 我们知道接口是不能创建实例的,所以实际中间还夹着一个匿名内部类
         * 关系:对象实例 > 类型为匿名内部类 > 实现函数接口
         *
         * jdk8之前:匿名内部类访问外部变量时,变量必须使用final修饰
         * jdk8之后:匿名内部类访问外部变量时,变量还是需要使用final修饰,不过final我们可以不用写,但变量不能进行修改
         *
         * 使用idea编辑器,在匿名内部类中使用了变量,该变量会有特殊的颜色以及下划线等样式的不同,你们可以试试
         */
        String str = "我们的时间";
        // 这里修改str变量时,下行代码编译报错(Variable used in lambda expression should be final or effectively final)
        // 编译报错中文意思是(在lambda表达式中使用的变量应该是final修饰的 或 实际没有修改过的变量)
//        str = "我们的茶壶";
        Consumer<String> consumer = s -> System.out.println(s + str);
        consumer.accept("2020-12-17");

        /**
         * 为什么匿名内部类访问外部变量时必须是final修饰的呢?(以下为个人观点)
         * 见下面示例:
         * list是一个变量,匿名内部类中使用了list变量,此时外部的list和匿名内部类中的list两个变量指向了同一个对象
         * 如果下行代码我修改了list的值,此时匿名内部类中的list变量还是指向原来的对象
         * java为了防止这种情况发生,定义匿名内部类访问外部变量必须使用final修饰
         */
        List<String> list = new ArrayList<>();
        Consumer<String> consumer2 = s -> System.out.println(s + list);
//        list = new LinkedList<>();
        consumer2.accept("2020-12-18");
    }

}

11.级联表达式和柯里化

public class Lambda11 {

    public static void main(String[] args) {
        /**
         * 级联表达式
         * 有多个箭头的lambda表达式称为“级联表达式”
         * 例:x -> y -> x + y
         * 外层:参数为x,返回为函数接口
         * 里层:函数接口的参数为y,返回为x + y的值
         */
        Function<Integer, IntUnaryOperator> function = x -> y -> x + y;
        System.out.println("使用级联表达式计算:2 + 3 = " + function.apply(2).applyAsInt(3));

        /**
         * 柯里化
         * 把多个参数的函数转换为只有一个参数的函数称为“柯里化”
         * 柯里化的目的:函数标准化(函数标准化后可以让函数调用更灵活)
         */
        Function<Integer, Function<Integer, Function<Integer, Integer>>> function1 = x -> y -> z -> x + y + z;

        int[] nums = { 2, 3, 4 };
        Function fun = function1;
        for (int num : nums) {
            if (fun instanceof Function) {
                Object obj = fun.apply(num);
                if (obj instanceof Function) {
                    fun = (Function) obj;
                } else {
                    System.out.println("使用级联表达式计算:2 + 3 + 4 = " + obj);
                }
            }
        }
    }

}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值