1. 概述

在实际Java开发中,我们经常需要根据字符串关键字过滤对象列表。具体来说,就是检查对象的任意字段是否包含给定的字符串,实现全文搜索功能。

本文将介绍几种在Java中实现多字段匹配过滤的方法,从简单到通用,满足不同场景需求。

2. 问题引入

先通过一个具体案例理解需求。假设我们有一个Book类,包含titletagsintropages字段:

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列表中搜索关键字,匹配titletagsintro字段。例如:

  • 搜索"Java" → 返回JAVAKOTLINJAVA.titleKOTLIN.tags包含)
  • 搜索"Art" → 返回JAVAGUITARJAVA.titleGUITAR.tags包含)
  • 搜索"Let's" → 返回KOTLINGUITARKOTLIN.titleGUITAR.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;
}

核心机制

  1. 使用反射获取所有字段
  2. 跳过excludedFields指定的字段
  3. 递归检查嵌套对象
  4. 通过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);

高级用法:排除tagspages字段,仅搜索titleintro

List<Book> byArtExcludeTag = fullTextSearchByReflection(BOOKS, "Art", "tags", "pages");
assertThat(byArtExcludeTag).containsExactlyInAnyOrder(JAVA);

⚠️ 注意事项

  • 反射会带来性能开销
  • 需处理安全异常
  • 适合动态字段场景,静态字段建议用方案4

6. 总结

本文介绍了三种在Java中实现多字段匹配过滤的方案:

方案 适用场景 优势 劣势
Stream.filter() 字段少且固定 实现简单 字段多时代码冗长
字符串表示法 字段结构稳定 逻辑清晰,易维护 需手动维护合并逻辑
反射通用法 动态字段/嵌套对象 高度灵活,通用性强 性能开销,复杂度较高

根据实际需求选择合适方案:

  • 简单场景 → 方案3
  • 字段固定但较多 → 方案4
  • 需要动态搜索/嵌套对象 → 方案5

完整示例代码可在GitHub获取。


原始标题:Filter a List by Any Matching Field | Baeldung