
在Java Stream API中,我们经常需要对对象集合进行过滤操作。一个常见的需求是,根据对象的某个特定属性(例如,Animal对象的color属性)来筛选流,但最终过滤出的结果仍然是完整的原始对象(即Animal对象本身),而不是该属性的值。
例如,我们有一个Animal对象的流animalStream,希望筛选出所有颜色为绿色的动物。直观上可能会想到以下两种方式:
-
直接使用filter与方法引用:
Stream
swimmingAnimalStream = animalStream .filter(Animal::canSwim); // 过滤所有会游泳的动物 这种方式简单明了,当过滤条件可以直接通过方法引用表达时非常高效。
立即学习“Java免费学习笔记(深入)”;
-
使用filter与Lambda表达式:
Stream
greenAnimals = animalStream .filter(animal -> animal.getColor().equals(Colors.GREEN)); // 过滤所有颜色为绿色的动物 当过滤条件稍微复杂,需要访问对象内部属性并进行比较时,Lambda表达式提供了极大的灵活性。
然而,有时开发者可能希望在过滤前先“映射”出需要判断的属性,然后再进行过滤,但又不想丢失原始对象的信息。例如,以下操作虽然可以过滤出绿色,但最终流中只剩下颜色信息,而非原始的Animal对象:
animalStream
.map(Animal::getColor) // 将Animal映射为Colors
.filter(Colors.GREEN::equals); // 过滤颜色为绿色的Colors,结果流为Stream这种方式会改变流中元素的类型,不符合“保留原始完整对象”的需求。
为了解决这一问题,同时避免提取辅助方法,Java Stream API提供了多种策略。
方案一:直接使用Stream#filter结合Lambda表达式(推荐用于简单场景)
对于大多数基于对象内部属性进行过滤的需求,最简洁、最符合Stream声明式风格的解决方案是直接在filter操作中使用Lambda表达式。这种方法将属性的获取和条件的判断逻辑封装在一个Lambda中,确保了流的类型不变。
示例代码:
import java.util.List;
import java.util.stream.Stream;
// 假设有Animal类和Colors枚举
class Animal {
enum Colors { GREEN, BLUE, RED }
private Colors color;
private String name;
// ... 构造函数, getter等
public Colors getColor() { return color; }
public String getName() { return name; }
public Animal(String name, Colors color) {
this.name = name;
this.color = color;
}
@Override
public String toString() {
return "Animal{name='" + name + "', color=" + color + '}';
}
}
public class StreamFilterExample {
public static void main(String[] args) {
List animals = List.of(
new Animal("Frog", Animal.Colors.GREEN),
new Animal("Parrot", Animal.Colors.BLUE),
new Animal("Chameleon", Animal.Colors.GREEN),
new Animal("Crab", Animal.Colors.RED)
);
Stream animalStream = animals.stream();
// 过滤颜色为绿色的动物,并保留原始Animal对象
Stream greenAnimalsStream = animalStream
.filter(animal -> Animal.Colors.GREEN.equals(animal.getColor()));
greenAnimalsStream.forEach(System.out::println);
// 输出:
// Animal{name='Frog', color=GREEN}
// Animal{name='Chameleon', color=GREEN}
}
} 优点:
- 简洁易读:代码逻辑清晰,一眼就能看出过滤的条件。
- 符合Stream风格:完全遵循Stream API的声明式编程范式。
- 无需额外库:使用Java标准库即可实现。
适用场景: 当过滤条件可以通过一个简单的Lambda表达式直接访问对象属性并进行判断时,此方法是首选。
方案二:利用Stream#mapMulti实现灵活过滤(Java 16+)
对于更复杂、需要精细控制元素发射,或者希望在单个中间操作中完成映射和过滤的场景,Java 16引入的Stream#mapMulti提供了一个强大的替代方案。mapMulti允许你根据任意逻辑,将输入流中的每个元素转换为零个、一个或多个输出元素。这使得它非常适合实现条件性地“接受”或“拒绝”原始对象的需求。
mapMulti方法接收一个BiConsumer
示例代码:
import java.util.List;
import java.util.stream.Stream;
import java.util.Optional;
// 假设有Animal类,并增加getMother方法
class Animal {
enum Colors { GREEN, BLUE, RED }
private Colors color;
private String name;
private Optional mother; // 假设动物可能有母亲
public Animal(String name, Colors color) {
this(name, color, Optional.empty());
}
public Animal(String name, Colors color, Optional mother) {
this.name = name;
this.color = color;
this.mother = mother;
}
public Colors getColor() { return color; }
public Optional getMother() { return mother; }
public String getName() { return name; }
@Override
public String toString() {
return "Animal{name='" + name + "', color=" + color + ", hasMother=" + mother.isPresent() + '}';
}
}
public class StreamMapMultiFilterExample {
public static void main(String[] args) {
Animal motherAnimal = new Animal("Mama", Animal.Colors.BLUE);
List animals = List.of(
new Animal("Frog", Animal.Colors.GREEN),
new Animal("Parrot", Animal.Colors.BLUE),
new Animal("Chameleon", Animal.Colors.GREEN, Optional.of(motherAnimal)),
new Animal("Crab", Animal.Colors.RED)
);
Stream animalStream = animals.stream();
// 使用mapMulti过滤颜色为绿色的动物
Stream greenAnimalsMapMultiStream = animalStream
.mapMulti((animal, consumer) -> {
if (Animal.Colors.GREEN.equals(animal.getColor())) { // 条件判断
consumer.accept(animal); // 如果满足条件,则将原始Animal对象发射到下游
}
});
greenAnimalsMapMultiStream.forEach(System.out::println);
// 输出:
// Animal{name='Frog', color=GREEN, hasMother=false}
// Animal{name='Chameleon', color=GREEN, hasMother=true}
System.out.println("\n--- 过滤有母亲的动物 ---");
Stream animalWithMotherStream = animals.stream()
.mapMulti((animal, consumer) -> {
if (animal.getMother().isPresent()) { // 检查是否有母亲
consumer.accept(animal); // 如果有,发射原始Animal对象
}
});
animalWithMotherStream.forEach(System.out::println);
// 输出:
// Animal{name='Chameleon', color=GREEN, hasMother=true}
}
} Stream#mapMulti的优点:
- 性能优势:在某些情况下,mapMulti可以避免map和filter等多个中间操作链带来的额外开销,因为它将转换和过滤逻辑合并到单个遍历中。
-
高度灵活性:
- 条件发射:可以根据任意复杂的条件决定是否发射元素,完美实现过滤效果。
- 一对多转换:consumer.accept()可以被调用多次,允许一个输入元素产生多个输出元素。这对于需要将一个复杂对象分解为多个子元素,或根据不同条件生成不同输出的场景非常有用。
- 保持原始对象:通过consumer.accept(animal),可以轻松地将原始对象传递到下游流,满足了核心需求。
Stream#mapMulti的缺点:
- 声明式风格减弱:与filter的简洁声明式相比,mapMulti内部的BiConsumer逻辑更偏向命令式,可能导致代码可读性略有下降,尤其对于简单的过滤任务。
- 版本限制:仅适用于Java 16及更高版本。
使用场景:
- 当需要结合映射和过滤逻辑,并希望在一个中间操作中完成,以优化性能时。
- 当过滤条件非常复杂,或者需要根据条件一个输入元素生成零个、一个或多个输出元素时。
- 当希望避免链式map().filter()操作可能引入的细微性能损耗时。
总结
在Java Stream中根据对象的某个属性进行过滤并保留原始对象,主要有两种策略:
- 对于简单直接的过滤条件:推荐使用Stream#filter结合Lambda表达式。它简洁、易读,且符合Stream的声明式风格,适用于绝大多数场景。
- 对于Java 16及以上版本,且面临复杂过滤逻辑、性能优化需求或需要一对多转换的场景:Stream#mapMulti提供了一个强大且灵活的解决方案。它允许在单个中间操作中实现精细的元素控制和条件发射,但代码风格会略微偏向命令式。
选择哪种方法取决于您的具体需求、Java版本以及对代码可读性和性能优化的权衡。对于日常开发中的常规过滤任务,filter操作通常是最佳选择。










