1. 概述
在实际Java开发中,我们经常需要根据字符串关键字过滤对象列表。具体来说,就是检查对象的任意字段是否包含给定的字符串,实现全文搜索功能。
本文将介绍几种在Java中实现多字段匹配过滤的方法,从简单到通用,满足不同场景需求。
2. 问题引入
先通过一个具体案例理解需求。假设我们有一个Book
类,包含title
、tags
、intro
和pages
字段:
class Book {
private String title;
private List<String> tags;
private String intro;
private int pages;
public Book(String title, List<String> tags, String intro, int pages) {
this.title = title;
this.tags = tags;
this.intro = intro;
this.pages = pages;
}
// getter和setter方法省略
}
接下来创建测试数据:
static final Book JAVA = new Book(
"The Art of Java Programming",
List.of("Tech", "Java"),
"Java is a powerful programming language.",
400);
static final Book KOTLIN = new Book(
"Let's Dive Into Kotlin Codes",
List.of("Tech", "Java", "Kotlin"),
"It is big fun learning how to write Kotlin codes.",
300);
static final Book PYTHON = new Book(
"Python Tricks You Should Know",
List.of("Tech", "Python"),
"The path of being a Python expert.",
200);
static final Book GUITAR = new Book(
"How to Play a Guitar",
List.of("Art", "Music"),
"Let's learn how to play a guitar.",
100);
static final List<Book> BOOKS = List.of(JAVA, KOTLIN, PYTHON, GUITAR);
需求:在BOOKS
列表中搜索关键字,匹配title
、tags
或intro
字段。例如:
- 搜索"Java" → 返回
JAVA
和KOTLIN
(JAVA.title
和KOTLIN.tags
包含) - 搜索"Art" → 返回
JAVA
和GUITAR
(JAVA.title
和GUITAR.tags
包含) - 搜索"Let's" → 返回
KOTLIN
和GUITAR
(KOTLIN.title
和GUITAR.intro
包含)
为简化问题,我们假设所有字段非空,并通过单元测试验证方案。
3. 使用Stream.filter()方法
Stream API的filter()
方法结合lambda表达式是解决此类问题的简单粗暴方案:
List<Book> fullTextSearchByLogicalOr(List<Book> books, String keyword) {
return books.stream()
.filter(book -> book.getTitle().contains(keyword)
|| book.getIntro().contains(keyword)
|| book.getTags().stream().anyMatch(tag -> tag.contains(keyword)))
.toList();
}
✅ 关键点:
- 直接在lambda中检查所有字段
- 对
List<String>
类型的tags
字段,使用anyMatch()
检查是否包含关键字
测试验证:
List<Book> byJava = fullTextSearchByLogicalOr(BOOKS, "Java");
assertThat(byJava).containsExactlyInAnyOrder(JAVA, KOTLIN);
List<Book> byArt = fullTextSearchByLogicalOr(BOOKS, "Art");
assertThat(byArt).containsExactlyInAnyOrder(JAVA, GUITAR);
List<Book> byLets = fullTextSearchByLogicalOr(BOOKS, "Let's");
assertThat(byLets).containsExactlyInAnyOrder(KOTLIN, GUITAR);
⚠️ 踩坑提示:当需要检查的字段很多时,lambda表达式会变得冗长,影响可读性。这时可以考虑提取方法或使用下文方案。
4. 创建用于过滤的字符串表示
我们可以为对象生成一个专用的搜索字符串,简化过滤逻辑:
class Book {
// 其他代码不变
public String strForFiltering() {
String tagsStr = String.join("\n", tags);
return String.join("\n", title, intro, tagsStr);
}
}
该方法将所有需要搜索的字段合并为换行符分隔的字符串。例如KOTLIN
对象会生成:
String expected = """
Let's Dive Into Kotlin Codes
It is big fun learning how to write Kotlin codes.
Tech
Java
Kotlin""";
assertThat(KOTLIN.strForFiltering()).isEqualTo(expected);
过滤方法变得极其简洁:
List<Book> fullTextSearchByStrForFiltering(List<Book> books, String keyword) {
return books.stream()
.filter(book -> book.strForFiltering().contains(keyword))
.toList();
}
测试验证同样通过:
List<Book> byJava = fullTextSearchByStrForFiltering(BOOKS, "Java");
assertThat(byJava).containsExactlyInAnyOrder(JAVA, KOTLIN);
List<Book> byArt = fullTextSearchByStrForFiltering(BOOKS, "Art");
assertThat(byArt).containsExactlyInAnyOrder(JAVA, GUITAR);
List<Book> byLets = fullTextSearchByStrForFiltering(BOOKS, "Let's");
assertThat(byLets).containsExactlyInAnyOrder(KOTLIN, GUITAR);
✅ 优势:
- 逻辑清晰,易于维护
- 添加新字段只需修改
strForFiltering()
方法
5. 创建通用的全文搜索方法
通过反射实现通用方案,支持任意对象和排除特定字段:
boolean fullTextSearchOnObject(Object obj, String keyword, String... excludedFields) {
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if (Arrays.stream(excludedFields).noneMatch(exceptName -> exceptName.equals(field.getName()))) {
field.setAccessible(true);
try {
Object value = field.get(obj);
if (value != null) {
if (value.toString().contains(keyword)) {
return true;
}
if (!field.getType().isPrimitive() && !(value instanceof String)
&& fullTextSearchOnObject(value, keyword, excludedFields)) {
return true;
}
}
} catch (InaccessibleObjectException | IllegalAccessException ignored) {
//忽略反射异常
}
}
}
return false;
}
✅ 核心机制:
- 使用反射获取所有字段
- 跳过
excludedFields
指定的字段 - 递归检查嵌套对象
- 通过
toString()
转换为字符串匹配
封装为List过滤方法:
List<Book> fullTextSearchByReflection(List<Book> books, String keyword, String... excludeFields) {
return books.stream().filter(book -> fullTextSearchOnObject(book, keyword, excludeFields)).toList();
}
基础测试(排除pages
字段):
List<Book> byJava = fullTextSearchByReflection(BOOKS, "Java", "pages");
assertThat(byJava).containsExactlyInAnyOrder(JAVA, KOTLIN);
List<Book> byArt = fullTextSearchByReflection(BOOKS, "Art", "pages");
assertThat(byArt).containsExactlyInAnyOrder(JAVA, GUITAR);
List<Book> byLets = fullTextSearchByReflection(BOOKS, "Let's", "pages");
assertThat(byLets).containsExactlyInAnyOrder(KOTLIN, GUITAR);
高级用法:排除tags
和pages
字段,仅搜索title
和intro
:
List<Book> byArtExcludeTag = fullTextSearchByReflection(BOOKS, "Art", "tags", "pages");
assertThat(byArtExcludeTag).containsExactlyInAnyOrder(JAVA);
⚠️ 注意事项:
- 反射会带来性能开销
- 需处理安全异常
- 适合动态字段场景,静态字段建议用方案4
6. 总结
本文介绍了三种在Java中实现多字段匹配过滤的方案:
方案 | 适用场景 | 优势 | 劣势 |
---|---|---|---|
Stream.filter() | 字段少且固定 | 实现简单 | 字段多时代码冗长 |
字符串表示法 | 字段结构稳定 | 逻辑清晰,易维护 | 需手动维护合并逻辑 |
反射通用法 | 动态字段/嵌套对象 | 高度灵活,通用性强 | 性能开销,复杂度较高 |
根据实际需求选择合适方案:
- 简单场景 → 方案3
- 字段固定但较多 → 方案4
- 需要动态搜索/嵌套对象 → 方案5
完整示例代码可在GitHub获取。