Java---Lambda

学习Lambda的理由

  1. 绝大多数公司代码的主流风格。
  2. 大数据量下处理集合效率高,优秀的高并发解决。
  3. 代码的可读性增强。
  4. 消灭嵌套地狱。>形状的if或者for再也不用写了。

为了了解Lambda表达式,我们必须了解什么是函数式接口,这是Lambda表达式得以实现的依据。

在java中,函数式接口指注解了@FunctionalInterface(非必须)的接口。
函数式接口具有一下特性:

  • 接口中有且仅有一个抽象方法(必须)。

除却以上性质与普通的接口没有区别。

由以上定义,那么问题来了,为什么要使用函数式接口?

Lambda的演化

让我们先看一个接口的实现案例。

朴素写法:定义实现类

// 传统的接口实现方式
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

方法入参 -> 方法实现(方法体)

// lambda的接口实现方式
interface MyInterface {
    void test();
}
class Client {
    public void process() {
        MyInterface mif = () -> { /* 各种处理 */ }; // 上文test()的实现
        mif.test();
    }
}

代码量肉眼可见的减少,接下来拓展下还可以怎么写。

Lambda的省略规则

// 方法参数类型可以省略
(a, b) -> { /* 各种处理 */ };
// 方法体只有一行代码时,{}、;、return可省略(必须同时省略)
(a, b) -> /* 各种处理 */;
// 方法参数只有一个时,()可以省略
a -> /* 各种处理 */;
// 方法无参时,()不可省略
() -> /* 各种处理 */;

仔细观察上述的所有写法可以了解到lambda表达式与传统匿名内部类的实现方式有一个本质的区别:有且仅有一个实现的抽象方法。这也是为什么函数式接口只能定义一个抽象方法的原因。

除了上述的Lambda的写法以外还有一个更为简洁的写法:方法引用

方法引用

对象名::方法名
类名::静态方法名

我们看一个普通Lambda的写法

Consumer<String> s = t -> System.out.println(t);
s.accept("Hello World");

使用方法引用简化Lambda

Consumer<String> s = System.out::println;
/*
	理解方法引用与理解Lambda是一样的逻辑,可推导->可省略,反之亦然,可省略->可推导。
	首先,方法引用,引用的是什么?方法。方法的什么部分被引用了?
	Lambda需要方法的什么部分:形参列表,方法体。
	此处同理,实际上方法引用就是借其它已经定义的方法的形参列表与方法体。
	为了达到可推导的目的,引用的方法体有额外的一个要求:参数与接口的方法一致。
 */
s.accept("Hello World");

竟然比普通的Lambda还简洁。

方法引用条件

  • 引用的方法已经被定义。
  • 引用的方法形参与接口的方法一致。

补充

  1. lambda基本可以认为是匿名内部类的语法糖[2](不太准确)
    但lambda与匿名内部类在原理上有一个区别:

    • 匿名内部类会生成class文件

    • lambda不会生成class文件

  2. lambda中使用的局部变量必须声明为final或事实上是final(虽未使用final修饰,但变量只有一次赋值)

    String str = "Hello World"; // 事实final
    Consumer<String> s = t -> System.out.println(t + str); // 编译通过
    
    final String str = "Hello World"; // 显示final
    Consumer<String> s = t -> System.out.println(t + str); // 编译通过
    
    String str = "Hello World"; // 显示final
    str = "Hello";
    // Consumer<String> s = t -> System.out.println(t + str); // 编译报错,str不是一个final局部变量
    
  3. 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); // 朴素写法,如无必要不推荐,也别啥都使用函数式接口,根据自己的业务合理使用
        test(arr, (t) -> {
            if (t > 12523) return true;
            return false;
        });
        /*
            16254
            3547345
         */
    }
    public static void test(int arr[]) {
        for (int s : arr) {
            /*
            	此处条件为硬编码,会有很多问题,我们来看看
            	1. 如果上线后根据客户产品迭代的需要此处的边界数值需要更改怎么办?
            	   有基础的同学可能会想到将边界数值参数化,传入test方法即可。没错
            	2. 但如果变得不是或不只是边界数值,这里不再是>而是<怎么办,这可不增加入参哦。。
            	   如果使用Predicate就可以解决这个问题。详情见下方test重载。
             */
            if (s > 12523) {
                System.out.println(s);
            }
        }
    }
    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<T> and(Predicate<? super T> other)
作用:返回当前断言 && 入参断言。

满足当前断言和入参断言时,返回true。

public static void main(String[] args) {
    int arr[] = {1, 23, 135, 534, 6245, 16254, 3547345};
    Predicate<Integer> pre1 = (t) -> {
        if (t > 12523) return true;
        return false;
    };
    Predicate<Integer> pre2 = (t) -> {
        if (t < 300000) return true;
        return false;
    };
    // 等价于 t > 12523 && t < 300000 的条件
    test(arr, pre1.and(pre2));
    /*
        16254
     */
}
public static void test(int arr[], Predicate<Integer> pre) {
    for (int s : arr) {
        if (pre.test(s)) {
            System.out.println(s);
        }
    }
}

方法签名:Predicate<T> or(Predicate<? super T> other)
作用:返回当前断言 || 入参断言。

满足当前断言或者入参断言时,返回true。

public static void main(String[] args) {
    int arr[] = {1, 23, 135, 534, 6245, 16254, 3547345};
    Predicate<Integer> pre1 = (t) -> {
        if (t > 12523) return true;
        return false;
    };
    Predicate<Integer> pre2 = (t) -> {
        if (t < 300000) return true;
        return false;
    };
    // 等价于 t > 12523 && t < 300000 的条件
    test(arr, pre1.or(pre2));
    /*
        1
        23
        135
        534
        6245
        16254
        3547345
     */
}
public static void test(int arr[], Predicate<Integer> pre) {
    for (int s : arr) {
        if (pre.test(s)) {
            System.out.println(s);
        }
    }
}

方法签名:Predicate<T> negate()
作用:返回当前断言的取反。

public static void main(String[] args) {
    int arr[] = {1, 23, 135, 534, 6245, 16254, 3547345};
    Predicate<Integer> pre1 = (t) -> {
        if (t < 12523) return true;
        return false;
    };
    Predicate<Integer> pre2 = (t) -> {
        if (t > 300000) return true;
        return false;
    };
    // 等价于 t > 12523 && t < 300000 的条件
    test(arr, pre1.or(pre2).negate());
    /*
    	16254
        3547345
     */
}
public static void test(int arr[], Predicate<Integer> pre) {
    for (int s : arr) {
        if (pre.test(s)) {
            System.out.println(s);
        }
    }
}

其他方法

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

static <T> Predicate<T> not(Predicate<? super T> target) {
    Objects.requireNonNull(target);
    return target.negate();
}
  • isEqual:传入一个对象,当对象引用null时,返回isNull方法引用的Predicate,否则返回equals的Predicate
  • not:返回目标Predicate的negate

Function

方法签名:R apply(T var1)
作用:将T类型转为R类型,返回。

public class Main {
    public static void main(String[] args) {
        String str = "13523";
        Integer i = test(str, (t) -> {
            return Integer.parseInt(str);
        });
        System.out.println("i的value: " + i); // i的value: 13523
    }
    public static <T, R> R test(T t, Function<T, R> f) {
        return f.apply(t);
    }
}

default方法

方法签名:<V> Function<T, V> andThen(Function<? super R, ? extends V> after)
作用:将两个Function结合,返回。

先根据当前Function执行类型转换,然后再根据入参Function执行类型转换。

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> f1 = (t) -> {
            return Integer.parseInt(t);
        };
        Function<Integer, String> f2 = (t) -> {
            return String.valueOf(t);
        };
        String str = "13523";
        String str1 = test(str, f1.andThen(f2));
        System.out.println("str1的value: " + str1); // str1的value: 13523
        /*
        	String -> Integer -> String
         */
    }
    public static <T, R> R test(T t, Function<T, R> f) {
        return f.apply(t);
    }
}

方法签名:<V> Function<V, R> compose(Function<? super V, ? extends T> before)
作用:将两个Function结合,返回。

与andThen刚好相反,先根据入参Function执行类型转换,然后再根据当前Function执行类型转换。

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> f1 = (t) -> {
            return Integer.parseInt(t);
        };
        Function<Integer, String> f2 = (t) -> {
            return String.valueOf(t);
        };
        Integer i = 13523;
        Integer i1 = test(i, f1.compose(f2));
        System.out.println("i1的value: " + i1);
        /*
        	Integer -> String -> Integer
         */
    }
    public static <T, R> R test(T t, Function<T, R> f) {
        return f.apply(t);
    }
}

其他方法

static <T> Function<T, T> identity() {
    return (t) -> {
        return t;
    };
}
  • identity:返回一个和输入类型相同的输出类型。

总结

Lambda的特点

  • 语法只关注参数与方法体
  • 可推导即可省略

Lambda的作用

  • 优化部分匿名内部类的写法。(函数式接口)

关于Lambda在开发中的实际用法可以参考此链接: Java---Stream入门
尤其是函数式接口在Stream中的应用颇广。


  1. 有着可推导即可省略的特性。 ↩︎

  2. 使用方式更为简单,但原理不变。 ↩︎

posted @ 2022-08-27 19:35  spoonb  阅读(323)  评论(0编辑  收藏  举报