在中文系統中,字串截取常遇到「半個漢字」問題:UTF-8 編碼下一個中文字佔 3 位元組,Big5/GBK 佔 2 位元組。若直接按位元組截取,極易產生亂碼。本文提供嚴謹的按位元組長度截取實作,確保不會切斷多位元組字元。
char[])<= 255)或多位元組字元(中文、日文等)package com.haohui.common.utils;
/**
* 字串工具類:提供按位元組長度安全截取
*/
public class StringTool {
/**
* 截取指定位元組長度的字串,不會產生半個漢字
* @param str 原始字串
* @param length 目標位元組長度(雙位元組編碼下:1 中文 = 2 位元組)
* @return 截取後字串,若長度不足回傳空字串
* @example 網頁最多顯示 17 個中文 → length = 34
* StringTool.getSubString(str, 34);
*/
public static String getSubString(String str, int length) {
if (str == null || length <= 0) {
return "";
}
int byteCount = 0;
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
// 簡易判斷:字元值 > 255 視為雙位元組字元(中日韓文字)
if (chars[i] > 255) {
byteCount += 2;
} else {
byteCount += 1;
}
// 精準匹配長度
if (byteCount == length) {
return str.substring(0, i + 1);
}
// 超出長度且最後一個是雙位元組字元 → 退回上一字元
if (byteCount == length + 1 && chars[i] > 255) {
return str.substring(0, i);
}
}
// 原字串位元組長度小於目標長度
return str;
}
}
/**
* UTF-8 編碼環境下的位元組長度截取
* @param str 原始字串
* @param byteLength 目標位元組長度
* @return 截取後字串
*/
public static String getSubStringUtf8(String str, int byteLength) {
if (str == null || byteLength <= 0) return "";
try {
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
if (bytes.length <= byteLength) return str;
// 從目標位置向前尋找合法 UTF-8 字元邊界
int end = byteLength;
while (end > 0 && (bytes[end] & 0xC0) == 0x80) { // 10xxxxxx 為續位元組
end--;
}
return new String(bytes, 0, end, StandardCharsets.UTF_8);
} catch (Exception e) {
return str.substring(0, Math.min(str.length(), byteLength)); // 兜底
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class StringToolTest {
@Test void testGbkSubstring() {
String str = "測試字串ABC"; // 4中文(8bytes) + 3英文(3bytes) = 11 bytes
assertEquals("測試", StringTool.getSubString(str, 4)); // 2中文 = 4 bytes
assertEquals("測試字", StringTool.getSubString(str, 6)); // 3中文 = 6 bytes
assertEquals("測試字串", StringTool.getSubString(str, 8)); // 4中文 = 8 bytes
assertEquals("測試字串A", StringTool.getSubString(str, 9)); // +1 ASCII
assertEquals("測試字串ABC", StringTool.getSubString(str, 11)); // 全部
assertEquals("測試字串ABC", StringTool.getSubString(str, 20)); // 超過長度回傳原字串
}
@Test void testUtf8Substring() {
String str = "測試ABC"; // 2中文(6bytes) + 3英文(3bytes) = 9 bytes
assertEquals("測", StringTool.getSubStringUtf8(str, 3)); // 1中文 = 3 bytes
assertEquals("測試", StringTool.getSubStringUtf8(str, 6)); // 2中文 = 6 bytes
assertEquals("測試A", StringTool.getSubStringUtf8(str, 7)); // +1 ASCII
}
@Test void testEdgeCases() {
assertEquals("", StringTool.getSubString(null, 10));
assertEquals("", StringTool.getSubString("ABC", 0));
assertEquals("ABC", StringTool.getSubString("ABC", 10)); // 不足長度回傳原字串
}
}
| 方法 | 問題 | 建議 |
|---|---|---|
str.substring(0, n) |
按 char 計數,中英文混雜時顯示寬度不一 | 僅適用純英文或固定字元數需求 |
new String(str.getBytes("GBK"), 0, len) |
若切在雙位元組中間會拋出異常或產生替換字元 | 需搭配邊界檢查,不如自行實作直觀 |
Apache Commons Lang StringUtils.leftBytes() |
依賴第三方庫,行為與編碼相關 | 專案已引入 Commons Lang 可直接使用 |
Java 9+ StringBuilder.setLength() + 字元寬度計算 |
需自行實作字元寬度邏輯(半形/全形) | 顯示層面截取建議用 ICU4J BreakIterator |
按位元組截取的核心在於字元邊界識別。雙位元組編碼(Big5/GBK)可用簡易 char > 255 判斷;UTF-8 必須檢查位元組標記位(10xxxxxx)。生產環境建議統一使用 UTF-8 並採用 getSubStringUtf8 版本,或引入 ICU4J 處理更複雜的書寫系統(如表情符號、複合字元)。