按任何匹配字段過濾列表
1. 概述
在許多現實世界的 Java 應用程式中,我們經常需要透過檢查物件的任何欄位是否與給定的String
相符來過濾物件List
。換句話說,我們想要搜尋一個String
並過濾在其任何屬性中匹配的物件。
在本教程中,我們將介紹透過 Java 中的任何符合欄位過濾List
的不同方法。
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 and setter methods are omitted
}
接下來,讓我們使用定義的建構函式建立四個Book
實例,並將它們放入List<Book>
中:
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**
BOOKS
包含關鍵字String
所有Book
物件。換句話說,我們想對BOOKS
執行全文搜尋。
例如,如果我們要搜尋“Java”,
結果中應該包含JAVA and KOTLIN
實例。這是因為JAVA.title
包含“Java”
,而KOTLIN.tags
包含“Java”.
同樣,如果我們搜尋“Art
”,我們期望找到JAVA
和GUITAR
,因為JAVA.title
和GUITAR.tags
包含單字“Art.
” 當“ Let's
” 是關鍵字, KOTLIN
和GUITAR
應該會出現在結果中,因為KOTLIN.title
和GUITAR.intro
包含關鍵字。
接下來,我們來探討如何解決這個問題,並使用這三個關鍵字範例來檢查我們的解決方案。
為簡單起見,我們假設所有Book
的屬性都不為null
,並且我們將利用單元測試斷言來驗證我們的解決方案是否按預期工作。
接下來,讓我們深入研究程式碼。
3.使用Stream.filter()
方法
Stream API提供了方便的filter()
方法,它允許我們**輕鬆地**透過**lambda表達式過濾**Stream**
中的物件。**
接下來我們用這種方法來解決全文搜尋問題:
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();
}
正如我們所看到的,上面的實作非常簡單。在傳遞給filter()
lambda 表達式中,我們檢查是否有任何屬性包含該關鍵字。
值得一提的是,由於Book.tags
是一個List<String>
,我們利用Stream.anyMatch()
來檢查List
中是否有任何標籤包含該關鍵字。
接下來我們來測試一下這種方法是否能正常運作:
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);
如果我們運行一下,測試就會通過。
在這個範例中,我們只需要檢查Book
類別中的三個屬性。然而,全文搜尋可能會在實際應用程式中檢查類別的十幾個屬性。在這種情況下,lambda 表達式會相當長,並且會使Stream
管道難以讀取和維護。
當然,我們可以提取lambda表達式作為求解的方法。或者,我們可以建立一個函數來產生全文搜尋的String
表示形式。
接下來,讓我們仔細看看這個方法。
4. 建立用於過濾的String
表示形式
我們知道toString()
傳回物件的String
表示形式。同樣,我們可以創建一個方法來提供物件的String
表示形式以進行全文搜尋:
class Book {
// ... unchanged codes omitted
public String strForFiltering() {
String tagsStr = String.join("\n", tags);
return String.join("\n", title, intro, tagsStr);
}
}
如上面的程式碼所示, strForFiltering()
函數將所有全文搜尋所需的String
值連接到換行符分隔的String.
如果我們以KOTLIN
為例,則該方法傳回以下String
:
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);
在此範例中,我們使用 Java 文字區塊來呈現多行String
。
那麼,全文搜尋對我們來說將是一件容易的事。我們只檢查book.strForFilter()'s
結果是否包含keyword
:
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);
測試通過。因此,這種方法可以完成任務。
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) {
//ignore reflection exceptions
}
}
}
return false;
}
fullTextSearchOnObject()
方法接受三個參數:我們要執行搜尋的物件、關鍵字和排除的欄位名稱。
此實作使用反射來檢索物件的所有欄位。然後,我們循環遍歷字段,使用Stream.nonMatch()
跳過excludedFields
,並透過field.get(obj)
取得字段的值。由於我們的目標是執行基於String
的搜索,因此我們使用toString()
將值轉換為String
並檢查欄位的值是否包含搜尋字詞。
我們的物件可能包含嵌套物件。因此,我們遞歸地檢查嵌套物件的欄位。如果任何欄位是物件(而不是基元或String
),它會在該欄位上呼叫fullTextSearchOnObject()
,使我們能夠搜尋深度嵌套的結構。
現在,我們可以使用fullTextSearchOnObject()
來建立一個方法來全文篩選Book
物件List
:
List<Book> fullTextSearchByReflection(List<Book> books, String keyword, String... excludeFields) {
return books.stream().filter(book -> fullTextSearchOnObject(book, keyword, excludeFields)).toList();
}
接下來,讓我們執行相同的測試來驗證此方法是否按預期工作:
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);
正如我們所看到的,我們在上面的測試中將“ pages
”作為排除欄位傳遞。如果需要,我們可以輕鬆地擴展自訂全文搜尋的排除欄位:
List<Book> byArtExcludeTag = fullTextSearchByReflection(BOOKS, "Art", "tags", "pages");
assertThat(byArtExcludeTag).containsExactlyInAnyOrder(JAVA);
此範例展示如何僅對title
和intro
欄位執行全文搜尋。由於GUITAR
tags
中僅包含搜尋關鍵字“ Art
”,因此它會被過濾掉。
使用反射和遞歸,我們可以在 Java 物件上實現全文搜索,檢查給定String
關鍵字的所有欄位(包括巢狀欄位)。這種方法允許我們動態搜尋物件的字段,而無需明確了解類別的結構。
六、結論
在本文中,我們探索了透過與 Java 中的String
相符的任何欄位來過濾List
的不同解決方案。
這些技術將幫助我們編寫更乾淨、更易於維護的程式碼,同時提供搜尋和過濾 Java 物件的強大方法。
與往常一樣,範例的完整原始程式碼可在 GitHub 上取得。