java篇 常用工具类 0x04:lambda

2024-08-31 11:28
文章标签 java 常用工具 lambda 0x04

本文主要是介绍java篇 常用工具类 0x04:lambda,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • lambda 的使用前提(限制)
    • lambda 的使用形式
    • lambda实际工作流程
      • 用 Exception 报错来追踪 lambda 调用流程
      • 自己定义能写成 lambda 格式的方法与接口
    • stream() 与 collect()
    • lambda还可以抛出异常
    • lambda 的精髓
    • lambda 使用建议

在 java 中,类是一等公民,即什么东西都离不开类,一定得先有类,然后再往类里面放各种方法和属性。

而 lambda 是函数式编程,函数(方法)是一等公民,无需依附于任何其他元素即可存在,并可以作为 参数返回值。(java 中,方法是必须要依附于某个类的,而现在 java 也支持 lambda 了)。

lambda 的使用前提(限制)

使用 lambda 要满足以下条件:

  1. 这个方法的形参是一个接口的实例
  2. 这个作为形参的接口实例,只包含一个待实现的抽象方法

lambda 可以取代的是只有一个待实现的抽象方法的接口(接口除了这个待实现的抽象方法,倒是可以另外有多个 “有缺省实现” 的抽象方法)

lambda 的使用形式

  • 完整形式:方法名((形参列表)->{抽象方法的方法体})
  • 单形参,单语句:方法名(形参1->抽象方法的方法体单语句)
    • 类型.静态方法:方法名(类型::静态方法)
    • 实例.实例方法:方法名(实例::实例方法)
    • 前一个方法的结果实例的类型.实例方法:方法名(结果类型::实例方法)
  • 多参数,单语句:方法名((形参1,形参2)->抽象方法的方法体单语句)
    • 类型.静态方法:方法名(类型::静态方法)
    • 实例.实例方法:方法名(实例::实例方法)

具体案例:可以对比一下 lambda 和 匿名内部类 的差异。

public class Cat {  public void sayHello(){  System.out.println("Hello, I'm a cat.");  }  
}import java.util.*;  
import java.util.function.Consumer;  public class LambdaTest {  public static void main(String[] args) {  List<String> myList = addElementToList(new ArrayList<>());  String outside = "outside string";  // 匿名内部类版  myList.forEach(new Consumer<String>() {  @Override  public void accept(String s) {  processString(outside + s);  }  });  /*  输出结果:  outside stringstr0  outside stringstr1  outside stringstr2  ...  outside stringstr20  outside stringstr21  */  /*  .forEach 是 Iterable 接口里的抽象方法,有默认实现:  根据代码含义,行为就是迭代Iterable的实例自身,分别传给 Consumer 接口的实例方法 .accept()  default void forEach(Consumer<? super T> action) {  Objects.requireNonNull(action);  for (T t : this) {  action.accept(t);  }  }  而 Consumer 接口只包含两个抽象方法,其中还有一个是有默认实现的,另一个(.accept())则没有默认实现,所以必须覆盖实现  public interface Consumer<T> {  void accept(T t);  default Consumer<T> andThen(Consumer<? super T> after) {  Objects.requireNonNull(after);  return (T t) -> { accept(t); after.accept(t); };  }  }  所以,上面匿名内部类的写法,其实就是将 myList 的元素迭代地传给 Consumer 的实例,使用其中的 .accept() 方法进行处理(调用静态方法 processString() 来打印字符串)。  其作用等同于:  for (String s : myList){  processString(outside + s);  }  区别在于使用 Consumer接口,是将数据传给 Consumer 去处理,而不是自己写迭代处理逻辑,也就是将原来的处理逻辑封装到 Consumer 的 accept() 方法中去了。  */  // lambda的表达式必须能够符合接口中定义的抽象方法(从参数,到返回值,到异常都必须匹配)  // lambda 完整版 (参数)->{方法体}  // lambda 可以有返回值,使用 return 语句即可(要与接口的方法一致,该方法有返回值就要有return,没有返回值就不能有return),  // 当前 forEach 参数所对应的接口的抽象方法 accept() 返回值是 void,所以没有返回值,也就不能有 return 语句。  // lambda 可以使用外部数据,怎么看都和匿名内部类是一样的  myList.forEach((s) -> {  processString(outside + s);  });  // 没有类名,没有方法名,直接就是参数 s(甚至参数类型都不用管) ,用箭头 -> 指明要去执行一个代码块{}。当然这里 s 也是随意命名的变量,命名为 e 或者其他任意名 foo 之类的都是可以的。  // 至于这个 s(参数) 的类型实际就是 forEach 迭代出来的元素的类型,到底是只有一个参数,还是多个参数,要看 forEach 调用接口实例的 accept() 方法是传入的参数个数,这里是 1 个。  // 所以使用 lambda 这里面就有隐藏的限制 —— 这个作为参数的接口仅有一个待实现的抽象方法(待实现的只能有一个,但除了这个待实现的,可以同时有多个有默认实现的抽象方法)  // 因为只有这样,java 才知道这个 lambda 是要找到接口的哪个抽象方法,才可以不用管方法名和参数类型(因为只有这个抽象方法)  /*  输出结果:  outside stringstr0  outside stringstr1  outside stringstr2  ...  outside stringstr20  outside stringstr21  */  // lambda 单参数,代码单行,可以进一步简化:(省去参数外面的括号、省去代码块外面的大括号)  myList.forEach(s -> processString(outside + s));  /*  输出结果:  outside stringstr0  outside stringstr1  outside stringstr2  ...  outside stringstr20  outside stringstr21  */  /*如果代码单行,且这一行是 return 语句,比如 return 123;myList.forEach(s->{return 123;});或myList.forEach(s->123);    // 即可以省略 return,但也得同时省略掉最后的分号*/// lambda 如果不使用外部变量(或对这个传入参数作进一步的提前处理,如 1+s ,因为下面的形式就只能直接传参了),还可以进一步终极简化(直接用方法引用):  myList.forEach(LambdaTest::processString);  // 等价于:myList.forEach(s -> processString(s));  // 这里往深理解一层就是将 .forEach()迭代的元素(结果)逐个喂给 LamdaTest.processString() 方法  // 这里的省略就意味着从 .forEach()迭代的元素(结果)不能在喂给LamdaTest.processString() 方法前(这个中间阶段)作进一步的处理了  /*  输出结果:  str0  str1  str2  ...  str19  str20  str21  */  // lambda 方法引用终极简化版也并不局限于用类名调用静态方法,用引用调用实例方法也是可以的  LambdaTest inst = new LambdaTest();  myList.forEach(inst::processStringInst);  // myList.forEach(inst::processString); // 报错:java: 方法引用无效 静态限制范围方法引用。即无法用实例引用去调用静态方法。  /*  输出结果:  str0  str1  str2  ...  str19  str20  str21  */  // lambda 也允许指定一个参数类型里的一个方法作为方法的引用  myList.forEach(String::toUpperCase);  // 这次的不同之处在于,toUpperCase() 是一个实例方法,怎么可以用类型名 String 去调用呢?这是因为 myList.forEach() 迭代出的元素(结果)是 String,  // 所以这种情况 java 允许这种“类型名::实例方法”的形式  // 下面这种就会报错,因为 forEach 迭代出来的并不是 Integer,所以这样写是不允许的  // myList.forEach(Integer::toString); // 报错:java: 方法引用无效 无法从静态上下文中引用非静态 方法 toString()List<Cat> myCatList = new ArrayList<>();  myCatList.forEach(Cat::sayHello); // 实际上没有任何输出,因为此时 myCatList 里什么元素都没有,但也没有报错,正常运行。但实际上,当里面没有元素时,是不会真的去调用sayHello()的,它只会什么都不做。  myCatList.add(new Cat());  myCatList.forEach(Cat::sayHello); // Hello, I'm a cat.  // myCatList.forEach(String::toUpperCase); // 报错:java: 方法引用无效 无法从静态上下文中引用非静态 方法 toUpperCase()// myCatList.forEach(s -> processString(s)); // 报错:java: 不兼容的类型: lambdatest.Cat无法转换为java.lang.String  /*  但要注意,虽然下面 3 种写法都是允许的  类型::静态方法  实例::实例方法  迭代类型::迭代类型的实例方法  但是不要混淆,这些语句始终只是作为单语句的方法体的内容,是封装在 accept() 里面的,而不是凭空越过 Consumer 的 accept() 的无主方法体,  不是说所有的java方法,都可以在形参中使用 lambda,它是需要满足以下两个条件:  1. 这个方法的形参是一个接口的实例  2. 这个作为形参的接口实例,只包含一个待实现的抽象方法  此时才可以使用 lambda。  而使用 lambda 要注意,传入的参数是这个方法迭代的结果,然后将结果传给这个接口的抽象方法,所以传给这个抽象方法的参数个数,要与lambda的参数个数要对应。  上面  for (T t : this) {  action.accept(t);  }  所以,只需要一个参数就可以了,但下面这个例子,就涉及到两个参数,此时就必须得传入两个,而不是说我迭代出两个结果,我只想传 1 个都行。  Map 里的 forEach:  default void forEach(BiConsumer<? super K, ? super V> action) {  Objects.requireNonNull(action);  for (Map.Entry<K, V> entry : entrySet()) {  K k;  V v;  try {  k = entry.getKey();  v = entry.getValue();  } catch(IllegalStateException ise) {  // this usually means the entry is no longer in the map.  throw new ConcurrentModificationException(ise);  }  action.accept(k, v);  }  }  重点关注:action.accept(k, v);  所以这个 forEach 传给 accept 的是两个参数,那么用 lambda 形式,也就必须要传入两个参数  */  Map<String,String> myMap = new HashMap<>();  myMap.put("k1","v1");  myMap.put("k2","v2");  myMap.put("k3","v3");  // 两个参数也没问题,把参数用括号括起来,用逗号分隔  myMap.forEach((k,v)->processTwoStrings(k,v));  /*  k,v 倒不要求与 Map 里面 forEach 的名字一样,完全可以改叫 a,b,只是这样的话,后面 processTwoStrings(a,b) 里也要改成 a,b即:myMap.forEach((a,b)->processTwoStrings(a,b));  而以下两种改法都会报错:java 找不到符号  myMap.forEach((a,b)->processTwoStrings(k,v));  myMap.forEach((k,v)->processTwoStrings(a,b));  也就是名字对应就好。  */  /*  输出结果:  k1v1  k2v2  k3v3  */  // 同样可以省略参数  myMap.forEach(LambdaTest::processTwoStrings);  /*  但如果不省略参数,两个参数就不能省略外面的小括号了,即下面这么写是会报错的:  myMap.forEach((k,v->processTwoStrings(k,v));  */  /*  输出结果:  k1v1  k2v2  k3v3  */  // 同样,用引用调用实例方法也是可以的myMap.forEach(inst::processTwoStringsInst);  /*  输出结果:  k1v1  k2v2  k3v3  */// 只传入1个参数就会报错  // myMap.forEach((k)->processString(k)); // 报错:java: 不兼容的类型: lambda 表达式中的参数类型不兼容  }  private void processTwoStringsInst(String s, String s1) {  System.out.println(s+s1);  }private static void processTwoStrings(String s, String s1) {  System.out.println(s+s1);  }  private void processStringInst(String str) {  System.out.println(str);  }  private static void processString(String str) {  System.out.println(str);  }  private static List<String> addElementToList(List<String> list) {  for (int i = 0; i < 22; i++) {  list.add("str" + i);  }  return list;  }  
}

lambda 相当于是 java 通过一顿后台操作帮我们生成了一个类来实现接口,并调用我们提供的方法。

另外还要习惯看一种多个参数的写法:

fun(实参1,接口实例的lambada写法);
如:
fun1(123,()->{xxxx;})

lambda实际工作流程

用 Exception 报错来追踪 lambda 调用流程

下面我们试图通过报错 Exception 来看看 lambda 背后的调用栈。

设计代码如下:

package lambdatest;  import java.util.ArrayList;  
import java.util.List;  public class LambdaWhere {  public static void main(String[] args) {  List<String> myList = new ArrayList<>();  myList.add("str01");  myList.forEach(LambdaWhere::processString);  }  private static void processString(String s) {  throw new RuntimeException();  }  }/* 
运行结果:
Exception in thread "main" java.lang.RuntimeExceptionat lambdatest.LambdaWhere.processString(LambdaWhere.java:14)at java.util.ArrayList.forEach(ArrayList.java:1259)at lambdatest.LambdaWhere.main(LambdaWhere.java:10)Process finished with exit code 1
*//*
分析:
lambdatest.LambdaWhere.main(LambdaWhere.java:10)
其实指向的就是
myList.forEach(LambdaWhere::processString);  java.util.ArrayList.forEach(ArrayList.java:1259)
指向的是 ArrayList.java 里的 action.accept()
@Override  
public void forEach(Consumer<? super E> action) {  Objects.requireNonNull(action);  final int expectedModCount = modCount;  @SuppressWarnings("unchecked")  final E[] elementData = (E[]) this.elementData;  final int size = this.size;  for (int i=0; modCount == expectedModCount && i < size; i++) {  action.accept(elementData[i]);  // <-------------------------指向此行}  if (modCount != expectedModCount) {  throw new ConcurrentModificationException();  }  
}lambdatest.LambdaWhere.processString(LambdaWhere.java:14)
指向的是
private static void processString(String s) {  throw new RuntimeException();  // <-------------------------指向此行
}所以这个 lambda 的工作流程是:
myList.forEach()  ----> ArrayList 的 .accept() ----> processString()
详细:myList.forEach(LambdaWhere::processString);  
1. main() 执行到 myList.forEach()
2. 跳转到 ArrayList 里的 forEach(),执行到 action.accept();
3. 带着for迭代出来的实参,跳转到封装在 .accept() 里的 lambda,即 LambadaWhere.processString()。所以当 processString() 中抛出异常时,逐个调用栈返回。
可见,java 对 lambda 的工作流程封装得很好,并没有给我们暴露出具体的跳转细节,只能看到大体的跳转。即,lambda 的确会让 java 在背后去用一个类去实现接口,然后调用其抽象方法(这里是 accept),再调用封装在 accept() 里的代码(lambda 让 java 把 processString() 封装进了 accept() 中了)。
*/

自己定义能写成 lambda 格式的方法与接口

其实这里面也就说明了如何去自己定义一个可以写成 lambda 的方法,而不是仅仅去调用 java 目前已有的方法来写成 lambda 形式。

// 情况1:抽象方法没有返回值(返回值类型为 void):main(){// 格式:fun1(abfun1抽象方法中的代码)fun1(System.out::println);/*那么这里就表示相当于把 abfun1 封装成:void abfun1(String str){System.out.println(str);}*/
}// fun1(接口类型 接口实例引用){
fun1(Inter1 inter1){                     // 对标 forEach(Consumer action) 中的 forEach()xxxx;// 格式:接口.唯一待实现抽象方法(实参); // 注意实参要和抽象方法abfun1()的形参类型相同inter1.abfun1(s);  // 这里 s 是个 String,// fun1 的作用就是不断给接口的抽象方法喂 s 参数(就是喂数据给抽象方法中的代码)
}// 接口定义
interface Inter1{                      // 对标 forEach(Consumer action) 中的 Consumer action// 返回值类型 抽象方法名(参数列表); void abfun1(String str);
}// 情况2:抽象方法有返回值main(){// 格式:fun2(abfun2抽象方法中的代码)String ret = fun2(String::valueOf);/*因为这个抽象方法有返回值,所以如果写成这种简单形式,就代表方法体内只有一条语句,那这条语句就只能是 return,因为有返回值时,必须要有 return 语句。那么这里就表示相当于把 abfun2 封装成:String abfun2(double number){return String.valueOf(number);}*/
}// fun2(接口类型 接口实例引用){
fun2(Inter2 inter2){xxxx;// 格式:接口.唯一待实现抽象方法(实参); // 注意实参要和抽象方法abfun2()的形参类型相同inter2.abfun2(d);  // 这里 d 是个 double,// fun2 的作用就是不断给接口的抽象方法喂 d 参数(就是喂数据给抽象方法中的代码)
}// 接口定义
interface Inter2{// 返回值类型 抽象方法名(参数列表); String abfun2(double number);
}

stream() 与 collect()

lambda 结构对计算优化很友好。lambda 其实最适合处理的任务是连续任务,一个接着一个处理,直到业务结束。

注意:
stream() 是 Collection 接口里的方法,而 Stream 是一个接口,Stream 接口里有很多方法都是仍旧返回 Stream 实例,也就是说可以用这些方法的返回值继续调用另外的一些方法。
collect() 是 Stream 接口里的一个方法,而 Collectors 是一个类。要注意区分 Collector 和 Collectors。

  • Collector 是 java.util.stream 包里的一个 final 类。
  • 而 Collectors 是 java.util.stream 包里的一个接口。

stream() 是 Collection 接口里带默认实现的抽象方法,返回一个 Stream (接口)实例,可以将.stream() 看作是将工作流引入到 lambda 体系,可以调用 Stream 接口中的各种方法,其中很多都支持 lambda 写法,可以让任务处理一个接一个进行。

collect() 是 Stream 接口中的一个抽象方法,可以将 stream 工作流,或者说将一连串的 lambda 处理过后的数据重新收集起来成为一个 List (toList)或 Map(toMap) 或 Set (toSet)实例。

import java.util.ArrayList;  
import java.util.List;  
import java.util.stream.Collectors;  public class LambdaStream {  public static void main(String[] args) {  List<String> myList = addElementsToList(new ArrayList<>());  myList.stream().filter(s->s.length()>4).forEach(System.out::println); // 等价于// myList.stream().filter(s->{return s.length()>4;}).forEach(System.out::println); /*  .stream() 可以看成是返回一个 stream,专门接收 lambda 封装的数据处理任务,一个接一个,像工作流一样  再看 filter(),filter() 是 Stream 接口里的一个抽象方法:Stream<T> filter(Predicate<? super T> predicate);  里面的 Predicate:  public interface Predicate<T> {  // 仅 test() 为待实现的抽象方法,其他方法都有缺省实现  boolean test(T t); // 接收一个元素,返回一个布尔值  }  也就是filter的作用就是接收一个元素,判断一下要不要这个元素  s->s.length()>4 就是筛选长度大于 4 的数据。  之后得到的元素长度都是超过 4 的,之后再 forEach(),  然后再 println 打印出来。  拆解一下:  s->s.length()>4 是一个 lambda,是封装在 fitler() 的参数 Predicate 接口的 test() 方法中的代码,filter按这个条件执行,筛选出长度大于 4 的 String(myList中元素)  这里要注意一下,因为 test() 的定义是有一个 boolean 返回值的,所以这里的 s.length()>4 其实就是返回值。也就是这种写法其实是等价于:  .filter(s->{return s.length()>4;})  当然,这么写没有问题,但 IDEA 会提示:Statement lambda can be replaced with expression lambda  会建议你直接写成  .filter(s->s.length()>4)  但你是不能写成下面这样的:  .filter(s->{s.length()>4;})  或  .filter(s->{s.length()>4})  会报错:不是语句  并且,这样也会缺少返回值。*/  /*  输出结果:  str10  str11  str12  str13  str14  str15  str16  str17  str18  str19  str20  str21  */  // 还可以继续添加步骤,比如说在中间再插入一个转换成大写字母的步骤  myList.stream().filter(s->s.length()>4).map(String::toUpperCase).forEach(System.out::println);  // 具体能加入哪些中间步骤,其实得看 Stream 接口都支持哪些方法,这个可以通过跳转到 Stream.java 文件后 ctrl+F12 查看。  // 上面的 .forEach() 其实也是 Stream 里的 forEach(),而不是 Iterable 里的 forEach(),和 myList.forEach() 其实不同。  // 你会发现 myList.forEach() 并非一个抽象方法,而 Stream 接口里的 forEach() 是一个抽象方法,  // 但实际上 myList.stream() 返回了一个实例,一步一步地去跳转,发现是一个 ReferencePipeline 类的实例,而这个 ReferencePipeline 实现了 Stream 接口,  // 也就是说实际上这里调用的 forEach() 的抽象方法,都是 ReferencePipeline 里实现 Stream 接口时定义的方法,比如里面的 forEach 定义如下:  /*  @Override  public void forEach(Consumer<? super P_OUT> action) {  evaluate(ForEachOps.makeRef(action, false));  }  *//*  输出结果:(变成了大写字母)  STR10  STR11  STR12  STR13  STR14  STR15  STR16  STR17  STR18  STR19  STR20  STR21  */  // 当然也可以使用 collect 让数据重新生成一个 ListSystem.out.println("----------使用collect----------");  // 当然这里还是会用到 .stream(),你可以理解成,用了 .stream() 就能把工作流引入到 lambda 的体系中来,可以实现这种连续的步骤操作。  List<String> longgerStrList = myList.stream().filter(s->s.length()>4)  .map(String::toUpperCase).collect(Collectors.toList());  /*  最后的 .collect() 是这里要重点说的:  collect 就是把 lambda 最后处理完的元素收集起来,可以 collect 成 list 或者别的。  Collectors.toList() 返回的是一个 Collector 接口实例  Collector<T, ?, List<T>> toList() {  return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,  (left, right) -> { left.addAll(right); return left; },  CH_ID);  }而 collect() 的定义是:  <R, A> R collect(Collector<? super T, A, R> collector);  当然,实际上因为 Collection.stream()里返回的是 Stream 实例是 ReferencePipeline 实例,所以实际调用的 collect 方法是:(不用去细看,反正就不是一个抽象方法就是了)public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {  A container;  if (isParallel()  && (collector.characteristics().contains(Collector.Characteristics.CONCURRENT)) && (!isOrdered() || collector.characteristics().contains(Collector.Characteristics.UNORDERED))) {  container = collector.supplier().get();  BiConsumer<A, ? super P_OUT> accumulator = collector.accumulator();  forEach(u -> accumulator.accept(container, u));  }  else {  container = evaluate(ReduceOps.makeRef(collector));  }  return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)  ? (R) container  : collector.finisher().apply(container);  }Collector 接口里并不仅只有一个待实现的抽象方法,而是有多个。所以这里.collect(Collectors.toList())就不再是 lambda 了,只是普通的 Stream 实例方法调用。  */  longgerStrList.forEach(System.out::println);  /*  这两步,其实合并起来,和一开始的  myList.stream().filter(s->s.length()>4).map(String::toUpperCase).forEach(System.out::println);  是一样的,只是为了说明可以使用 collect() 将stream、 lambda得出的结果重新保存为 List,之后再进一步处理。  当然 longgerStrList.forEach() 的 .forEach() 就不是 Stream 的 forEach() 了,而是 Iterable 里的 forEach()。  */  /*  输出结果:  STR10  STR11  STR12  STR13  STR14  STR15  STR16  STR17  STR18  STR19  STR20  STR21  */  }  private static List<String> addElementsToList(List<String> list) {  for(int i=0;i<22;i++){  list.add("str"+i);  }  return list;  }  
}

List 的 .stream() 返回的 stream 对象的 map() 实际运行的代码可以通过 IDEA 点进去一级一级地找到,内容大致是执行map()的形参Function实例里面的 apply() 方法,二这个 apply() 里的代码,其实就是 List.stream().map((t)->{对t的运算返回一个r}) 里的 lambda。

lambda还可以抛出异常

写成 lambda 形式的接口的抽象方法也是可以抛出异常的,不会影响它其中的代码被写成 lambda 形式,只需对会抛出异常的语句进行异常处理(catch捕获,或者进一步往外 throws就行)。

当然,需要进一步处理的是 checked Exception,若是 unchecked Exception,如 RuntimException,甚至可以不处理。

import java.util.function.Function;  public class LambdaReturn {  public static void main(String[] args) throws Exception {  // (见最下方代码)调用那个很正常的,参数可以写成lambda格式的方法作为对比  String ret = randomData(String::valueOf);  /*  这里就是把 Math.random() 返回的 double 随机数 转换成 StringString apply(Object a){  return String.valueOf(a);  }  复杂形式:String ret = randomData(s -> {return String.valueOf(s);});  */  System.out.println(ret); // 0.2890837966543165  // (见最下方代码)调用哪个参数可以写成lambda,但会抛出异常的方法  String retException = randomDataWithExceptionHandling(String::valueOf);  // 由于 randomDataWithExceptionHandling() 进一步将 Exception throws 出来,所以主程序(main)中也得进一步处理,catch 或继续 throws,  // 这里选择了继续往外 throws/*  这里就是把 Math.random() 返回的 double 随机数 转换成 StringString hasException(Object p) throws Exception{  return String.valueOf(p);  }  其实可以看到,虽然接口的抽象方法会抛出异常,但是在具体使用 lambda 时,没有什么区别(除了得对这个语句 try-catch 或在外部 throws 异常)。  */  System.out.println(retException); // 0.1310644524002148  }  // 定义一个很正常的,参数可以写成 lambda 的方法  private static String randomData(Function<Object,String> converter) {  return converter.apply(Math.random());  }  /*  Function 是 java 自带的一个接口,只有一个待实现的抽象方法 .apply()public interface Function<T, R> {  R apply(T t); // 返回值类型为 R,形参为 T}  写成上方的形式后, R 就是 String,而 T 就是 Object 了(当然实际上传入的 Math.random() 返回值为 double)  即:  String apply(Object t);  */  // 定义一个参数可以写成lambda,但接口抽象方法要抛出异常的方法  private static String randomDataWithExceptionHandling(ExceptionLambda<Object,String> converter) throws Exception {  return converter.hasException(Math.random());  }  /*  这里就是将自己定义的 ExceptionLambda<P,R> 接口作为参数。  要能写成 lambda,自然也需要在方法体内调用 ExceptionLambda 接口的抽象方法 hasException(),  而由于 hasException() 是要抛出 Exception 的,所以这里要进一步处理,用 catch 或 继续 throws,  这里选择的是继续往外 throws  所以其实有异常抛出的 lambda 也没有什么太大区别,就是在定义这个调用接口的抽象方法的方法里对异常要进行处理罢了。  R hasException(P p) throws Exception;  写成上方的形式后, R 就是 String,而 P 就是 Object 了(当然实际上传入的 Math.random() 返回值为 double)  即:  String hasException(Object p) throws Exception;  */    
}  interface ExceptionLambda<P,R>{  R hasException(P p) throws Exception;  // hasException() 是个抽象方法,所以这里没有方法体,其中返回值类型为 R,形参类型为 P,  // 并且需要抛出 Exception 异常,由于这是一个 checked Exception,  // 所以,无论具体实现的方法体内是否真的产生了异常,都得在调用这个方法的外部作针对此异常的处理(catch捕获,或进一步往外 throws)  
}

也就是说,要在调用 lambda 封装后的那个接口的地方,处理好异常。

lambda 的精髓

lambda 的精髓:让代码脱离类的束缚,自由地飞翔。这样就可以把代码传递给数据提供方(就是那个调用接口抽象方法的那个方法),而不是只能把数据传递给代码。通过这种写法,达到链式的数据处理,非常优美。

lambda 的理解比喻
原先 java 都是将数据传给代码,让代码去处理。就好比甲方把工程项目交给乙方外包厂商去处理。(我有项目,你拿走搞定!)

而使用 lambda 则是把处理数据的代码传给数据提供方。就好比甲方让乙方外包厂商派团队到甲方公司的办公场地来处理工程项目。(我有项目,你过来搞!)
这样还有一个好处就是可以很好地链式处理,我拿你传过来的代码,把我的数据处理好,还可以继续去找别的代码传过来,把之前处理过的数据进一步处理。这就像我这个政府工程项目,找了联通公司的人来外包驻场处理,处理获得的成果,继续让移动公司的人来外包驻场处理,进一步的结果,继续让电信公司的人来外包驻场处理。(比如说联通过来搭建数据平台,移动过来搞数据平台的安全检测,电信最后派人过来搞数据平台的日常运维)

当然你也可以说把数据先交给A,处理完再传递给B,之后再进一步处理后传给 C,这样链式处理。就像把项目扔给联通建设,联通搞完扔给移动安全检测,移动检测完扔给电信运维。但个人的确感觉传代码的方式,比传数据的方式更优美。因为传数据写成链式的话,参数就是一个一个的方法调用,会显得很臃肿:C(B(A(数据))),而传代码则是:数据提供方.A().B().C()

java 现在版本(如 1.8)中的 lambda 在最终的实现上其实也是使用类的。

可以看到,我们在调用代码的时候,其实还是通过接口。只是 java 帮我们把 “如何用我们提供的 lambda 来实现这个接口” 的细节隐藏掉了(而且隐藏得很好)。

lambda 的本质,仍旧是实例化一个单方法接口,它只是 Java 的语法糖,底层编译时,仍旧和匿名类是一样的。 也就是说 lambda 其实只是一个接口的实例对象,具体最终这个方法的行为,其实不是看 lambda 表达式,而是看那个将lamba表达式(接口的实例对象)作为参数的那个方法的方法体里面是什么样的程序逻辑。如果那个方法就是调用接口实例的方法,那么看起来就好像是 lambda 表达式就是这个“方法本身”一样。
这就像,别人找你借钱,你就是那个 lambda,即便你定义借钱利息 4%,也不代表这这次借钱的数学期望是 4%,因为取决于对方这个“方法”具体做些什么,假如他拿去赌博了,风险极大,可能压根没有钱还。除非他借钱后,直接什么都不做,而调用你lambda里定义的还钱程序,给你支付 4% 的利息,只有这样,才会显得你的 lambda 就是他这个“方法”本身。

lambda 使用建议

虽然 lambda 很美,尤其是写成链式数据处理时非常优美。但也不需要刻意去追求把代码写成 lambda 形式,或者故意去把方法、接口设计成可以写成 lambda 形式的方法、接口,因为毕竟理解上有点绕。倒是可以多练习一下,实际项目中使用则顺其自然就好,无须刻意追求。

这篇关于java篇 常用工具类 0x04:lambda的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1123773

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听