前言
Lambda表达式是 Java 8
引入的一种简洁的定义匿名函数的方式。它们允许以简洁的语法编写实例化匿名类的方式,特别是当实现只有一个方法的接口时(这种接口通常被称为函数式接口)。
Lambda表达式可以视为一种特殊的匿名类,它提供了一种不必编写类定义和构造器就能实现函数式接口的语法糖。在本文中,我们将深入探讨Lambda表达式的概念、语法和用法,并着重以Lambda的用法为主,为每个实例提供代码演示。
一、Lambda表达式的作用
Lambda表达式主要用于实现以下目的:
- 简化代码:用一行或几行代码替换复杂的匿名类。
- 函数参数:允许将行为作为参数传递给方法。
- 函数返回:允许方法返回一个行为。
二、Lambda表达式的语法
Lambda表达式的一般语法如下:
(parameters) -> expression
或者
(parameters) -> { statements; }
- parameters:参数列表,它可以是一个参数(用圆括号括起来),多个参数(同样用圆括号括起来),或者没有参数(空的圆括号)。
- ->:Lambda操作符,又称为“箭头操作符”或“哈希操作符”,用于分隔参数列表和Lambda体。
- expression:Lambda体,它可以是一个表达式或语句块。如果Lambda体是一个表达式,那么该表达式会被求值并成为Lambda表达式的结果。如果Lambda体是一个语句块,那么它需要用花括号
{}
括起来,并且如果需要返回结果,必须显式使用return
语句。
示例
单参数Lambda表达式
(String s) -> s.length()
这个Lambda表达式接受一个String
类型的参数s
,并返回该字符串的长度。
无参数Lambda表达式
()-> System.out.println("Hello, World!")
这个Lambda表达式没有参数,并且会打印出"Hello, World!"。
多个参数的Lambda表达式
(int a, int b) -> a + b
这个Lambda表达式接受两个int
类型的参数a
和b
,并返回它们的和。
使用表达式作为Lambda体
s -> s.toUpperCase()
这个Lambda表达式接受一个String
类型的参数s
,并使用toUpperCase()
方法将其转换为大写。
使用语句块作为Lambda体
(String s1, String s2) -> {
if (s1.length() > s2.length()) {
return s1;
} else {
return s2;
}
}
这个Lambda表达式接受两个String
类型的参数s1
和s2
,并返回两者中较长的那个。
三、函数式接口
Lambda表达式经常与函数式接口一起使用。函数式接口是只包含一个抽象方法的接口。Java API中定义了很多这样的接口,如Runnable
、Callable
、Comparator
和Predicate
等。Java 8引入了一个新的包java.util.function
,它包含了一系列这样的单方法接口,如Function<T,R>
、Consumer<T>
、Supplier<T>
等,它们可以与Lambda表达式一起使用。
1、使用Supplier
接口
Supplier<U>
是Java中的一个函数式接口(Functional Interface)。它是一个代表供应者的接口,该接口不接收任何参数,但会返回一个类型为U的结果。
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
Supplier
接口定义了一个无参数方法 get(),该方法用于获取一个结果值。根据函数式编程的理念,Supplier
接口通常用于延迟计算或惰性求值,即在需要时才计算生成结果。可以将Supplier
视为一个工厂函数,用来提供某种类型的对象。
接口定义:
@FunctionalInterface
public interface Supplier<U> {
U get();
}
- 好处:
- 延迟计算:
Supplier
对象在需要时才计算结果,可以实现延迟计算或惰性求值。 - 可复用性:可以将一些常用的计算逻辑封装到
Supplier
对象中,并在需要时复用。
- 延迟计算:
- 坏处:
- 性能开销:
Supplier
对象在每次调用时需要进行计算,可能会引入一定的性能开销。
- 性能开销:
以下是一些使用 Supplier
接口的示例:
1. 使用Supplier获取当前日期时间
Supplier<LocalDateTime> getCurrentDateTime = () -> LocalDateTime.now();
LocalDateTime dateTime = getCurrentDateTime.get();
System.out.println(dateTime); // 当前日期时间
2. 与 Stream API 结合使用
List<Supplier<String>> suppliers = Arrays.asList(
() -> "Java",
() -> "Kotlin",
() -> "Scala"
);
List<String> words = suppliers.stream()
.map(Supplier::get) // 使用方法引用 Supplier::get
.collect(Collectors.toList());
在这个例子中,我们使用 Stream API
的 map 方法将一系列 Supplier<String>
转换成它们提供的字符串列表。
2、使用Predicate接口
Predicate 是 Java 中的一个非常重要的函数式接口,位于 java.util.function 包中。它只包含一个方法 test,该方法接受一个参数并返回一个布尔值,表示对参数进行测试的结果。
接口定义
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
- 好处:
- 简化条件判断:Predicate接口可以减少代码中的大量的if-else语句,使代码逻辑更清晰。
- 可复用性:可以将常用的条件判断逻辑封装成Predicate对象,并在多个地方复用。
- 坏处:
- 可读性较差:对于复杂的条件判断,使用Lambda表达式可能会降低代码的可读性和可维护性。
使用场景
Predicate<T>
接口通常用于需要对对象进行判断或测试的场景,比如验证一个条件是否成立。它广泛用于 Stream API 中的 filter
、anyMatch
、allMatch
、noneMatch
等操作。
以下是一些使用 Predicate
接口的示例:
1. 判断输入日期是否属于半年范围内
Predicate<Date> isWithinHalfYear = date -> {
LocalDate sixMonthsAgo = LocalDate.now().minusMonths(6);
LocalDate dateToLocalDate = date
.toInstant()
.atZone(ZoneId.of("Asia/Shanghai"))
.toLocalDate();
return !dateToLocalDate.isBefore(sixMonthsAgo);
};
Date startDate = DateUtil.parse("2022-06-24 00:00:00", "yyyy-MM-dd HH:mm:ss");
Date endDate = DateUtil.parse("2023-01-24 23:59:00", "yyyy-MM-dd HH:mm:ss");
if (!isWithinHalfYear.test(startDate ) || !isWithinHalfYear.test(endDate )) {
System.out.println("时间选择区间不能超过6个月");
}
2. 与 Stream API 结合使用
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy");
// 使用Predicate进行过滤
List<String> filteredWords = words.stream()
.filter(word -> word.startsWith("J"))
.collect(Collectors.toList());
在这个例子中,Lambda 表达式 word -> word.startsWith("J")
实际上就是一个 Predicate<String>
的匿名实现。
3、使用Fuction<T, R>接口
Function<T, R>
是 Java 中的一个函数式接口,位于 java.util.function 包中。它表示一个函数,它接受一个类型为 T 的参数并返回一个类型为 R 的结果。
接口定义
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
- T:函数的输入参数类型。
- R:函数的返回结果类型。
- 好处:
- 逻辑封装:将输入类型转换为输出类型的逻辑封装到Function对象中,使代码更具可读性和可维护性。
- 可组合性:Function对象可以通过组合和链式调用来创建更复杂的转换逻辑。
- 坏处:
- 性能开销:使用Function对象可能会引入一定的性能开销,特别是在大规模数据处理的情况下。
使用场景
Function 接口常用于需要映射
或者转换操作
的场合,比如将一种类型的值转换成另一种类型的值。
以下是一些使用 Function
接口的示例:
1. 将一个字符串转换为小写。
Function<String, String> toLowerCase = String::toLowerCase;
String name = "xiaoming";
String nameLowerCase = toLowerCase.apply(name);
System.out.println(nameLowerCase );
2. 与 Stream API 结合使用
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy");
List<Integer> lengths = words.stream()
.map(word -> word.length()) // 使用Lambda表达式获取字符串长度
.collect(Collectors.toList());
// 或者使用Function接口引用
List<Integer> lengthsUsingFunction = words.stream()
.map(String::length) // 使用方法引用
.collect(Collectors.toList());
在这个例子中,我们使用 Stream API
的 map 方法来转换流中的每个字符串元素到它们的长度。map 方法接受一个 Function 作为参数,我们可以使用 Lambda 表达式或者方法引用来提供这个 Function。
4、使用Consumer接口
Consumer<T>
是 Java 中的一个函数式接口,位于 java.util.function 包中。它表示一个接受单个参数并且不返回结果的操作。Consumer 接口主要用于执行对单个输入参数的处理,但不返回任何结果,即它的返回类型是 void。
接口定义
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
Consumer<T> andThen(Consumer<? super T> after);
}
- T:消费者函数的输入参数类型。
- 好处:
- 无副作用:可以使用Consumer对象对数据进行处理,而不破坏原始数据的状态。
- 操作封装:将某些操作封装到Consumer对象中,可以模块化和复用。
- 坏处:
- 可读性较差:对于复杂的操作,使用Lambda表达式可能会降低代码的可读性和可维护性。
使用场景
Consumer 接口通常用于需要对元素执行某些操作的场景,如记录日志
、更新外部资源
、或者执行某些副作用
。
以下是一些使用 Consumer
接口的示例:
1. 遍历列表并将其中的元素打印出来
Consumer<String> printToConsole = str -> System.out.println(str);
List<String> list = Arrays.asList("hello", "world");
list.forEach(printToConsole);
2. 与 Stream API 结合使用
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy");
words.forEach(System.out::println); // 使用方法引用执行forEach
// 或者使用Consumer
words.forEach(Consumer.<String>of(System.out::println));
5、使用BiPredicate<T, U>接口
BiPredicate<T, U>
是 Java 中的一个双参数谓词接口,位于 java.util.function
包中。它表示一个接受两个参数(类型分别为 T
和 U
)并返回一个布尔值的函数。BiPredicate
接口主要用于执行涉及两个不同类型输入参数的条件判断。
接口定义
@FunctionalInterface
public interface BiPredicate<T, U> {
boolean test(T t, U u);
}
T
和U
:函数的两个输入参数类型。
使用场景
BiPredicate
接口常用于需要对两个对象进行比较或逻辑判断的场合,比如检查两个对象是否满足某种关系。
以下是一些使用 BiPredicate
接口的示例:
1. 使用 Lambda 表达式
BiPredicate<String, String> stringComparator = (a, b) -> a.equals(b);
boolean areEqual = stringComparator.test("Java", "Java"); // 返回 true
在这个例子中,我们创建了一个 BiPredicate<String, String>
,用于比较两个字符串是否相等。
2. 使用方法引用
如果需要使用现有方法作为 BiPredicate
,可以使用方法引用。例如,使用 Objects::equals
:
BiPredicate<Object, Object> objectComparator = Objects::equals;
boolean areEqual = objectComparator.test(new Object(), new Object()); // 返回 false
3. 与 Stream API 结合使用
虽然 Stream
API 的 filter
方法通常只接受单参数的 Predicate
,但结合 Collectors
可以间接使用 BiPredicate
,例如进行配对操作:
List<Person> people = Arrays.asList(
new Person("John", 20),
new Person("Jane", 25)
);
boolean hasPersonOverTwenty = people.stream()
.anyMatch(person -> new BiPredicate<Integer, String>() {
public boolean test(Integer age, String name) {
return age > 20 && name.equals("Jane");
}
}.test(person.getAge(), person.getName()));
// 输出 true 或 false 取决于条件是否满足
System.out.println(hasPersonOverTwenty);
在这个例子中,我们使用了一个匿名内部类实现 BiPredicate<Integer, String>
来检查列表中是否有年龄超过20且名字为 “Jane” 的人。
6、使用BiFunction<T, U, R>接口
BiFunction<T, U, R>
是 Java 中的一个双参数函数接口,位于 java.util.function
包中。它表示一个接受两个参数(类型分别为 T
和 U
)并返回一个类型为 R
的结果的函数。BiFunction
接口在需要对两个输入执行某个操作并得到一个结果时非常有用。
接口定义
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
T
:函数的第一个输入参数类型。U
:函数的第二个输入参数类型。R
:函数的返回结果类型。
使用场景
BiFunction
接口常用于需要根据两个输入参数执行操作并返回结果的场景,例如,根据两个数值计算一个数值结果,或者基于两个对象生成一个新对象。
以下是一些使用 BiFunction
接口的示例:
1. 使用 Lambda 表达式
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
int result = sum.apply(5, 10); // 返回 15
在这个例子中,我们创建了一个 BiFunction<Integer, Integer, Integer>
,用于计算两个整数的和。
2. 使用方法引用
BiFunction<String, String, Integer> stringLengthComparator = String::compareToIgnoreCase;
int comparisonResult = stringLengthComparator.apply("Java", "java"); // 返回 0,因为忽略大小写比较相同
这里,我们使用方法引用 String::compareToIgnoreCase
来创建一个 BiFunction
,它比较两个字符串是否相同,忽略大小写。
3. 与 Stream API 结合使用
虽然 Stream
API 没有直接使用 BiFunction
的操作,但可以通过一些间接方式使用它,比如通过 Collectors
进行复杂的规约操作:
List<String> names = Arrays.asList("John", "Jane", "Jim");
List<String> upperCaseNames = names.stream()
.map(name -> new AbstractMap.SimpleEntry<>(name, name.toUpperCase()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
.entrySet()
.stream()
.map(entry -> new BiFunction<String, String, String>() {
public String apply(String key, String value) {
return key + " -> " + value;
}
})
.collect(Collectors.toList());
在这个例子中,我们使用 BiFunction
来创建一个新的字符串表示每个名字及其大写形式。
四、Lambda表达式的用法
1、实现简单forEach循环
使用Lambda表达式实现简单的ForEach循环,可以使用IntStream的forEach方法,或者在处理集合时使用forEach方法。以下是两种实现方式的示例:
使用IntStream的ForEach方法
// 假设我们有一个整型数组
int[] numbers = {1, 2, 3, 4, 5};
// 使用IntStream.forEach进行遍历
IntStream.of(numbers).forEach(number -> {
System.out.println(number);
});
使用集合的forEach方法
// 假设我们有一个字符串列表
List<String> strings = Arrays.asList("aaa", "bbb", "ccc", "ddd");
// 使用集合的forEach方法进行遍历
strings.forEach(s -> {
System.out.println(s);
});
2、实现简单过滤
过滤通常用于从集合中选择满足特定条件的元素。从Java 8开始,Stream API提供了一种强大的方式,通过Lambda表达式对集合进行操作。
过滤列表中的元素
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy", "C#");
// 使用Stream的filter方法过滤出长度大于3的字符串
List<String> filteredWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toList());
// 输出过滤后的列表
filteredWords.forEach(System.out::println);
过滤数组中的元素
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 使用IntStream的filter方法过滤出所有偶数
List<Integer> evenNumbers = IntStream.of(numbers)
.filter(number -> number % 2 == 0)
.boxed()
.collect(Collectors.toList());
// 输出过滤后的列表
evenNumbers.forEach(System.out::println);
3、实现简单的数据转换
在Java中,使用Lambda
表达式进行数据转换是一种非常常见的做法。数据转换通常涉及到将一种数据结构或数据类型转换为另一种。从Java 8开始引入的Stream API
提供了多种方法来支持这种转换,其中map
方法是最常用的一种。
将字符串列表转换为字符串长度列表
假设我们有一个字符串列表,我们想要创建一个新的列表,其中包含原始字符串的长度:
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy");
// 使用Stream的map方法将每个字符串转换为其长度
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
// 输出转换后的长度列表
lengths.forEach(System.out::println);
在这个例子中,我们使用了String::length
方法引用作为map
方法的参数,将每个字符串映射为其长度。
将整数数组转换为对应的平方值列表
假设我们有一个整数数组,我们想要得到一个包含每个数字平方的新列表:
int[] numbers = {1, 2, 3, 4, 5};
// 使用IntStream的map方法将每个数字转换为其平方
List<Integer> squares = IntStream.of(numbers)
.map(number -> number * number)
.boxed()
.collect(Collectors.toList());
// 输出转换后的平方列表
squares.forEach(System.out::println);
在这个例子中,我们通过map
方法传递了一个Lambda
表达式number -> number * number
,将每个数字转换为其平方。
将字符串列表转换为首字母大写的字符串列表
假设我们有一个字符串列表,我们想要将每个字符串转换为首字母大写的形式:
List<String> words = Arrays.asList("java", "kotlin", "scala", "groovy");
// 使用Stream的map方法将每个字符串转换为首字母大写的形式
List<String> capitalizedWords = words.stream()
.map(String::toUpperCase) // 使用方法引用
.collect(Collectors.toList());
// 输出转换后的字符串列表
capitalizedWords.forEach(System.out::println);
在这个例子中,我们使用了String::toUpperCase
方法引用来将每个字符串转换为大写形式。
将字符串列表转换为自定义格式的字符串
假设我们有一个字符串列表,我们想要将每个字符串转换为"Word: "后跟原始字符串的格式:
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy");
// 使用Stream的map方法将每个字符串转换为"Word: "后跟原始字符串的格式
List<String> formattedWords = words.stream()
.map(word -> "Word: " + word)
.collect(Collectors.toList());
// 输出转换后的字符串列表
formattedWords.forEach(System.out::println);
在这个例子中,我们通过map
方法传递了一个Lambda
表达式word -> "Word: " + word
,将每个字符串转换为带有前缀"Word: "的格式。
4、实现简单的去重
在Java中,使用Lambda
表达式实现去重通常涉及到集合转换为Stream
,然后使用distinct
方法来过滤掉重复的元素。distinct
方法可以基于对象的equals
方法来区分不同的元素。
去重一个字符串列表
假设我们有一个字符串列表,其中可能包含重复的字符串:
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Java", "Scala", "Groovy");
// 使用Stream的distinct方法去除重复的字符串
List<String> distinctWords = words.stream()
.distinct()
.collect(Collectors.toList());
// 输出去重后的列表
distinctWords.forEach(System.out::println);
在这个例子中,distinct()
方法会自动根据字符串的equals
方法去除重复的字符串。
去重一个自定义对象的列表
如果有一个自定义对象的列表,并且想要去重,需要确保类重写了equals
和hashCode
方法。否则,distinct
方法可能不会按预期工作。
假设我们有一个Person
类,它有两个属性:name
和age
,并且已经重写了equals
和hashCode
方法:
@Data
class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Alice", 30), // 重复
new Person("Charlie", 35)
);
// 使用Stream的distinct方法去除重复的Person对象
List<Person> distinctPeople = people.stream()
.distinct()
.collect(Collectors.toList());
// 输出去重后的Person对象列表
distinctPeople.forEach(System.out::println);
在这个例子中,我们创建了一个Person
对象的列表,其中包含重复的对象。使用distinct()
方法可以去除这些重复的对象,因为Person
类正确地实现了equals
和hashCode
方法。
请注意,distinct
方法只能基于对象的equals
和hashCode
方法进行去重,它不会使用任何额外的Lambda
表达式参数。如果需要基于对象的某个特定属性进行去重,可能需要先提取这些属性,然后对提取后的流使用distinct
方法。
5、实现特定属性去重
要基于特定属性去重,比如根据name
属性去重,可以使用Stream
API中的filter
方法结合自定义的逻辑,或者使用Collectors.toMap
来转换流中的元素到一个映射(Map),其中特定的属性作为键。以下是两种实现方式的示例:
使用filter
方法
使用filter
方法去重需要手动实现逻辑,以确保只保留第一次出现的元素。这种方法可能在处理大量数据时效率较低,因为它需要额外的逻辑来跟踪已经出现过的元素。
@Data
class Person {
private String name;
private int age;
}
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
// 重复
new Person("Alice", 35),
new Person("Charlie", 35)
);
Set<String> seenNames = new HashSet<>();
List<Person> distinctPeople = people.stream()
// 只添加第一次出现的name
.filter(person -> seenNames.add(person.getName()))
.collect(Collectors.toList());
// 输出去重后的Person对象列表
distinctPeople.forEach(System.out::println);
在这个例子中,我们使用了一个HashSet
来跟踪已经出现过的name
。filter
方法中的Lambda
表达式尝试向HashSet
中添加name
,而HashSet
的add
方法在尝试添加一个已存在的元素时会返回false
,这样我们就可以通过filter
方法排除重复的元素。
使用Collectors.toMap
使用Collectors.toMap
可以更简洁地基于特定属性去重。这个方法尤其适用于当流中的元素可以自然地映射到键值对时。
// 使用Collectors.toMap基于name属性去重
Map<String, Person> peopleMap = people.stream()
.collect(Collectors.toMap(
// 作为键的函数
Person::getName,
// 作为值的函数,即直接使用Person对象
Function.identity(),
// 如果有重复的键,则保留现有的值
(existing, replacement) -> existing
));
// 将Map转换回List
List<Person> distinctPeople = new ArrayList<>(peopleMap.values());
// 输出去重后的Person对象列表
distinctPeople.forEach(System.out::println);
在这个例子中,我们使用Collectors.toMap
来创建一个映射,其中name
是键,Person
对象是值。由于映射的键必须是唯一的,所以这自然实现了去重。如果遇到具有相同name
的不同Person
对象,toMap
的第三个参数指定了保留现有值的策略(在这个例子中,即保留映射中已有的Person
对象)。
6、实现多属性去重
实现基于多个属性的去重通常需要自定义逻辑来比较这些属性。在Java中,可以使用Stream
API结合自定义的比较逻辑来实现这一点。以下是一个示例,展示如何基于多个属性去重:
使用Stream
API和Collectors.toMap
假设我们有一个Person
类,我们想要基于name
和age
属性去重:
class Person {
private String name;
private int age;
// 构造器、getter和setter省略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// 为了去重,提供一个方法来获取属性的组合
public String getNameAndAge() {
return name + ":" + age;
}
}
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Alice", 30), // 重复
new Person("Charlie", 35),
new Person("Alice", 30) // 另一个重复
);
// 使用Collectors.toMap基于name和age属性去重
Map<String, Person> uniquePeople = people.stream()
.collect(Collectors.toMap(
// 作为键的函数,返回name和age的组合
Person::getNameAndAge,
// 作为值的函数,即直接使用Person对象
Function.identity(),
// 如果有重复的键,则保留现有的值
(existing, replacement) -> existing
));
// 将Map转换回List
List<Person> distinctPeople = new ArrayList<>(uniquePeople.values());
// 输出去重后的Person对象列表
distinctPeople.forEach(System.out::println);
在这个例子中,我们为Person
类添加了一个getNameAndAge
方法,它返回一个包含name
和age
的字符串。这个字符串被用作Collectors.toMap
中的键。由于每个Person
对象的name
和age
组合是唯一的,这实现了基于这两个属性的去重。
使用了Stream API
和Collectors
写法1
// 使用Stream和lambda表达式进行多属性去重
List<Person> distinctPersons = persons.stream()
.collect(Collectors.toCollection(() ->
new TreeSet<>(Comparator.comparing(Person::getName)
.thenComparingInt(Person::getAge)))).stream()
.collect(Collectors.toList());
// 输出去重后的Person对象列表
distinctPeople.forEach(System.out::println);
写法2
// 直接使用TreeSet进行去重,并根据name和age进行排序
List<Person> distinctPersons = persons.stream()
.collect(Collectors.toCollection(() ->
new TreeSet<>(Comparator.comparing(Person::getName + ";" + Person::getAge)))))
.stream()
.collect(Collectors.toList());
// 输出去重后的Person对象列表
distinctPersons.forEach(System.out::println);
7、实现简单排序
在Java中,使用Lambda
表达式实现简单排序可以通过Stream
API中的sorted
方法来完成。sorted
方法可以根据自然排序或者提供的一个Comparator
来对流中的元素进行排序。
以下是使用Lambda
表达式实现排序的一些示例:
基于自然排序
如果元素类型具有自然的排序顺序(如整数、字符串等),可以简单地使用sorted
方法进行排序:
List<String> words = Arrays.asList("banana", "apple", "orange", "kiwi");
// 使用sorted方法进行自然排序
List<String> sortedWords = words.stream()
.sorted() // 默认使用自然排序
.collect(Collectors.toList());
// 输出排序后的列表
sortedWords.forEach(System.out::println);
基于自定义属性排序
对于自定义对象,如Person
类,可能需要根据一个或多个属性进行排序。这时,可以使用sorted
方法并提供一个Comparator
:
class Person {
private String name;
private int age;
// 构造器、getter和setter省略
public String getName() {
// ...
}
public int getAge() {
// ...
}
}
List<Person> people = Arrays.asList(
// 初始化Person对象列表
);
// 使用sorted方法根据年龄升序排序
List<Person> sortedPeople = people.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
// 输出排序后的Person对象列表
sortedPeople.forEach(System.out::println);
在这个例子中,我们使用Comparator.comparingInt
来根据Person
对象的age
属性进行整数比较,从而实现升序排序。
8、实现多重排序条件
如果需要根据多个属性进行排序,可以链式调用Comparator
的thenComparing
方法:
// 使用sorted方法先按年龄升序排序,年龄相同的情况下按名字排序
List<Person> sortedPeople = people.stream()
.sorted(Comparator.comparingInt(Person::getAge)
.thenComparing(Person::getName))
.collect(Collectors.toList());
在这个例子中,首先根据age
进行排序,如果age
相同,则使用name
作为第二排序条件。
降序排序
要实现降序排序,可以调用Comparator.reversed()
:
// 使用sorted方法根据年龄降序排序
List<Person> sortedPeopleDesc = people.stream()
.sorted(Comparator.comparingInt(Person::getAge).reversed())
.collect(Collectors.toList());
9、匹配第一条数据
在Java中,如果想要使用Lambda
表达式匹配并获取流(Stream
)中的第一条元素,可以使用Stream
API提供的filter
方法结合findFirst
方法。这里的“匹配”通常意味着根据某个条件筛选元素。
以下是一个使用Lambda
表达式匹配流中第一条满足特定条件的元素的示例:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class Main {
public static void main(String[] args) {
List<String> words = new ArrayList<>();
// 假设我们向列表中添加了一些字符串
words.add("apple");
words.add("banana");
words.add("cherry");
// 使用filter和findFirst来匹配并获取第一条长度大于4的字符串
Optional<String> firstLongWord = words.stream()
.filter(word -> word.length() > 4)
.findFirst()
// 检查是否找到元素,并处理结果
.ifPresent(System.out::println);
}
}
在这个例子中,我们首先创建了一个字符串列表words
。然后,我们调用了stream()
方法来创建一个流,并使用filter
方法筛选出长度大于4的字符串。findFirst
方法用于获取筛选后流中的第一条元素。findFirst
方法返回一个Optional<String>
对象,它可能包含一个值(在这种情况下是匹配的字符串),也可能什么都不包含(如果没有元素匹配条件)。
Optional
类提供了多种方法来处理可能不存在的值,例如ifPresent
方法,它接受一个Consumer
作为参数,当Optional
对象包含一个值时,就会执行这个Consumer
。
如果确实需要获取流中的第一个元素,而不需要任何条件匹配,可以直接使用findFirst
方法,因为流中的第一个元素总是存在的:
Optional<String> firstWord = words.stream()
.findFirst()
.ifPresent(System.out::println);
在这个例子中,无论列表内容如何,findFirst
将简单地返回流中的第一个元素。如果列表为空,Optional
将不会包含任何值。
10、实现汇总
在Java中,reduce
是一个Stream
操作,它用于通过某个连接动作(通常是数学计算)将流中的所有元素汇总(缩减)成一个总结性的结果。reduce
操作可以非常强大,因为它不仅可以用来进行简单的累加计算,还可以用来执行更复杂的逻辑汇总。
reduce
方法接受一个BinaryOperator<T>
作为参数,它是一个接受两个输入参数并返回一个结果的函数,这个结果的类型与输入参数的类型相同。
以下是一些reduce
方法的示例:
累加求和
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用reduce进行求和
int sum = numbers.stream()
.reduce(0, Integer::sum);
// 输出: Sum: 15
System.out.println("Sum: " + sum);
在这个例子中,我们使用reduce
方法和Integer::sum
方法引用来计算列表中所有整数的总和。reduce
方法的第一个参数0
是初始化的累加器值。
连接字符串
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy");
// 使用reduce连接字符串,用逗号和空格分隔
String joined = words.stream()
.reduce("", (a, b) -> a + ", " + b);
// 输出: Joined: Java, Kotlin, Scala, Groovy
System.out.println("Joined: " + joined);
在这个例子中,我们使用reduce
方法将列表中的所有字符串连接成一个由逗号和空格分隔的单个字符串。
基于条件的汇总
List<Person> people = Arrays.asList(
// 假设这是Person对象的列表
);
// 使用reduce基于条件汇总Person对象
Person oldestPerson = people.stream()
.reduce((p1, p2) -> p1.getAge() >= p2.getAge() ? p1 : p2);
System.out.println("Oldest person: " + oldestPerson.getName());
在这个例子中,我们使用reduce
方法找到列表中最年长的人。我们提供了一个Lambda表达式,它接受两个Person
对象并返回其中年龄较大的一个。
11、实现简单分组
在Java中,实现简单分组通常涉及到使用Stream
API的collect
方法,结合Collectors.groupingBy
收集器。这种分组操作可以将流中的元素根据某个属性或者某个函数返回的值进行分类。
以下是使用Lambda
表达式和Stream
API实现分组的一些示例:
按属性分组
假设我们有一个Person
类,我们想要根据人的年龄来分组:
@Data
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class GroupingExample {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("John", 20),
new Person("Jane", 20),
new Person("Mark", 30),
new Person("Mary", 30)
);
// 按年龄分组Person对象到Map
Map<Integer, List<Person>> peopleByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
peopleByAge.forEach((age, person) -> System.out.println("Age " + age + ": " + person));
// for (Map.Entry<Integer, List<Person>> entry : peopleByAge.entrySet()) {
// Integer age = entry.getKey();
// List<Person> persons = entry.getValue();
// System.out.println("Age " + age + ": " + persons);
// }
}
}
在这个例子中,我们创建了一个Person
对象的列表,并使用groupingBy
收集器按年龄属性分组。结果是一个Map
,其键是年龄,值是具有该年龄的所有Person
对象的列表。
按多个属性分组
如果需要根据多个属性进行分组,可以首先创建一个复合键,然后根据这个复合键进行分组:
@Data
class Person {
private String name;
private int age;
private String country;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name, int age, String country) {
this.name = name;
this.age = age;
this.country = country;
}
}
List<Person> people = Arrays.asList(
new Person("XiaoMing", 20, "China"),
new Person("Jane", 20, "America"),
new Person("Mark", 30, "Janpan"),
new Person("Mary", 30, "America")
);
// Person类新增属性:country,然后根据name和country这两个属性进行分组
Map<String, List<Person>> peopleByCountryAndName = people.stream()
.collect(Collectors.groupingBy(p -> p.getName() + " from " + p.getCountry()));
for (Map.Entry<String, List<Person>> entry : peopleByCountryAndName.entrySet()) {
String key = entry.getKey();
List<Person> persons = entry.getValue();
System.out.println("name is " + key + ": " + persons);
}
在这个例子中,我们使用了一个Lambda表达式p -> p.getName() + " from " + p.getCountry()
来创建一个复合键,该键由人的名字和国家组成。
分组并计数
如果不仅想要分组,还想要计算每个分组中的元素数量,可以使用Collectors.groupingBy
与Collectors.counting
结合使用:
// 按年龄分组并计数
Map<Integer, Long> peopleCountByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
peopleCountByAge.forEach((age, count) -> System.out.println("Age " + age + " has " + count + " people"));
在这个例子中,我们不仅按年龄分组了Person
对象,而且还计算了每个年龄分组中的人数。
Collectors.groupingBy
是一个非常强大的工具,它可以用来进行各种复杂的分组操作,从简单的单属性分组到复杂的多属性复合键分组,再到分组后的聚合计算。
12、peek实现日志记录
peek
是 Java Stream
API 中的一个中间操作(intermediate operation),它允许在流的元素被传递到下一操作之前对它们进行某种处理。peek
通常用于调试目的,或者在流处理过程中执行一些辅助操作,比如记录日志、计算某些值、应用副作用等,但它并不改变流中元素本身。
由于 peek
是一个无状态操作(stateless operation),它不会影响流的最终结果,因此它经常用于调试和监控。peek
方法接受一个 Consumer
作为参数,对流中的每个元素执行这个 Consumer
。
以下是一些 peek
方法的使用示例:
调试和日志记录
List<String> words = Arrays.asList("Java", "Kotlin", "Scala", "Groovy");
words.stream()
.peek(System.out::println) // 在这里打印每个元素
.filter(word -> word.length() > 4)
.collect(Collectors.toList());
在这个例子中,peek
用于在过滤操作之前打印流中的每个单词。
注意事项
peek
操作不会改变流中的元素,它仅仅在每个元素上执行一个操作。peek
是无状态的(stateless),这意味着它不会改变流的懒惰性质。- 在生产环境中,过度使用
peek
可能会使代码难以理解和维护,因此应谨慎使用。
13、实现最小值最大值获取
在Java中,要获取流(Stream)中的最小值或最大值,可以使用Stream
API提供的min
和max
方法。这两个方法都返回一个Optional
对象,它可能包含一个值(最小值或最大值),也可能不包含值(如果流为空)。
以下是使用min
和max
方法获取最小值和最大值的示例:
获取最小值和最大值
List<Integer> numbers = Arrays.asList(10, 20, 30, 40, 50);
// 获取最小值
Optional<Integer> minNumber = numbers.stream()
.min(Integer::compareTo)
.ifPresent(n -> System.out.println("Min value: " + n));
// 获取最大值
Optional<Integer> maxNumber = numbers.stream()
.max(Integer::compareTo)
.ifPresent(n -> System.out.println("Max value: " + n));
在这个例子中,我们使用min
和max
方法以及Integer::compareTo
方法引用作为比较器来找到列表中的最小值和最大值。由于Integer
实现了Comparable
接口,我们可以直接传递类名和比较方法。
处理空流
由于min
和max
方法返回Optional
对象,可以使用ifPresent
方法来安全地处理流中的值,即使流是空的:
List<Integer> emptyList = Collections.emptyList();
// 尝试获取最小值和最大值
Optional<Integer> min = emptyList.stream().min(Integer::compareTo);
Optional<Integer> max = emptyList.stream().max(Integer::compareTo);
// 打印结果,处理空流的情况
System.out.println("Min value: " + (min.isPresent() ? min.get() : "None"));
System.out.println("Max value: " + (max.isPresent() ? max.get() : "None"));
在这个例子中,由于emptyList
是空的,min
和max
方法将返回不包含任何值的Optional
对象。我们使用isPresent
方法来检查是否存在值,并相应地打印结果。
使用自定义对象
对于自定义对象,需要确保类实现了Comparable
接口,或者在调用min
和max
方法时提供一个Comparator
:
class Person implements Comparable<Person> {
private String name;
private int age;
// 构造器、getter和setter省略
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
List<Person> people = Arrays.asList(
// 初始化Person对象列表
);
// 获取最年轻和最年长的人
Optional<Person> youngest = people.stream().min(Comparator.comparingInt(Person::getAge));
Optional<Person> oldest = people.stream().max(Comparator.comparingInt(Person::getAge));
// 打印结果
youngest.ifPresent(p -> System.out.println("Youngest: " + p.getName()));
oldest.ifPresent(p -> System.out.println("Oldest: " + p.getName()));
在这个例子中,我们假设Person
类实现了Comparable
接口,根据age
属性进行比较。我们使用Comparator.comparingInt
方法引用来获取最年轻和最年长的人。
14、实现平铺数据
在Java中,实现简单平铺数据通常指的是将一个由集合组成的集合(即集合的集合,或称为嵌套集合)展平成一个单一的集合。Stream
API中的flatMap
方法非常适合这项任务,它可以将一个流中的每个元素(这些元素本身也是流)替换为组成元素的流,然后将所有的流连接起来形成一个流。
以下是使用flatMap
实现平铺数据的示例:
平铺列表的列表
假设我们有一个字符串列表的列表,我们想要将其平铺为一个单一的字符串列表:
List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("Java", "Kotlin"),
Arrays.asList("Scala", "Groovy")
);
List<String> flattenedList = listOfLists.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
// 输出: Java Kotlin Scala Groovy
flattenedList.forEach(System.out::println);
在这个例子中,listOfLists
是一个包含两个字符串列表的外部列表。我们使用flatMap
方法,它接受一个函数作为参数,该函数将每个内部列表转换成一个流,然后flatMap
将所有这些流连接起来形成一个单一的流。
平铺数组的数组
如果你有一个整数数组的数组,并且想要将其平铺为一个单一的整数数组,可以使用如下方式:
int[][] arrays = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
List<Integer> flattenedList = Arrays.stream(arrays)
.flatMapToInt(intArray -> Arrays.stream(intArray)) // 使用flatMapToInt展平数组
.boxed() // 将原始类型流转换为装箱类型流
.collect(Collectors.toList());
// 输出平铺后的列表
flattenedList.forEach(System.out::println);
在这个例子中,我们使用flatMapToInt
方法来处理原始类型数组,它避免了自动装箱的开销。然后我们使用boxed
方法将IntStream
中的整数转换为Integer
对象的流,最后收集到列表中。
flatMap
和flatMapToInt
是处理嵌套集合和数组的强大工具,它们允许你以声明式的方式将复杂的数据结构简化为更易于处理的形式。
15、实现对列表进行分组后获取每组最大值
@Data
class Person {
private String name;
private int age;
}
List<Person> peoples = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Alice", 12), // 重复
new Person("Charlie", 35),
new Person("Alice", 35) // 另一个重复
);
Map<String, Optional<Person>> groupedData = peoples .stream()
.collect(Collectors.groupingBy(
// 根据名称进行分组
Person::getName,
// 获取年龄最大的人
Collectors.maxBy(
Comparator.comparing(PartsInstockDetailOutBO::getAge))));
// 去掉Optional包装,获取结果
return groupedData.values().stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
16、处理大批量数据
处理大批量数据时,Java的Stream
API 提供了强大的功能,可以有效地进行并行处理以加速计算。以下是一些处理大批量数据的技巧和方法:
1. 使用并行流
将串行流转换为并行流可以显著加快处理速度,尤其是在多核处理器上:
List<String> largeDataSet = // ... 大批量数据
long wordCount = largeDataSet.parallelStream()
.filter(line -> line.contains("某个关键字"))
.count();
System.out.println("Count of lines with keyword: " + wordCount);
2. 使用无序流
如果顺序不重要,可以使用无序流来提高性能。无序流可以减少同步的开销。
long count = largeDataSet.parallelStream()
.unordered() // 指定流是无序的
.count();
17、两个列表比较,删除匹配上的列
在Java中,Stream API 提供的removeIf
方法可以用来移除满足某个条件的元素。这个方法属于 Collection 接口,并且是一个中间操作,它基于给定的谓词(通常是 Lambda 表达式)从集合中移除元素。
anyMatch
是 Java Stream API 中的一个终端操作,它用于检查流中是否存在至少一个元素满足给定的条件。这个方法通常与 Lambda 表达式结合使用,以便对流中的元素执行特定的判断逻辑。
@Data
class Person {
private String name;
private int position;
}
List<Person> person1 = Arrays.asList(
new Person("Alice", "Python工程师"),
new Person("Bob", "Java工程师"),
new Person("Charlie", "C++工程师")
);
List<Person> person2 = Arrays.asList(
new Person("Alice", "Python工程师"),
new Person("BiBo", "Go语言工程师")
);
// 删除person1与person2有关的数据
person1.removeIf(p1-> person2.stream()
.anyMatch(p2-> p1.getName().equals(p2.getName)));