一、背景
有這樣一個需求:如果一個字元串超過某個長度,則超過該長度的部分用省略号代替。
很多人會覺得這 so easy,有點 Java基礎的同學都可以簡單編寫出來。
那麼我們來分析這個簡單的問題。
二、編碼
2.1 思路
思路很簡單,判斷size 是否小于字元串長度,如果小于,則超過部分替換為 ... 即可。
2.2 編碼
我們編碼要多考慮一些:
- 為了健壯性,我們要進行參數校驗;
- 另外如果想寫一個完善的工具類,可以支援自定義省略符;
我們來編寫工具類:
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
public class StringUtil {
/**
* 超過 maxSize 的部分用省略号代替
*
* @param originStr 原始字元串
* @param maxSize 最大長度
*/
public static String abbreviate(String originStr, int maxSize) {
return abbreviate(originStr, maxSize, null);
}
/**
* 超過 maxSize 的部分用省略号代替
*
* @param originStr 原始字元串
* @param maxSize 最大長度
* @param abbrevMarker 省略符
*/
public static String abbreviate(String originStr, int maxSize, String abbrevMarker) {
Preconditions.checkArgument(maxSize > 0, "size 必須大于0");
if (StringUtils.isEmpty(originStr)) {
return StringUtils.EMPTY;
}
String defaultAbbrevMarker = "...";
if (originStr.length() < maxSize) {
return originStr;
}
return originStr.substring(0, maxSize) + StringUtils.defaultIfEmpty(abbrevMarker, defaultAbbrevMarker);
}
}

這裡借助了 commons-lang3 包裡的StringUtils,和guava 包的Preconditions,如果項目裡沒引入這些包,可以自己手動實作也很簡單。
寫完了怎麼驗證正确性呢?
作為一個合格的程式,肯定要寫單元測試的嘛!
public class StringUtilTest {
@Test
public void abbreviateLess() {
String input = "123456789";
String abbreviate = StringUtil.abbreviate(input, 11);
Assert.assertEquals(input, abbreviate);
}
@Test
public void abbreviateCommon() {
String input = "123456789";
String abbreviate = StringUtil.abbreviate(input, 3);
Assert.assertEquals("123...", abbreviate);
}
@Test
public void abbreviateWithMarker() {
String input = "123456789";
String abbreviate = StringUtil.abbreviate(input, 3, "***");
Assert.assertEquals("123***", abbreviate);
}
@Test(expected = IllegalArgumentException.class)
public void abbreviateWithNegativeSize() {
String input = "123456789";
String abbreviate = StringUtil.abbreviate(input, -3);
}
}
發現功能通過。
2.3 思考問題?
如果就這麼完了,是不是也沒太大價值呢?
2.3.1 如果是emoji 表情,占兩個字元,如果截取到了第一個字元,會不會有問題?
寫單測驗證一下,果然有問題。
作為優秀的程式員,我們是不是應該和産品交流一下這種情況該怎麼辦呢?
假設産品說:這種情況就把整個表情不要了。
我們對此工具函數做出修改:
/**
* 超過 maxSize 的部分用省略号代替
*
* @param originStr 原始字元串
* @param maxSize 最大長度
* @param abbrevMarker 省略符
*/
public static String abbreviate(String originStr, int maxSize, String abbrevMarker) {
Preconditions.checkArgument(maxSize > 0, "size 必須大于0");
if (StringUtils.isEmpty(originStr)) {
return StringUtils.EMPTY;
}
String defaultAbbrevMarker = "...";
if (originStr.length() < maxSize) {
return originStr;
}
// 截取前maxSize 個字元
String head = originStr.substring(0, maxSize);
// 最後一個字元是高代理項,則移除掉
char lastChar = head.charAt(head.length() - 1);
if (Character.isHighSurrogate(lastChar)) {
head = head.substring(0, head.length() - 1);
}
return head + StringUtils.defaultIfEmpty(abbrevMarker, defaultAbbrevMarker);
}
重新運作單元測試,發現得到了我們想要的效果。
關于 unicode 詳細内容參考維基百科,相關字元的用法參考下面的文章:
https://www.ibm.com/developerworks/cn/java/j-unicode/2.3.2 如何可以寫的更完善?
上面的做法看似很完美了,但是如何寫的更完善呢? what? 這還不行嗎?!
我們看下 commons-lang3 的 StringUtils 工具類的源碼:
/**
* <p>Returns either the passed in CharSequence, or if the CharSequence is
* empty or {@code null}, the value of {@code defaultStr}.</p>
*
* <pre>
* StringUtils.defaultIfEmpty(null, "NULL") = "NULL"
* StringUtils.defaultIfEmpty("", "NULL") = "NULL"
* StringUtils.defaultIfEmpty(" ", "NULL") = " "
* StringUtils.defaultIfEmpty("bat", "NULL") = "bat"
* StringUtils.defaultIfEmpty("", null) = null
* </pre>
* @param <T> the specific kind of CharSequence
* @param str the CharSequence to check, may be null
* @param defaultStr the default CharSequence to return
* if the input is empty ("") or {@code null}, may be null
* @return the passed in CharSequence, or the default
* @see StringUtils#defaultString(String, String)
*/
public static <T extends CharSequence> T defaultIfEmpty(final T str, final T defaultStr) {
return isEmpty(str) ? defaultStr : str;
}
可以發現,源碼給出了常見參數的傳回值,用起來特别容易。
是以我們做出下面的修改:
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
public class StringUtil {
/**
* 超過 maxSize 的部分用省略号代替
* <p>
* 使用範例:
* 1 不超過取所有
* StringUtil.abbreviate("123456789", 11) = "123456789"
* <p>
* 2 超過最大長度截取并補充省略号
* StringUtil.abbreviate("123456789", 3) = "123..."
* <p>
* 3 emoji表情被截斷則丢棄前面的字元(整個表情)
* StringUtil.abbreviate("123456789??", 10) = "123456789..."
*
* @param originStr 原始字元串
* @param maxSize 最大長度
*/
public static String abbreviate(String originStr, int maxSize) {
return abbreviate(originStr, maxSize, null);
}
/**
* 超過 maxSize 的部分用省略号代替
* <p>
* 使用範例:
* <p>
* StringUtil.abbreviate("123456789"", 3, "***") = "123..."
*
* @param originStr 原始字元串
* @param maxSize 最大長度
* @param abbrevMarker 省略符
*/
public static String abbreviate(String originStr, int maxSize, String abbrevMarker) {
Preconditions.checkArgument(maxSize > 0, "size 必須大于0");
if (StringUtils.isEmpty(originStr)) {
return StringUtils.EMPTY;
}
String defaultAbbrevMarker = "...";
if (originStr.length() < maxSize) {
return originStr;
}
// 截取前maxSize 個字元
String head = originStr.substring(0, maxSize);
// 最後一個字元是高代理項,則移除掉
char lastChar = head.charAt(head.length() - 1);
if (Character.isHighSurrogate(lastChar)) {
head = head.substring(0, head.length() - 1);
}
return head + StringUtils.defaultIfEmpty(abbrevMarker, defaultAbbrevMarker);
}
}
最為優秀的程式員,我們編寫工具類時,可以把工具類的常見輸入和輸出在注釋中給出,友善使用者。
三、總結
這個簡單的功能,實作很容易,寫好卻沒那麼容易。
可以加上參數校驗,加上單元測試,加上注釋,加上emoji表情問題處理等。
很多新手總是覺得很多問題很簡單,但是簡單的功能代碼能否寫的嚴謹,是一件值得思考的問題。
另外希望大家能從各方面吸收源碼的精華,而不是想當然地讀源碼,源碼的注釋,源碼的設計模式,源碼的編寫思路都是非常有價值的東西。
程式設計在細微之處見真章,希望大家在平時程式設計時能夠養成好的習慣,努力做一個有追求的優秀的程式員。