目录
一.基本语法
在Java中,lambda表达式是一种简洁、函数式的方法来表示匿名函数。它们主要用于需要函数式接口的场合,如Java 8引入的Stream API、Comparator接口等。
Lambda表达式的基本语法如下:
(parameter-list) -> { body-of-lambda }
其中:
parameter-list:参数列表,可以有一个或多个参数,也可以没有参数。
->:Lambda操作符,用于分隔参数列表和Lambda主体。
{ body-of-lambda }:Lambda主体,包含了Lambda表达式要执行的代码。
Lambda表达式的几个例子:
1.无参数、无返回值的Lambda表达式:
() -> System.out.println("Hello, Lambda!")
2.单个参数、无返回值的Lambda表达式(参数类型可以省略):
x -> System.out.println(x)
3.多个参数、有返回值的Lambda表达式:
(int x, int y) -> x + y
4.如果Lambda表达式只有一个参数,那么圆括号可以省略:
x -> x * x
5.如果Lambda表达式的主体只有一条语句,那么花括号可以省略,并且这条语句的返回值会被自动返回:
x -> x * x
6.使用Lambda表达式实现Runnable接口:
Runnable runnable = () -> System.out.println("Running Lambda!");
runnable.run();
7.使用Lambda表达式作为比较器(Comparator):
List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
list.sort((s1, s2) -> s1.compareTo(s2));
在Java中,Lambda表达式主要用于替代匿名内部类,尤其是当接口只有一个抽象方法(即函数式接口)时。Lambda表达式提供了一种更简洁、更直观的方式来表达这些函数式接口的实现。
要注意的是,Lambda表达式的类型是由上下文推断的,即Lambda表达式需要赋值给一个函数式接口的变量,或者作为参数传递给一个接受函数式接口的方法。Java编译器会根据Lambda表达式的参数和返回类型,以及上下文中的函数式接口,来推断Lambda表达式的类型。
二.实现函数接口
在Java中,Lambda表达式通常用于实现只有一个抽象方法的接口,这种接口被称为函数式接口(Functional Interface)。从Java 8开始,接口可以声明为函数式接口,通过添加@FunctionalInterface注解来明确这一点。如果接口包含超过一个抽象方法,那么添加这个注解会导致编译错误。
函数式接口的一个典型例子是Runnable接口,它只有一个run方法。使用Lambda表达式,我们可以很方便地实现这个接口,而不需要创建匿名内部类。下面是一个函数式接口的定义和如何使用Lambda表达式来实现它的例子:
// 定义一个函数式接口
@FunctionalInterface
interface MyFunction {
void apply(int x, int y);
}
public class LambdaDemo {
public static void main(String[] args) {
// 使用Lambda表达式实现MyFunction接口
MyFunction myLambda = (x, y) -> System.out.println("Result: " + (x + y));
// 调用Lambda表达式
myLambda.apply(10, 20);
}
}
在这个例子中,MyFunction是一个函数式接口,它有一个名为apply的抽象方法,接受两个int参数并没有返回值。然后,我们使用Lambda表达式(x, y) -> System.out.println("Result: " + (x + y))来实现这个接口,并将Lambda表达式赋值给MyFunction类型的变量myLambda。最后,我们调用myLambda的apply方法,它会执行Lambda表达式中的代码。
除了Runnable和自定义的函数式接口,Java库中还提供了许多其他的函数式接口,如Consumer、Supplier、Function、Predicate等,它们都在java.util.function包中定义。这些接口可以与Lambda表达式一起使用,以简化代码并增加可读性。例如,使用Consumer接口来接受一个参数并执行某个操作:
import java.util.function.Consumer;
public class LambdaWithConsumer {
public static void main(String[] args) {
// 使用Lambda表达式实现Consumer接口
Consumer<String> consumer = (s) -> System.out.println("Consumed: " + s);
// 使用Consumer接口的方法accept来调用Lambda表达式
consumer.accept("Hello, Consumer!");
}
}
在这个例子中,Consumer接口有一个accept方法,它接受一个参数并没有返回值。Lambda表达式(s) -> System.out.println("Consumed: " + s)实现了这个接口,并打印出传入的字符串。
三.引用方法创建线程
在Java中,使用Lambda表达式和方法引用可以非常方便地创建线程。从Java 8开始,你可以使用Thread类的构造函数,它接受一个Runnable对象,而Runnable是一个函数式接口,所以你可以使用Lambda表达式来创建线程。下面是一个使用Lambda表达式创建线程的示例:
public class LambdaThreadExample {
public static void main(String[] args) {
// 使用Lambda表达式创建线程
Thread thread = new Thread(() -> {
System.out.println("线程正在运行,由Lambda表达式创建");
});
// 启动线程
thread.start();
}
}
在这个例子中,我们创建了一个Thread对象,并传递了一个Lambda表达式作为参数给Thread的构造函数。这个Lambda表达式实现了Runnable接口的run方法,当线程启动时,它会执行这个Lambda表达式中的代码。
除了使用Lambda表达式,你还可以使用方法引用来创建线程。方法引用实际上是对已有方法的引用,它使得Lambda表达式的写法更加简洁。
下面是一个使用方法引用创建线程的示例:
public class LambdaThreadExample {
public static void main(String[] args) {
// 使用方法引用创建线程
Thread thread = new Thread(LambdaThreadExample::printThreadRunning);
// 启动线程
thread.start();
}
// 这是一个静态方法,它符合Runnable接口的run方法签名
public static void printThreadRunning() {
System.out.println("线程正在运行,由方法引用创建");
}
}
在这个例子中,我们使用了LambdaThreadExample类的静态方法printThreadRunning作为Thread构造函数的参数。这个方法的签名与Runnable接口的run方法相匹配,所以我们可以直接使用方法引用来代替Lambda表达式。
使用Lambda表达式或方法引用来创建线程可以使代码更加简洁和易读。同时,它们也是Java 8引入的Lambda特性的一部分,使得函数式编程在Java中更加容易实现。
四.操作集合
在Java中,Lambda表达式可以非常方便地用于操作集合(如List、Set等),尤其是与Stream API结合使用时。Stream API允许你以声明性方式处理数据集合(即你可以描述你想要做什么,而不是描述如何去做)。
以下是一些使用Lambda表达式操作集合的例子:
1.遍历集合(forEach)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
2.或者使用方法引用:
names.forEach(System.out::println);
3.过滤集合(filter)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
filteredNames.forEach(System.out::println); // 输出 Alice
4.映射集合(map)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
nameLengths.forEach(System.out::println); // 输出名字长度
5.排序集合(sorted)
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
sortedNames.forEach(System.out::println); // 输出排序后的名字
6.聚合操作(reduce)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println(sum); // 输出 15
7.收集到不同的集合类型(collect)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 收集到 Set
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
// 收集到 List,并排序
List<String> sortedNames = names.stream()
.sorted()
.collect(Collectors.toList());
// 收集到自定义的类
class Person {
String name;
// 构造器、getter和setter等
}
List<Person> people = names.stream()
.map(name -> new Person(name))
.collect(Collectors.toList());
8.查找元素(findAny, findFirst)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Optional<String> anyName = names.stream().findAny();
Optional<String> firstName = names.stream().findFirst();
anyName.ifPresent(System.out::println); // 输出任意一个名字
firstName.ifPresent(System.out::println); // 输出第一个名字
这些示例展示了如何使用Lambda表达式与Stream API结合来操作集合。Lambda表达式允许你简洁地表达操作逻辑,而Stream API则提供了丰富的函数式操作来处理数据。使用这些特性,你可以更加高效地处理集合数据。
五.闭包问题
在Java中,Lambda表达式可以捕获其外部作用域的变量,使得这些变量在Lambda体内部可用。这个过程被称为闭包(Closure)。闭包是函数式编程的一个重要概念,它允许函数记住并访问其词法作用域,即使该函数在其原始作用域之外执行。
在Java中,当一个Lambda表达式捕获其外部变量时,这些变量实际上是被隐式地“传递”给了Lambda表达式。如果Lambda表达式修改了捕获的变量,并且这个变量是基本数据类型,那么实际上会捕获该变量的一个副本,因此原始变量的值不会被改变。但如果捕获的是对象引用,那么Lambda表达式和原始变量将引用同一个对象,因此对该对象的任何修改都会反映在原始变量上。
这里有几个关于Java中Lambda闭包的要点:
1.值捕获:对于局部变量,Lambda表达式只能捕获final或effectively final(实际上是final,即初始化后不再改变的变量)的变量。这是因为在Lambda表达式创建时,需要确保捕获的变量不会在其后的代码中改变,从而保持Lambda表达式的稳定性。
2.引用捕获:对于成员变量和静态变量,没有这样的限制,因为它们的生命周期与类的实例或类本身相同。Lambda表达式可以自由地捕获和修改这些变量的值。
3.逃逸分析:Java编译器会进行逃逸分析,以确定一个对象是否可能被外部方法所访问。如果一个对象只被一个线程访问,并且不会被外部方法所逃逸,那么编译器可能会对该对象进行优化,比如标记为栈上分配(Stack Allocation),而不是堆上分配。这可以提高性能并减少垃圾收集的压力。但是,如果Lambda表达式捕获了一个对象,并且这个Lambda表达式可能被传递到其他方法中执行,那么这个对象就不能被逃逸分析优化,它必须被分配在堆上。
4.序列化问题:如果一个Lambda表达式捕获了非瞬态(non-transient)和非静态(non-static)的字段,那么该Lambda表达式不能被序列化,因为序列化机制无法处理这种捕获关系。为了解决这个问题,可以将这些字段标记为瞬态(transient)或静态(static),或者提供一个自定义的序列化机制。
5.性能考虑:虽然Lambda表达式和闭包提供了很大的便利性和灵活性,但它们也可能带来一些性能开销。捕获变量会增加Lambda表达式的大小,并可能导致额外的对象创建和内存分配。因此,在性能敏感的应用中,应谨慎使用Lambda表达式和闭包,并进行适当的性能测试和分析。
下面是一个简单的Java Lambda闭包示例:
public class LambdaClosureExample {
public static void main(String[] args) {
String str = "Hello, Lambda!";
Runnable runnable = () -> System.out.println(str); // 捕获str变量
str = "Changed!"; // 修改str变量,但不会影响Lambda表达式中的str
runnable.run(); // 输出:Hello, Lambda!
}
}
在这个例子中,str变量被Lambda表达式捕获并用于打印。随后,我们修改了str变量的值,但这并不影响已经创建的Lambda表达式中的str变量的值,因为Lambda表达式捕获的是str的一个副本(对于基本数据类型)或一个引用(对于对象)。
文章制作不易,如果有帮助的话,还希望能给个点赞和关注支持一下,谢谢大家!🙏🙏🙏