Java中的Lambda表达式
目录
1. 概念
1.1. 为什么要引入Lambda表达式
简化实现函数式接口(Functional Interface,
@FunctionalInterface
)
函数式接口: 有且只有一个抽象方法,但可以有多个非抽象方法的接口。
- 结构:
- 注解:
@FunctionalInterface
,可以不加此注解,只要符合只有一个抽象方法的定义,那编译器也会识别为一个函数式接口; - 一个抽象方法:必须要有,且只能有一个;
- 一个或多个非抽象方法。
@FunctionalInterface public interface GreetingService { // 抽象方法 String greet(String name); // 默认方法(非抽象) default void printGreeting(String name) { String greeting = greet(name); System.out.println("Hello, " + greeting + "!"); } // 静态方法(非抽象) static GreetingService createDefaultGreetingService() { return (name) -> "Hello, " + name; } }
- 注解:
- 作用:
简化匿名内部类
我现在有一个函数式接口:
public interface Authorization {
int calculate(int a, int b);
}
我需要使用这个接口中的calculate方法,用来实现a+b:
public class Addition implements Authorization{
@Override
public int calculate(int a, int b) {
return a + b;
}
}
public static void main(String[] args) {
Authorization auth = new Addition();
int calculate = auth.calculate(1, 2);
System.out.println(calculate);// return 3
}
如果我使用匿名内部类的写法,那就是这样的:
public static void main(String[] args) {
Authorization auth = new Authorization() {
@Override
public int calculate(int a, int b) {
return a + b;
}
};
System.out.println(auth.calculate(1, 2));// return 3
}
如果我用Lambda表达式的写法,那就是这样:
public static void main(String[] args) {
// 也可以不声明参数类型 Authorization authorization = (a, b) -> a + b;
Authorization authorization = (int a, int b) -> a + b;
authorization.calculate(1, 2);// return 3
}
美化代码
1.2. 概念说明
抽象方法:
- 概念:仅有方法签名(包括返回类型、方法名和参数列表)而没有具体实现(方法体)的成员方法,必须在抽象类或接口中声明,并要求子类或实现类提供其实现。
- 识别:在抽象类中使用
abstract
来修饰的方法,以及在接口中没有方法体的方法(接口中的方法都默认是抽象方法,不过在JDK8引入了default
方法,此方法不是抽象的)。 - 案例:
-
抽象类中的抽象方法
public abstract class Shape { // 非抽象方法(已实现) public String getDescription() { return "This is an abstract shape."; } // 抽象方法(未实现,子类必须提供实现) public abstract double calculateArea(); // 可选:其他成员变量、构造方法、非抽象方法等 }
-
接口
public interface MyInterface { // 抽象方法 void abstractMethod(); // 默认方法(非抽象) default void nonAbstractMethod() { System.out.println("This is a default method implementation."); } }
-
为什么函数式接口是Lambda表达式的基石?
由于Lambda表达式本身是一种简洁、轻量级的匿名函数形式,它没有名称,也没有明确的类定义。为了让Lambda表达式能够与现有的面向对象结构兼容,Java要求Lambda表达式必须能够被赋值给一个类型明确的对象。函数式接口恰好提供了这种类型,它的单个抽象方法定义了Lambda表达式可以具有的行为签名。通过将Lambda表达式赋值给函数式接口的实例,Lambda表达式获得了类型,从而能够在Java代码中被安全地使用。
简化行为参数化
函数式接口使得方法可以接受行为作为参数。这意味着方法可以要求调用者传递一个特定操作的实现(如一个计算规则、一个筛选条件、一个转换逻辑等),而不是具体的业务数据。这极大地提升了代码的灵活性和可重用性。
支持函数式编程风格
使用Lambda表达式、函数式接口、Stream API等工具来编写简洁、声明式、无副作用的代码。
-
使用Lambda表达式
public static void main(String[] args) { List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); // 使用Lambda表达式过滤长度大于3的名字 names.stream() .filter(name -> name.length() > 3) .forEach(System.out::println); // 输出:Bob, Charlie }
-
使用函数式接口作为方法参数
public class FunctionInterfaceExample { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // 使用Lambda表达式作为Function接口参数 List<Integer> squaredNumbers = transform(numbers, number -> number * number); System.out.println(squaredNumbers); // 输出:[1, 4, 9, 16, 25] } public static <T, R> List<R> transform(List<T> input, Function<T, R> mapper) { List<R> result = new ArrayList<>(); for (T item : input) { result.add(mapper.apply(item)); } return result; } }
-
使用Stream API进行复杂数据处理
public class StreamApiExample { public static void main(String[] args) { List<Person> people = Arrays.asList( new Person("Alice", 25), new Person("Bob", 3½), new Person("Charlie", ¼)); // 使用Stream API进行复杂数据处理 List<String> adultNames = people.stream() .filter(person -> person.getAge() >= 18) .map(Person::getName) .sorted() .collect(Collectors.toList()); System.out.println(adultNames); // 输出:[Alice, Bob] } static class Person { private String name; private double age; public Person(String name, double age) { this.name = name; this.age = age; } public String getName() { return name; } public double getAge() { return age; } } }
促进API设计与扩展
函数式接口使得API设计更加模块化和可扩展。标准库和第三方库可以提供一系列通用的函数式接口(如 Function<T, R>
、Predicate<T>
、Consumer<T>
等),这些接口定义了常见的操作模式。开发人员可以轻松地以Lambda表达式的形式提供这些操作的具体实现,而无需创建大量的小型匿名内部类。此外,函数式接口可以通过添加默认方法和静态方法来增强接口的功能,而不会增加实现类的负担。
促进API设计与扩展
使用函数式接口,编译器可以在编译时检查Lambda表达式或方法引用是否与函数式接口的抽象方法签名匹配,确保类型安全。加上 @FunctionalInterface
注解后,编译器还会进一步检查接口是否确实符合函数式接口的定义(仅有一个抽象方法),提供额外的保障。
-
内部类
嵌套在另一个类内部定义的类,能够访问其外部类的所有成员(包括私有成员),并与其外部类之间存在一种特殊的包含与被包含关系。public class InnerClassExample { private String secretMessage = "This is a secret message."; public class MessagePrinter { public void printMessage() { System.out.println(secretMessage); } } public static void main(String[] args) { InnerClassExample outer = new InnerClassExample(); InnerClassExample.MessagePrinter printer = outer.new MessagePrinter(); printer.printMessage(); // 输出:This is a secret message. } }
-
局部内部类
在方法或块内部定义的类,只在该范围内可访问,且能够访问其封闭范围内的局部变量。public class LocalInnerClassExample { public void processData(int value) { // 定义局部内部类,用于封装特定逻辑 class DataProcessor { int process(int input) { return input * 2; } } // 创建局部内部类的实例并使用 DataProcessor processor = new DataProcessor(); int processedValue = processor.process(value); System.out.println("Processed value: " + processedValue); } public static void main(String[] args) { LocalInnerClassExample example = new LocalInnerClassExample(); example.processData(10); // 输出:Processed value: 20 } }
-
静态内部类
嵌套在另一个类内部、使用 static 修饰的类,不依赖于外部类实例,可独立访问,且不能直接访问外部类的非静态成员。public class StaticInnerClassExample { private static final int MAX_VALUE = 100; public static void main(String[] args) { // 使用静态内部类实现策略模式 ValidationStrategy strategy = new ValidationStrategy.IsInRangeStrategy(MAX_VALUE); boolean isValid = strategy.validate(50); System.out.println("Is valid: " + isValid); // 输出:Is valid: true } // 静态内部类,表示验证策略 static abstract class ValidationStrategy { public abstract boolean validate(int value); // 具体的验证策略实现 static class IsInRangeStrategy extends ValidationStrategy { private final int max; public IsInRangeStrategy(int max) { this.max = max; } @Override public boolean validate(int value) { return value >= 0 && value <= max; } } } }
-
匿名内部类
在某个类或方法的内部,直接定义且不声明类名的特殊类。它继承自某个父类或实现某个接口,并在创建时立即实例化。这种设计常用于简化代码,特别是在仅需一次性使用、与外部环境紧密相关的类场景中,避免了为这类短暂存在的类单独命名。
如Runnable接口,我需要使用它的run方法,但我不想写一个类来实现Runnable接口,再去重写run,然后用这个子类的run,这样比较麻烦,所以就用了匿名内部类的写法,如下:- 不使用Lambda:
new Thread(new Runnable() { @Override public void run() { System.out.println("Running in a thread using an anonymous inner class"); } }).start();
- 使用Lambda:
new Thread(() -> System.out.println("Running in a thread using a lambda expression")).start();
- 不使用Lambda:
2. Lambda表达式的语法
2.1. 基础语法
Lambda表达式的结构:
- 参数列表:
- 参数列表以括号
()
包围,可以包含零个、一个或多个参数。 - 如果只有一个参数且参数类型可以被编译器推断,可以省略括号。例如:
x -> ...
- 多个参数之间用逗号 , 分隔。例如:
(x, y) -> ...
- 对于参数类型,如果编译器可以从上下文中推断出来,可以省略类型声明;否则需要显式声明。例如:
(int x, double y) -> ...
- 参数列表以括号
- 箭头符号:箭头符号用于分隔参数列表和 Lambda 表达式主体,表示“参数”到“操作”的映射关系。
- 方法主体:
- 函数体紧跟箭头符号之后,可以是一个表达式或一个代码块。
- 表达式形式(单行):当主体是一个简单的表达式时,可以直接写出表达式,编译器会自动返回表达式的值作为 Lambda 结果。例如:
x -> x + 1
- 代码块形式(多行):如果需要执行多个语句或需要显式使用
return
语句,可以使用大括号{}
包裹代码块。例如:(x, y) -> { return x * y; }
- 返回类型(省略):在Java中,Lambda表达式的返回类型总是由编译器根据上下文推断得出,无需显式声明。
::
上面的Lambda表达式还可以这样写:
::
在 Java 中被称为方法引用来创建 Lambda,它用于引用已有方法作为 Lambda 表达式。这种写法适用于以下两种情况:
-
对象方法引用: 当要引用一个已有的特定对象的方法作为 Lambda 表达式时,使用 对象名::方法名 的形式。例如:
import java.util.Comparator; public interface StringComparator extends Comparator<String> { }
StringComparator comparator = String::compareToIgnoreCase;
-
类(静态)方法引用: 当要引用一个已有的类(静态)方法作为 Lambda 表达式时,使用 类名::方法名 的形式。例如:
Authorization authorization = Integer::sum;
2.2. 搭配流(Stream API)
案例: 不用Lambda和Stream API遍历ArrayList:
public class TraditionalForEachExample {
public static void main(String[] args) {
List<String> words = new ArrayList<>(Arrays.asList("apple", "banana", "cherry", "date", "elderberry"));
// 使用传统的for-each循环遍历ArrayList
for (String word : words) {
// 使用if语句进行筛选和打印
if (word.length() >= 5) {
System.out.println(word);
}
}
}
}
使用Lambda表达式搭配Stream API来实现ArrayList的遍历:
public class LambdaStreamExample {
public static void main(String[] args) {
List<String> words = new ArrayList<>(Arrays.asList("apple", "banana", "cherry", "date", "elderberry"));
// 使用Lambda表达式搭配Stream API遍历ArrayList
words.stream()
.filter(word -> word.length() >= 5) // 过滤出长度大于等于5的单词
.forEach(System.out::println); // 打印每个满足条件的单词
}
}
对比上述案例,可见使用Stream+Lambda之后,代码好看了不少,可读性也更好。
Lambda表达式与Java 8的Stream API结合使用时,通常遵循的基本步骤和语法:
-
创建Stream:
从集合、数组或其他支持流式处理的数据源获取一个 Stream 对象。例如,对于 ArrayList,可以调用其stream()
方法:List<String> list = ...; // 假设有一个ArrayList实例 Stream<String> stream = list.stream();
-
中间操作:
对 Stream 应用一系列中间操作(如 filter、map、sorted 等),形成一个新的 Stream。这些操作是延迟执行的,即它们不会立即计算结果,而是等到终端操作触发时才执行。中间操作通常接收Lambda表达式作为参数。Stream<String> filteredStream = stream.filter(s -> s.startsWith("a")); // 过滤以"a"开头的元素
-
终端操作:
应用一个终端操作(如 forEach、collect、count、anyMatch 等),触发中间操作的执行,并产生最终结果或副作用。终端操作也经常使用Lambda表达式。List<String> list = Arrays.asList("apple", "banana", "cherry", "date", "elderberry"); list.stream() // 创建Stream .filter(s -> s.startsWith("a")) // Lambda表达式作为中间操作的参数,过滤以"a"开头的元素 .forEach(System.out::println); // Lambda表达式作为终端操作的参数,打印符合条件的元素
哪些类可以使用Stream API?
-
Collections
List<String> employeeNames = Arrays.asList("Alice", "Bob", "Charlie"); List<String> upperCaseNames = employeeNames.stream() .map(String::toUpperCase) .collect(Collectors.toList()); System.out.println(upperCaseNames); // 输出:[ALICE, BOB, CHARLIE]
-
Map
Map<String, Integer> data = new HashMap<>(); data.put("one", 11); data.put("two", 8); data.put("three", 15); data.put("four", 17); Map<String, Integer> filteredMap = data.entrySet().stream() .filter(entry -> entry.getValue() > 10) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue));
-
数组:基本类型数组(如 int[], double[], char[] 等)和对象类型数组(如 String[], MyClass[] 等)都可以通过 java.util.Arrays 类的静态方法 stream() 转化为流。
int[] numbers = {1, 2, 3, 4, 5}; IntStream intStream = Arrays.stream(numbers); // 基本类型数组转为流 String[] words = {"apple", "banana", "cherry"}; Stream<String> stringStream = Arrays.stream(words); // 对象类型数组转为流
-
第三方库
说明:
- Java 8 的 Stream API 主要针对的是 java.util.Collection、java.util.Map 及其子接口和实现类,这些类已经内置了 stream() 方法,可以直接用来创建相应的流;
.collect()
是 Java 8 引入的 Stream API 中的一个关键方法,用于将中间操作产生的中间流转换为最终的单一结果。它通常用于完成对流中元素的聚合操作,将流中的数据结构化或汇总成一个新的数据结构,如 List、Set、Map 或者自定义的聚合结果。.collect() 方法接收一个 Collector 实例作为参数,该实例定义了如何将流元素累积到最终结果中。
2.3. 常用的Stream API
创建 Stream
Collection.stream()
:将 Collection(如 List、Set)转换为 Stream。Arrays.stream(T[] array)
:将数组转换为 Stream。Stream.of(...elements)
:直接创建包含指定元素的 Stream。Stream.generate(Supplier<T>)
:生成无限序列的 Stream,由提供的 Supplier 不断产生新元素。Stream.iterate(initial, UnaryOperator<T>)
:创建一个无限序列的 Stream,从初始值开始,每次应用指定的函数更新元素。
中间操作
filter(Predicate<? super T>)
:基于指定条件过滤流中的元素。map(Function<? super T, ? extends R>)
:对流中的每个元素应用一个函数,生成新的元素。flatMap(Function<? super T, ? extends Stream<? extends R>>)
:扁平化操作,将流中的每个元素转换为另一个 Stream,然后将所有生成的 Stream 元素连接成一个单一的 Stream。peek(Consumer<? super T>)
:对流中的每个元素执行一个操作(如打印或更新状态),但不影响流的整体处理。limit(long maxSize)
:限制流最多包含指定数量的元素。skip(long n)
:跳过流的前 n 个元素。distinct()
:去除流中重复的元素。
终止操作
forEach(Consumer<? super T>)
:遍历流中的每个元素,对其执行指定的操作。toArray()
:将流转换为数组。collect(Collector<? super T, A, R>)
:将流元素收集到一个容器(如 List、Set、Map)或其他自定义数据结构中,通常结合 Collectors 类的工厂方法使用。reduce(BinaryOperator<T>)
:通过一个二元操作符将流中的元素累积为一个单一结果,如求和、求积、求最大值等。min(Comparator<? super T>)
/max(Comparator<? super T>)
:返回流中最小/最大的元素。anyMatch(Predicate<? super T>)
/allMatch(Predicate<? super T>)
/noneMatch(Predicate<? super T>)
:检查流中是否存在至少一个元素/所有元素/没有元素满足指定条件。count()
:返回流中元素的总数。findFirst()
/findAny()
:返回流中的第一个元素/任意一个元素(在并行流中可能更快)。
并行流相关
parallel()
/sequential()
:将流转换为并行流/顺序流,以便利用多核处理器进行并行处理或恢复为单线程处理。unordered()
:去除流的排序特性,允许在并行处理时提高性能。
补充:
- 无线序列:无限序列的 Stream 是 Java 8 引入的一种特殊类型的 Stream,它能够表示一个理论上无止境的元素序列。这类 Stream 不像传统集合那样预先存储所有元素,而是通过某种机制动态生成元素,使得在处理过程中可以源源不断地获取新的值。无限序列 Stream 主要用于需要处理大量数据或者无法预知数据规模的场景,同时它们充分利用了 Stream API 的惰性求值特性,确保只有在真正需要时才生成和消耗数据。
原文地址:https://blog.csdn.net/m0_54355172/article/details/138242805
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!