java中的lambda表达式(从小白也能看懂做起)

时间:2023-02-11 17:00:57


历史背景

到目前为止,在 Java 中传递一个代码段并不容易, 不能直接传递代码段,Java 是一种面 向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码

但是在其他语言中可以直接处理代码块,Java在很长时间里面拒绝加入这个特性。我们知道Java的优点:

  • 简单性
  • 一致性

如果只要有一个特性能让代码简洁,就加入的话,这个语言很快就会变得一团糟。(来自Java核心技术卷1)

在Java8中提出了lambda表达式,处理代码块 (变得更加简洁紧凑)

lambda表达式的语法

(参数)->表达式

(参数)->{ 语句; }

特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。

具体的写法

根据上面所说的特征,lambda表达式大致可以分为下面3种写法

无返回值有形参

package lambda;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public class Test {
public static void main(String[] args) {
TestInterFace testInterFace = new TestInterFace() {
@Override
public void show(int a, int b) {
System.out.println(a + b);
}
};
testInterFace.show(30,40);

TestInterFace testInterFace1 = (int a,int b)->{
System.out.println(a+b);
};

TestInterFace testInterFace2 = (a,b) ->{
System.out.println(a+b);
};

TestInterFace testInterFace3 = ((a, b) -> System.out.println(a+b));
}
}
package lambda;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public interface TestInterFace {
public abstract void show(int a,int b);
}
  • 可以省略方法名,编译器会帮你自动检测方法名
  • 可以省略方法中的形参类型,编译器可以推导出,a,b必然是int类型
  • 如果对抽象方法的实现逻辑只有一行,可以省略方法体的大括号

有返回值

package lambda;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public class Test2 {
public static void main(String[] args) {
TestInterFace2 testInterFace = new TestInterFace2() {
@Override
public int add(int a, int b) {
return a+b;
}
};

TestInterFace2 testInterFace2 = (int a,int b) ->{
return a-b;
};

TestInterFace2 testInterFace21 = (a,b) -> {return a-b;};

TestInterFace2 testInterFace22 = (a,b) -> a-b;
}
}
package lambda;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public interface TestInterFace2 {
public abstract int add(int a,int b);
}

简化说明参考:无返回值有形参的

有一个形参

当只有一个形参的时候,可以去掉形参的括号

package lambda;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public class Test3 {
public static void main(String[] args) {
InterFace interFace = a -> System.out.println(a);
interFace.show(10);
}
}
package lambda;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public interface InterFace {
public abstract void show(int a);
}

函数式接口

Java中有很多封装代码的接口,如Comparator,lambda表达式与这些接口是兼容的。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式,这种接口称为函数式接口

lambda和Arrays.sort

注:sort的第二个参数需要一个Compartor实例

package lambda;

import java.util.Arrays;
import java.util.Comparator;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public class ArraySort {
public static void main(String[] args) {
// lambda方式
Integer[] numsArr = new Integer[10];
for(int i = 0; i<10; i++){
numsArr[i] = i;
}
/*
Arrays.sort(numsArr,new Comparator<Integer>(){
public int compare(Integer a, Integer b){
return b-a;
}
});
*/
Arrays.sort(numsArr, ( Integer a, Integer b) -> { return b-a;});
for (int i: numsArr){
System.out.println(i);
}
}
}

实际上,在 Java 中, 对 lambda 表达式所能做的也只是能转换为函数式接口。在其他支 持函数字面量的程序设计语言中,可以声明函数类型(如(String, String) -> int)、 声明这些类 型的变量,还可以使用变量保存函数表达式。不过,Java 设计者还是决定保持我们熟悉的接 口概念, 没有为 Java语言增加函数类型。

方法引用

有可能已经有现成的方法来帮助你完成你想要传递到其他代码的某个动作,可以使用方法引用,有点抽象,通俗的来说:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用,

比如我希望只要出现一个定时器事件就打印这个事件的对象:

  1. ​Timer t = new Timer(1000,event -> System.out.println(event))​
  2. 改进:讲println方法直接传递到Timer构造器
    ​Timer t = new Timer(1000,System.out::println);​

要用::操作符分隔方法名和对象或类名,主要有三种情况

  • 对象::实例方法
  • 类::静态方法
  • 类::实例方法

在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式。前面已经提到, System.out::println 等价于 x -> System.out.println(x)。 类似地,Math::pow 等价于(x,y) -> Math.pow(x, y)。

对于第 3 种情况, 第 1 个参数会成为方法的目标。例如,String::compareToIgnoreCase 等 同于 (x, y) -> x.compareToIgnoreCase(y) 。

对象::实例方法

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

类::静态方法

(x,y) -> Math.pow(x, y)

类::实例方法

package lambda;

import java.util.Comparator;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public class Test4 {
public static void main(String[] args) {
Comparator<String> comparator = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
};

System.out.println(comparator.compare("20", "2"));//1

Comparator<String> comparator1 = String::compareTo;
System.out.println(comparator1.compare("20", "2"));//1
}
}

构造器引用

构造器引用与方法引用很类似,只不过方法名为new

​ClassName::new​

假设你有多个方法,那么构造哪一个呢?这取决于上下文,我们来看实例:

package lambda;

import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/**
* @Author 秋名山码神
* @Date 2022/12/21
* @Description
*/
public class 构造器 {
public static void main(String[] args) {
/*************** 构造器的引用 ****************/
// 无参构造函数,创建实例
Supplier<Emp> supper2 = () -> new Emp();
Supplier<Emp> supper3 = Emp::new;
Emp emp1 = supper3.get();
emp1.setAddress("上海");
// 一个参数
Function<String, Emp> fun = address -> new Emp(address);
Function<String, Emp> fun1 = Emp::new;
System.out.println(fun1.apply("beijing"));
// 两个参数
BiFunction<String, Integer, Emp> bFun = (name, age) -> new Emp(name, age);
BiFunction<String, Integer, Emp> bFun1 = Emp::new;
System.out.println(bFun1.apply("xiaohong", 18));

}


static class Emp {
private String address;

private String name;

private Integer age;

public Emp() {

}

public Emp(String address) {
this.address = address;
}

public Emp(String name, Integer age) {
this.name = name;
this.age = age;
}

public Emp(String address, String name, Integer age) {
super();
this.address = address;
this.name = name;
this.age = age;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "Emp [address=" + address + ", name=" + name + ", age=" + age + "]";
}

}

变量作用域

lambda表达式:

  1. 一个代码块
  2. 参数
  3. *变量的值,指非参数而且不在代码种定义的变量

关于代码块以及*变量值有一个术语: 闭包,如果有人吹嘘他们的语 言有闭包,现在你也可以自信地说 Java 也有闭包。在 Java 中, lambda 表达式就是闭包。

lambda 表达式可以捕获外围作用域中变量的值。 在 Java 中,要确保所捕获 的值是明确定义的,这里有一个重要的限制。在 lambda 表达式中, 只能引用值不会改变的 变量。

public static void countDown(int start, int delay)
{
ActionListener listener = event ->
{
start--; // Error: Can't mutate captured variable
System.out.println(start);
};
new Timer(delay, listener).start();
}

如果在lambda表达式中改变变量,并发执行多个动作时,就会不安全。

另外如果在 lambda 表达式中引用变量, 而这个变量可能在外部改变,这也是不合法的。 例如,下面就是不合法的:

public static void repeat(String text, int count)
{
for (int i = 1; i <= count; i++)
{
ActionListener listener = event ->{
System.out.println(i + ": " + text);// Error: Cannot refer to changing i
}
}
new Timer(1000, listener).start();
}

规则:1. lambda 表达式中捕获的变量必须实际上是最终变量

这个变量初始化之后就不会再为它赋新值。

  1. 在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。