前端列表展示、消息預覽、搜索高亮等場景,常需「顯示前 N 個字符」。但 Java String.substring 按 char (UTF-16 代碼單元) 計數,導致:
import java.util.Arrays;
public class ByteSubstringUtil {
/**
* @param str 輸入字符串
* @param charLen 期望顯示字符數(按「中文佔2、英文佔1」的視覺寬度估算)
* @return 截取後字符串,超長時尾部追加 ".."
*/
public static String subStringByVisualLength(String str, int charLen) {
if (str == null) return "";
int byteLen = charLen * 2; // 視覺長度換算為字節預算
byte[] bytes = str.getBytes(StandardCharsets.UTF_8); // 明確編碼
if (bytes.length <= byteLen) return str; // 無需截取
// 1. 先按字節硬截取
byte[] truncated = Arrays.copyOf(bytes, byteLen);
// 2. 計算截取區間內的「負字節數」(UTF-8 中文高位為 1)
int negativeCount = 0;
for (byte b : truncated) {
if (b < 0) negativeCount++;
}
// 3. 若負字節數為奇數,說明切斷了雙字節字符,需向後擴展 1 字節
if (negativeCount % 2 != 0) {
if (byteLen < bytes.length) {
truncated = Arrays.copyOf(bytes, byteLen + 1);
}
}
return new String(truncated, StandardCharsets.UTF_8) + "..";
}
// 測試
public static void main(String[] args) {
String s = "2024年中秋節快樂 Hello";
System.out.println(subStringByVisualLength(s, 10)); // 2024年中秋..
System.out.println(subStringByVisualLength(s, 20)); // 2024年中秋節快樂 He..
}
}
| 字符類型 | UTF-8 字節 | 首字節最高位 | 負字節判定 |
|---|---|---|---|
| ASCII (英文/數字) | 1 字節 | 0 (0xxxxxxx) | b >= 0 |
| 中文 (BMP) | 3 字節 | 1 (1110xxxx) | b < 0 |
| Emoji (補充平面) | 4 字節 | 1 (11110xxx) | b < 0 |
UTF-8 多字節字符的 所有字節最高位均為 1(即 Java byte 為負數)。統計截取區間負字節總數:
public static String subStringExactChars(String str, int maxChars, String suffix) {
if (str == null) return "";
int count = 0;
int byteEnd = 0;
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < bytes.length; ) {
int charLen = 1;
if (bytes[i] < 0) { // 多字節字符
// 簡易判斷:連續負字節屬於同一字符
while (i + charLen < bytes.length && bytes[i + charLen] < 0) charLen++;
}
if (count + 1 > maxChars) break;
count++;
i += charLen;
byteEnd = i;
}
return new String(bytes, 0, byteEnd, StandardCharsets.UTF_8)
+ (count < maxChars ? "" : (suffix != null ? suffix : ".."));
}
核心邏輯:UTF-8 編碼 → 按字節預算截取 → 統計負字節奇偶性修正邊界。該方案無需引入第三方庫,僅依賴 JDK 基礎 API,適用於對性能敏感、中英混排的截斷場景。若需嚴格按「字符數」而非視覺寬度截取,請使用進階版逐字符解碼邏輯。