当前位置: 代码迷 >> 综合 >> Java--lambda(λ)表达式
  详细解决方案

Java--lambda(λ)表达式

热度:46   发布时间:2023-09-21 21:27:14.0

在Java引入lambda表达式之前,并不能在Java中传递一个代码段。因为Java是严格的面向对象编程,所以必须构造一个对象,这个对象的类需要有一个方法来包含所需的代码。

Java SE8中加入了lambda表达式来处理代码块,增强Java来支持函数式编程。

lambda表达式的语法:

//表达式形式:参数,箭头以及一个表达式
(String first, String second) -> first.length() - second.length() 
//如果代码要完成的计算无法放在一个表达式中,可以像方法一样使用{},并包含显式return语句
(String first, String second) -> 
{if(first.length()<second.length()) return -1;else return 1;
}

如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型,如:

//在这里,first和second必然是字符串
Comparaor<String> comp = (first,second)->first.length()-second.length()

如果方法只有一个参数,而且这个参数的类型可以推导出,还可以省略小括号:

//这里event可以写作(event) or (ActionEvent event)
ActionListener listener = event->System.out.println("the time is"+ new Date());

分析上面的所有代码发现,无需指定lambda表达式的返回类型,因为lambda表达式的返回类型总会根据上下文推导得出。

函数式接口:

Java在不支持lambda表达式之前,已经存在很多封装代码块的接口,如Comparator, lambda表达式和这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口成为函数式接口(使用lambda表达式比创建一个类并实现该接口更加简单,同时该名称也体现了函数式编程的概念)。

例如,Arrays.sort()方法提供了自定义排序。其第一个参数是待排序的数组,第二个参数是排序规则--一个实现了Comparator接口的实例。因为Comparator是只有一个抽象方法的接口,我们不必再去写一个类去实现这个接口,直接用lambda表达式:

Arrays.sort(arrays, (first,second)->first.length()-second.length());

这行代码可以实现字符串数组arrays中字符串的按长度排序。

上面的实例就是lambda表达式可以转换为函数式接口。但也只能做到这些。相比较而言,其他支持函数式编程的程序设计语言可以声明函数类型,声明这些类型的变量,还可以使用变量保存函数表达式。

方法引用:

如果有一个现成的方法可以完成想要传递的代码段的操作,那么我们可以直接传递该方法:

Timer t = new Timer(1000,System.out::println);

上面代码的功能是每出现一个定时器事件(1秒)就打印这个事件对象,上面的代码等同于:

Timer t = new Timer(1000,event->System.out.println(event));

再比如,如果想对字符串排序,并且不考虑大小写,可以传递以下方法表达式:

Arrays.sort(strings,String::compareToIgnoreCase);

等价于:

Arrays.sort(strings,(x,y)->x.compareToIgnoreCase(y));

从上面例子可以看出,要用 " :: " 分隔对象名/类名与方法名,主要有三种情况:

  1. object::instanceMethod
  2. Class::staticMethod
  3. Class::instanceMethod

当然,this和super关键字也可以用在方法引用中。

构造器引用:

构造器引用和方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。哪一个构造器取决于上下文。

例如,假设有一个字符串列表要把它转换成Person数组,为此要在各个字符串上调用构造器:

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

可以用数组类型建立构造器引用,如:int[]::new是一个构造器引用,它有一个参数:数组长度,等价于:  x->new int[x];

Java无法构造泛型类型T的数组,因为new T[n]会变成new Object[n]。但流苦利用构造器引用可以解决这个问题。例如需要一个Person数组,Stream接口有一个toArray方法可以返回一个Object数组:

Object[] people = stream.toArray();

但我们真正想要的是Person数组,可以这样做:

Person[] people = stream.toArray(Person::new);

变量的作用域:

我们可能会在lambda表达式中访问外围变量,如:

public static void repeatMessage(String text,int delay){ActionListener listener = event->{System.out.println(text);Toolkit.getDefaultToolkit().beep();};new Timer(delay,listener).start();
}

然后我们这样调用:

repeatMessage("Hello",1000);     //每隔1秒打印一个“Hello"

这里会有一个问题,lambda表达式可能在repeatMessage调用返回很久以后才运行,那时参数变量text已经不存在了,如何保留text?

来看一下lambda表达式的组成:

  1. 参数;
  2. 一个代码块;
  3. 自由变量的值,这是指非参数而且不在代码中定义的变量。

这里text就是自由变量,是lambda表达式的数据结构中必须保存的值。我们说它被lambda表达式捕获。

注意:关于代码块和自由变量值有一个术语:闭包。在Java中,lambda表达式就是闭包。

在Java中,要确保捕获的值是明确定义的,且有一个重要的限制:lambda表达式要捕获的变量必须是实际上的最终变量(该变量初始化之后不会再为它赋新值)。

处理lambda表达式:

上面讲了如何编写lambda表达式以及如何将lambda表达式传递到方法中。但如果我们写一个方法,如何保证我们的方法可以处理lambda表达式呢?

例如,我们要重复一个动作n次,将这个动作和重复次数传进repeat:

repeat(10,()->System.out.println("Hello World!"));

要接受这个lambda表达式,需要选择(有时需要提供)一个函数式接口。例如我们选用Runable接口:

public static void repeat(int n,Runnable action){for(int = 0; i < n; i++) action.run();
}

当我们吧lambda表达式传入方法的第二个参数后,调用action.run()时会执行lambda表达式的主体。

补充:

什么时候使用lambda表达式:

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法的适当位置运行代码(例如排序中的比较操作)
  • 发生某种事件时执行代码
  • 只在必要时才运行的代码

常用的函数式接口:

函数式接口 参数类型 返回类型 抽象方法名 描述
Runnable void run 作为无参数或返回值的动作执行
Supplier<T> T get 提供一个T类型的值
Consumer<T> T void accept 处理一个T类型的值
BiConsumer<T> T,U void accept 处理T和U类型的值
Function<T,R> T R apply 有一个T类型参数的函数
BiFunction<T,U,R> T,U R apply 有T和U类型参数的函数
UnaryOperator<T> T T apply 类型T上的一元操作符
BinaryOperator<T> T,T T apply 类型T上的二元操作符
Predicate<T> T boolean test 布尔值函数
BiPredicate<T> T,U boolean test 有两个参数的布尔值函数

 

  相关解决方案