你可能也会掉进这个简单的 String 的坑

点击上方蓝色字体,关注我 ——
java

一个在阿里云打工的清华学渣!程序员


图 by:石头@阿里巴巴飞天园区web

关于做者:程序猿石头(ID: tangleithu),现任阿里巴巴技术专家,清华学渣,前大疆后端 Leader。欢迎关注,交流和指导!回复  “0” 送阿里技术大礼包。

背景

石头同窗是某大公司高级开发工程师,某日收到很多错误告警信息,因而便去开始排查。面试

跟踪日志发现是某个服务抛出的异常信息,奇怪的是这个服务上线也有一段时间了。以前不多看到相似的错误信息,最近偶尔多了起来。算法

后来才定位到是由于服务调用了某外部接口,发现对方对参数长度作了限制,若是输入参数超过 1000 bytes,就直接抛异常,代码相似以下:后端

/**
 * @param status
 * @param result, the size should less than 1000 bytes
 * @throws Exception
 */

public XXResult(boolean status, String result) {
    if (result != null && result.getBytes().length > 1000) {
        throw new RuntimeException("result size more than 1000 bytes!");
    }
  ......
}

心想,这还不简单,我们的 result 也不是什么关键性的东西,你有限制,我直接 trim 一下不就好了?数组

解决方案

因而三下五除二,给搞了个 trim 方法,支持传不一样参数按需 trim,代码以下:微信

/**
 * 将给定的字符串 trim 到指定大小
 * @param input
 * @param trimTo 须要 trim 的字节长度
 * @return trim 后的 String
 */

public static String trimAsByte(String input, int trimTo) {
    if (Objects.isNull(input)) {
        return null;
    }
    byte[] bytes = input.getBytes();
    if (bytes.length > trimTo) {
        byte [] subArray = Arrays.copyOfRange(bytes, 0, trimTo);
        return new String(subArray);
    }
    return input;
}

再在须要调用外部服务的地方,先调用这个 trimAsByte 方法,一顿操做连忙上线,一切完美~app

灾难现场

一切完美,石头哥也是这样认为的。而后幸福老是短暂的。less

通过一段时间后(前面也提到,业务场景确实是偶发的),相同的错误仍然发生了。

简直不敢相信,都 trim 了为啥还会超出?你也帮忙想一想,是哪里的问题?


看看上面的例子(为了方便展现,简单修改文首代码了下),

trimAsByte("WeChat:tangleithu"8)

输入字符串 WeChat:tangleithu 太长了,只 trim 到剩下 8 个字节,对应的字节数组是从 [87,101,67,104,97,116,58,116,97,110,103,108,101,105,116,104,117] 变为了 [87,101,67,104,97,116,58,116],字符串变成了 WeChat:t ,结果正确。

其实在写这个方法的时候仍是太草率了,本应该很容易想到中文的状况的,咱们来试试:

trimAsByte("程序猿石头"8)

看上述截图,悲剧了,输入程序猿石头,3 个字节一个汉字,一共 15 个字节 [-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76],trim 到 8 位,剩下前 8 位 [-25,-88,-117,-27,-70,-113,-25,-116] 也正确。再 new String变成3 个 “中文” 了,虽然第 3 个“中文”,咱也不认识,咱也不敢问到底读啥,总之再转换成字节数组,长度多了 1 个,变成 9 了。

问题算是定位到了。

不由要问,为何?

来看看这个 String 的构造函数,看看上面注释才发现,其实咱们忽略了一个很重要的概念,就是编码方式。

/**
 * Constructs a new {@code String} by decoding the specified array of bytes
 * using the platform's default charset.  The length of the new {@code
 * String} is a function of the charset, and hence may not be equal to the
 * length of the byte array.
 *
 * <p> The behavior of this constructor when the given bytes are not valid
 * in the default charset is unspecified.  The {@link
 * java.nio.charset.CharsetDecoder} class should be used when more control
 * over the decoding process is required.
 *
 * @param  bytes
 *         The bytes to be decoded into characters
 *
 * @since  JDK1.1
 */

public String(byte bytes[]) {
    //this(bytes, 0, bytes.length);
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}

当咱们用默认的构造函数 new String 的时候,只是用了系统默认的编码(本文是“UTF-8”)去尝试解码,构造出字符串。

因此,当咱们在用字节数组(字节流)来表达具体的语义的时候,必定要约定好以什么方式进行编码,本文不具体阐述编码问题了。下面用一个例子来解释上文的现象:

[-25,-88,-117,-27,-70,-113,-25,-116,-65,-25,-97,-77,-27,-92,-76] 仍然用这串字节数组来实验,这串字节数组,若是用 “UTF-8” 编码去解释,那么其想表达的语义就是中文“程序猿石头”,从上文标注的 1,2,3 中能够看出来,没有写即用了系统中的默认编码“UTF-8”。

假设按照 “GBK” 来解释(标注 4),就是表达的 “绋嬪簭鐚跨煶澶�”,注意看下其中的  是否是似曾相识;

注意标注 5,经过 GBK 解释构造字符串后,再经过默认的 “UTF-8” 获取字节数组,长度就变成 24 了,而后还经过 “GBK” 编码获得的字节数组长度为 15(标注 6),再试图构造字符串(标注 7),其中“程序猿石头”的“头”字,已经没了。说明这个转换过程当中,其实信息已经被丢了。

上面的  实际上是 UNICODE 编码方式中的一个特殊的字符,也就是 0xFFFD(65535),实际上是一个占位符(REPLACEMENT CHARACTER),用来表达未知的、没办法表达的东东。上文中在进行编码转换过程当中,出现了这个玩意,其实也就是没办法准确表达含义,会被替换成这个东西,所以信息也就丢失了。你能够试试前面的例子,好比把前 8 个字节中的最后一两个字节随便改改,都是同样的。

程序猿石头:65533 示例

总结

总结一下,其实原本是一个很简单的问题,却通过几回修改才最终解决,说明对 “基础” 掌握得仍是不够,一个重要的点是,在处理二进制数据的时候,必定要联想到 “编码” 方式。

另外,提醒咱们,看似简单的问题,咱们每每容易忽略。好比若是单纯看到文中提到的这个trim 方法,其实很容易写个单元测试就能尽早发现有问题;

越是基础的方法,咱们越应该考虑其代码的健壮性,在以前的 从一道面试题谈谈一线大厂码农应该具有的基本能力 中,我也谈到了写单元测试、测试用例的重要性。

后记

以为本号分享的文章有价值,记得添加星标哦。周更很累,不要白 piao,须要来点正反馈,安排个 “一键三连”(点赞、在看、分享)如何?😝 这将是我持续输出优质文章的最强动力。


推 荐 阅 读



程序猿石头 


程序猿石头(ID: tangleithu),现任阿里巴巴技术专家,清华学渣,前大疆后端 Leader。用不一样的视角分享高质量技术文章,以每篇文章都让人有收获为目的,欢迎关注,交流和指导!扫码回复关键字 “1024” 获取程序员大厂面试指南


本文分享自微信公众号 - 程序猿石头(tangleithu)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索