字符串格式中的命名佔位符
一、概述
Java 標準庫提供了String.format()
方法來格式化基於模板的字符串,例如: String.format(“%s is awesome”, “Java”)
。
在本教程中,我們將探討如何使字符串格式支持命名參數。
2. 問題介紹
String.format()
方法使用起來非常簡單。但是,當format()
調用有很多參數時,很難理解哪個值將來自哪個格式說明符,例如:
Employee e = ...; // get an employee instance
String template = "Firstname: %s, Lastname: %s, Id: %s, Company: %s, Role: %s, Department: %s, Address: %s ...";
String.format(template, e.firstName, e.lastName, e.Id, e.company, e.department, e.role ... )
此外,當我們將這些參數傳遞給方法時,它很容易出錯。例如,在上面的示例中,我們錯誤地將e.department
放在了e.role
之前。
因此,如果我們可以在模板中使用命名參數之類的東西,然後通過包含所有參數name->value
映射的Map
應用格式,那就太好了:
String template = "Firstname: ${firstname}, Lastname: ${lastname}, Id: ${id} ...";
ourFormatMethod.format(template, parameterMap);
在本教程中,我們將首先看一個使用流行的外部庫的解決方案,它可以解決這個問題的大多數情況。然後,我們將討論一個破壞解決方案的邊緣情況。
最後,我們將創建自己的format()
方法來涵蓋所有情況。
為簡單起見,我們將使用單元測試斷言來驗證方法是否返回預期的字符串。
還值得一提的是,在本教程中我們將只關注簡單的字符串格式 ( %s
) 。不支持其他格式類型,例如日期、數字或具有定義寬度和精度的格式。
3. 使用 Apache Commons Text StrSubstitutor
Apache Commons Text 庫包含許多用於處理字符串的便捷實用程序。它附帶StrSubstitutor
,它允許我們根據命名參數進行字符串替換。
首先,讓我們將該庫作為新的依賴項添加到我們的 Maven 配置文件中:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
當然,我們總能在 Maven 中央存儲庫中找到最新版本。
在我們了解如何使用StrSubstitutor
類之前,讓我們創建一個模板作為示例:
String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";
接下來,讓我們創建一個測試,使用StrSubstitutor
基於上面的模板構建一個字符串:
Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");
如測試代碼所示,我們讓params
保存所有name -> value
映射。當我們調用StrSubstitutor.replace()
方法時,除了template
和params,
我們還傳遞前綴和後綴來告知StrSubstitutor
參數在模板中包含什麼。 StrSubstitutor
將搜索參數名稱的prefix + map.entry.key + suffix
。
當我們運行測試時,它通過了。所以, StrSubstitutor
似乎解決了這個問題。
4. 邊緣案例:當替換包含佔位符時
我們已經看到StrSubstitutor.replace()
測試通過了我們的基本用例。但是,某些特殊情況不在測試範圍內。例如,參數值可能包含參數名稱模式“ ${ … }
”。
現在,讓我們測試一下這個案例:
Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");
在上面的測試中,參數“ ${text}
”的值包含文本“ ${number}
”。所以,我們期望“ ${text}
”被文本“ ${number}
”替換。
但是,如果我們執行它,測試就會失敗:
org.opentest4j.AssertionFailedError:
expected: "Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]"
but was: "Text: ['42' is a placeholder.] Number: [42] Text again: ['42' is a placeholder.]"
因此, StrSubstitutor
將文字${number}
視為參數佔位符。
事實上, StrSubstitutor
的 Javadoc 已經說明了這種情況:
變量替換以遞歸方式工作。因此,如果一個變量值包含一個變量,那麼該變量也將被替換。
發生這種情況是因為,在每個遞歸步驟中, StrSubstitutor
將最後一個替換結果作為新template
繼續進行進一步的替換。
為了繞過這個問題,我們可以選擇不同的前綴和後綴,這樣它們就不會受到干擾:
String TEMPLATE = "Text: [%{text}] Number: [%{number}] Text again: [%{text}]";
Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "%{", "}");
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");
但是,從理論上講,由於我們無法預測值,因此值總是可能包含參數名稱模式並干擾替換。
接下來,讓我們創建自己的format()
方法來解決問題。
5. 自行構建格式化程序
我們已經討論了為什麼StrSubstitutor
不能很好地處理邊緣情況。因此,如果我們創建一個方法,困難在於我們不應該使用循環或遞歸來將最後一步的結果作為當前步驟的新輸入。
5.1。解決問題的思路
這個想法是我們在模板中搜索參數名稱模式。但是,當我們找到一個時,我們不會立即將其替換為地圖中的值。相反,我們構建了一個可用於標準String.format()
方法的新模板。如果我們舉個例子,我們將嘗試轉換:
String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";
Map<String, Object> params ...
進入:
String NEW_TEMPLATE = "Text: [%s] Number: [%s] Text again: [%s]";
List<Object> valueList = List.of("'${number}' is a placeholder.", 42, "'${number}' is a placeholder.");
然後,我們可以調用String.format(NEW_TEMPLATE, valueList.toArray());
完成工作。
5.2.創建方法
接下來,讓我們創建一個方法來實現這個想法:
public static String format(String template, Map<String, Object> parameters) {
StringBuilder newTemplate = new StringBuilder(template);
List<Object> valueList = new ArrayList<>();
Matcher matcher = Pattern.compile("[$][{](\\w+)}").matcher(template);
while (matcher.find()) {
String key = matcher.group(1);
String paramName = "${" + key + "}";
int index = newTemplate.indexOf(paramName);
if (index != -1) {
newTemplate.replace(index, index + paramName.length(), "%s");
valueList.add(parameters.get(key));
}
}
return String.format(newTemplate.toString(), valueList.toArray());
}
上面的代碼非常簡單。讓我們快速瀏覽一下以了解它是如何工作的。
首先,我們聲明了兩個新變量來保存新模板 ( newTemplate
) 和值列表 ( valueList
)。稍後我們調用String.format()
時將需要它們。
我們使用Regex
在模板中定位參數名稱模式。然後,我們將參數名稱模式替換為“%s”
,並將相應的值添加到valueList
變量中。
最後,我們使用新轉換的模板和valueList.
的值調用String.format()
。
為簡單起見,我們在方法中硬編碼了前綴“ ${
”和後綴“ }
”。此外,如果未提供參數“ ${unknown}
”的值,我們只需將“ ${unknown}
”參數替換為“ null
” 。
5.3.測試我們的format()
方法
接下來,讓我們測試該方法是否適用於常規情況:
Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");
同樣,如果我們試一試,測試就會通過。
當然,我們想看看它是否也適用於邊緣情況:
params.put("text", "'${number}' is a placeholder.");
result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");
如果我們執行這個測試,它也通過了!我們已經解決了這個問題。
六,結論
在本文中,我們探討瞭如何從一組值中替換基於模板的字符串中的參數。基本上,Apache Commons Text 的StrSubstitutor.replace()
方法使用起來非常簡單,可以解決大多數情況。但是,當值包含參數名稱模式時, StrSubstitutor
可能會產生意外結果。
因此,我們實現了一個format()
方法來解決這種極端情況。
與往常一樣,示例的完整源代碼可在 GitHub 上獲得。