学习Lambda的理由
- 绝大多数公司代码的主流风格。
- 大数据量下处理集合效率高,优秀的高并发解决。
- 代码的可读性增强。
- 消灭嵌套地狱。>形状的if或者for再也不用写了。
为了了解Lambda表达式,我们必须了解什么是函数式接口,这是Lambda表达式得以实现的依据。
在java中,函数式接口指注解了@FunctionalInterface(非必须)的接口。
函数式接口具有一下特性:
除却以上性质与普通的接口没有区别。
由以上定义,那么问题来了,为什么要使用函数式接口?
让我们先看一个接口的实现案例。- // 传统的接口实现方式
- interface MyInterface {
- void test();
- }
- class MyInterfaceImpl implements MyInterface{
- public void test() {
- // 各种处理
- }
- }
- class Client {
- public void process() {
- MyInterface mif = new MyInterfaceImpl();
- mif.test();
- }
- }
复制代码 目前看上去我们的实现是没有什么问题的,无外乎定义一个接口,然后定义接口的实现类,在客户端调用的过程中父类引用子类对象。。。
但是,如果这么定义的话,意味着实现类是可以复用的。实际上应该反过来思考,就因为需要复用实现类,我们才这么定义接口与实现。
有基础的小伙伴肯定反应过来我想说的问题:如果一个接口的实现根据业务需求在项目中指调用两三次,并且每次的实现方式还不同,我需要为了这三个不一样的实现分别写三个不同的实现类吗?
其实没有必要,由此引出接下来的内容:匿名内部类实现接口。- // 匿名内部类的接口实现方式
- interface MyInterface {
- void test();
- }
- class Client {
- public void process() {
- MyInterface mif = new MyInterface() {
- @Override
- public void test() {
- // 各种处理
- }
- };
- mif.test();
- }
- /*
- 其实不难看出,对于只有一个抽象方法的接口,匿名内部类的实现方式比较冗余
- 首先new MyInterface(){}并不是非写不可,
- @Override,public,void,test也同样。
- 可能有同学看到这里已经蒙了:这人在说啥?我接下来解释。
- 接口只有一个抽象方法也就是说没有重载,所以我们通过明确写出方法的参数列(类型也不需要,可推定)
- 已经方法体就可以确定重写的是哪个方法,也就省去了@Override public void test。
- 又因为变量声明了一个接口类型所以就知道需要实例化的对象接口也就省去了new MyInterface(){}
- 至此代码变为了以下这种形式:
- MyInterface mif = () -> { /* 各种处理 */ };
- */
- }
复制代码 虽然上述的实现方式可以让我们比较方便的实现需求频繁变动且复用需求低的接口,但人类是非常懒的,为了让我们实现上述功能的同时写更少的代码,技术人员发明了lambda[1]表达式。
接下来,我们来看看上述实现使用lambda的效果。- // lambda的接口实现方式
- interface MyInterface {
- void test();
- }
- class Client {
- public void process() {
- MyInterface mif = () -> { /* 各种处理 */ };
- mif.test();
- }
- }
复制代码 代码量肉眼可见的减少,接下来解释为什么可以这么写。- MyInterface mif = () -> { /* 各种处理 */ };
复制代码
- ():接口抽象方法的形式参数。
- ->:箭头标记,固定写法。
- {}:实现的方法体。
lambda的省略规则- // 方法参数类型可以省略
- (a, b) -> { /* 各种处理 */ };
- // 方法体只有一行代码时,{}、;、return可省略(必须同时省略)
- (a, b) -> /* 各种处理 */;
- // 方法参数只有一个时,()可以省略
- a -> /* 各种处理 */;
- // 方法无参时,()不可省略
- () -> /* 各种处理 */;
复制代码 仔细观察上述的所有写法可以了解到lambda表达式与传统匿名内部类的实现方式有一个本质的区别:有且仅有一个实现的抽象方法。这也是为什么函数式接口只能定义一个抽象方法的原因。
补充
- lambda基本可以认为是匿名内部类的语法糖[2](不太准确)
但lambda与匿名内部类在原理上有一个区别:
- 匿名内部类会生成class文件
- lambda不会生成class文件
- lambda有着延迟执行的特点
- public Main {
- public static void main(String args[]) {
- String str1 = "你";
- String str2 = "是";
- String str3 = "谁?";
- // lambda的实现方式
- Test.doTest(true, () -> {
- System.out.println("满足条件时执行");
- return "lambda实现:"+str1+str2+str3;
- });
- // 匿名内部类的实现方式(同样有延迟执行的特点)
- Test.doTest(false, new Test() {
- @Override
- public void test() {
- System.out.println("满足条件时执行");
- return "匿名内部类实现:"+str1+str2+str3;
- }
- });
- // 常规写法
- doTest(false, str1+str2+str3);
- /*
- 满足条件时执行
- lambda实现:你是谁?
- */
- /*
- 通常情况下会使用常规写法,因为理解简单,但存在一个问题,
- 可以看出flag=false时,确实没有输出str,但str却提前拼接好,
- 导致性能的浪费。
- 为此使用lambda(推荐)或者匿名内部类的形式,将字符串的拼接
- 延迟到判定条件之后。
- */
- }
- public static void doTest(boolean flag, String str) {
- if (flag) {
- System.out.println(str);
- }
- }
- }
- interface Test {
- String test();
- static void doTest(boolean flag, Test t) {
- if (flag) {
- System.out.println(t.test());
- }
- }
- }
复制代码 核心函数式接口
Supplier
方法签名:T get()
作用:供应商接口。生成T类型的数据。- public class Main {
- public static void main(String[] args) {
- int nums[] = {1, 23, 135, 534, 6245, 16254, 3547345};
- Integer res = test(() -> {
- int max = -1;
- for (int num : nums) {
- max = Math.max(max, num);
- }
- return max;
- });
- System.out.println("最大值:" + res); // 最大值:3547345
- }
- public static <T> T test(Supplier<T> s) {
- return s.get();
- }
- }
复制代码 Consumer
方法签名:void accept(T t)
作用:消费者接口。使用T类型的数据。- // 格式化打印
- public class Main {
- public static void main(String[] args) {
- String strs[] = {"张三 男", "李四 男", "小红 女"};
- test(strs, (arr) -> {
- for (String str : arr) {
- String s[] = str.split(" ");
- System.out.println(s[0] + ":" + s[1]);
- }
- });
- /*
- 张三:男
- 李四:男
- 小红:女
- */
- }
- public static <T> void test(T t, Consumer<T> c) {
- c.accept(t);
- }
- }
复制代码 当想将上面的数据正向与逆向输出两遍怎么办?- // 正向与逆向格式化打印, 使用andThen
- public class Main {
- public static void main(String[] args) {
- String strs[] = {"张三 男", "李四 男", "小红 女"};
- Consumer<String[]> c1 = (arr) -> {
- System.out.println("------正序输出------");
- for (String str : arr) {
- String s[] = str.split(" ");
- System.out.println(s[0] + ":" + s[1]);
- }
- };
- Consumer<String[]> c2 = (arr) -> {
- System.out.println("------逆序输出------");
- for (int i = arr.length - 1; i >= 0; i --) {
- String s[] = arr[i].split(" ");
- System.out.println(s[0] + ":" + s[1]);
- }
- };
- test(strs, c1.andThen(c2));
- /*
- ------正序输出------
- 张三:男
- 李四:男
- 小红:女
- ------逆序输出------
- 小红:女
- 李四:男
- 张三:男
- */
- }
- public static <T> void test(T t, Consumer<T> c) {
- c.accept(t);
- }
- }
复制代码 由此看出两种写法等效,并且可以看出andThen可以链接两个Consumer的处理变为一个处理,或者说一起处理。当需要链接的Consumer数量不定时,有非常大的作用。传入的参数只需如下即可。- // c1,c2,c3,c4,c5均为Consumer实例
- // 如此一来就可以连续执行c1,c2,c3,c4,c5的处理了
- test(strs, c1.andThen(c2).andThen(c3).andThen(c4).andThen(c5));
复制代码 Predicate
方法签名:boolean test(T t)
作用:断言接口。封装判断语句。
其实Predicate就是将条件语句打包成一个类,减少编程时传参的麻烦,同时使得条件语句也可以延迟执行。- public class Main {
- public static void main(String[] args) {
- int arr[] = {1, 23, 135, 534, 6245, 16254, 3547345};
- test(arr, (t) -> {
- if (t > 12523) return true;
- return false;
- });
- /*
- 16254
- 3547345
- */
- }
- public static void test(int arr[], Predicate<Integer> pre) {
- for (int s : arr) {
- // 用pre.test(s)来代替条件语句
- if (pre.test(s)) {
- System.out.println(s);
- }
- }
- }
- }
复制代码 default方法
方法签名:Predicate and(Predicate |