一、背景
有这样一个需求:如果一个字符串超过某个长度,则超过该长度的部分用省略号代替。
很多人会觉得这 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表情问题处理等。
很多新手总是觉得很多问题很简单,但是简单的功能代码能否写的严谨,是一件值得思考的问题。
另外希望大家能从各方面吸收源码的精华,而不是想当然地读源码,源码的注释,源码的设计模式,源码的编写思路都是非常有价值的东西。
编程在细微之处见真章,希望大家在平时编程时能够养成好的习惯,努力做一个有追求的优秀的程序员。