作者 | 程序猿石头
责编 | 晋兆雨
头图 | 付费下载于视觉中国

关于作者:程序猿石头(ID: tangleithu),现任阿里巴巴技术专家,清华学渣,前大疆后端 Leader。

背景

作者的同学是某大公司高级开发工程师,某日收到不少错误告警信息,于是便去开始排查。

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

后来才定位到是因为服务调用了某外部接口,发现对方对参数长度做了限制,如果输入参数超过 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 方法,一顿操作连忙上线,一切完美~

灾难现场

一切完美,作者也是这样认为的。然后幸福总是短暂的。

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

简直不敢相信,都 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 方法,其实很容易写个单元测试就能尽早发现有问题;

更多阅读推荐

  • 大神们都是如何在时间序列中进行特征提取的?看完就懂了!

  • 亿级大表分库分表实战总结(万字干货,实战复盘)

  • 赠书 | 华为数据底座的整体架构与建设策略

  • 区块链和大数据一起能否开启数据完整性的新纪元?

  • 情感 AI,再不涉猎就要晚了

你可能也会掉进这个简单的 String 的坑相关推荐

  1. 撇去 Windows 的微软,又掉进了贪污贿赂的深坑

    前一阵子,微软刚刚"舍弃"了运营多年的 Windows 众多业务,导致众人唏嘘不已.现在,一起几年前的事件又将 Word 和 Excel 等软件业务拉下了水. 微软软件业务涉及贪污 ...

  2. 【S操作】轻松优雅防止(解决)两次掉进同一坑的完美解决方案,arduino通知提醒方案...

    公众号关注 "DLGG创客DIY" 设为"星标",重磅干货,第一时间送达. 搞技术,经常掉坑里是正常的,但掉进同一个坑两次就有点不能忍了,为了防止第三次掉坑,我 ...

  3. 【S操作】轻松优雅防止(解决)两次掉进同一坑的完美解决方案

    公众号关注 "DLGG创客DIY" 设为"星标",重磅干货,第一时间送达. 搞技术,经常掉坑里是正常的,但掉进同一个坑两次就有点不能忍了,为了防止第三次掉坑,我 ...

  4. xssfsheet removerow 剩下空白行怎么处理_糟糕!开瓶时酒塞不小心掉进酒里该怎么处理?...

    开葡萄酒还真是个技术活,会遇到各种各样的情况,有断塞的,也有将酒塞整个戳进酒里的,这到底是为什么呢?酒塞掉进酒里,这酒还能喝吗? 酒塞种类 市场上的瓶塞大致分这五类, 一类是天然软木塞,一般是采用精选 ...

  5. 自学编程的人,90%以上都会掉进这些坑,避开这些误区能提高N倍学习效率

    前言 几乎每一个程序员都会走上那么一段自学的道路,尤其是在校生或进入工作岗位之后,技术的提升基本都靠自学,有的虽然是网上报班学习,但更多时候还是自己在学习,师傅引进门,修行靠个人. 有的人自学很快,几 ...

  6. 掉进悬崖的小白,捡到python基础秘籍,学习第一周——语法基础,小游戏,无脑循环

    掉进悬崖的小白,捡到python基础秘籍,学习第一周--语法基础,小游戏,无脑循环 人生苦短,我用python 语言的种类: 语言的发展: 什么是python 搭建 Python开发环境: 集成开发环 ...

  7. 在混乱的数字货币世界里,如何掌握你的思维避免掉进陷阱?

    加密货币处在一个理性的世界:计算机冷静地交换信息,程序员自动写出无尽的软件代码.感受和情绪似乎毫无用武之地,对吧? 错! 加密货币绝对具有破坏性,它不仅会动摇我们生活和娱乐等外部世界的基础,还会扰乱我 ...

  8. 【 MATLAB 】通过案例学会编写一个 matlab 函数(小猫掉进山洞问题)

    这是关于matlab学习的第一篇博文,我是不愿意承认自己不会MATLAB的,因为这东西大一的时候就学过,如果白驹过隙,都不好意思说自己研几了,科研的过程中MATLAB是必须要会的,于是得系统的看一下了 ...

  9. 人造卫星为什么会绕着地球转而不是停在太空中或者越飞越远.掉进地球的卫星为什么烧不完....

    人造卫星为什么会绕着地球转而不是停在太空中或者越飞越远.掉进地球的卫星为什么烧不完. 卫星被火箭推到太空中之后失去火箭的推动不就停在太空中或者因为惯性越飞越远了吗,为什么会绕着地球在椭圆形的轨道上飞? ...

最新文章

  1. linux基础命令学习(五)目录或文件权限
  2. 一种基于AliOS Things的uData感知设备软件框架
  3. 循序渐进之Maven(4) - 第一个SpringMVC项目
  4. .Net 转战 Android 4.4 日常笔记(1)--工具及环境搭建
  5. python最简单的游戏源代码_Python 练习: 简单角色游戏程序
  6. media recovery oracle,Oracle非归档模式MediaRecovery错误之--ORA-26040
  7. SSH反向代理转发至内网msf
  8. java 实现pdf 转图片_java实现pdf转图片pdf
  9. js设计模式 -- 单例模式
  10. 棕色和褐色的区别及联系
  11. 12年双11:从春雷到秋实,为复苏喝彩
  12. 乐理知识1--十二平均律
  13. Second season seventh episode,Ross finds out Rachel like him,what will he do???
  14. ES5 Array新方法reduce()  数组累加
  15. es module 和 commonjs 模块化实践
  16. Xen,Hypervisor,XenServer的关系
  17. Android6.0动态获取摄像头权限(举一反三)
  18. codeforces 463D Gargari and Permutations
  19. pyhton获取 中国各个省份/直辖市拥有的上市公司数目
  20. (七)python网络爬虫(理论+实战)——json数据解析

热门文章

  1. unity vs没有智能提示_Unity博主营地你不可不知的Unity C#代码小技巧
  2. php 继承多个接口,PHP接口多继承及tarits实现多继承效果的方法
  3. MATLAB静力学分析,锻造操作机静力学的Matlab仿真分析
  4. td之间的间距怎么改_论文的一级标题、二级标题格式怎么弄?
  5. 高校讲师年终奖,能有多少?
  6. 清华来了第二位菲尔兹奖得主,是丘成桐力荐的老朋友Caucher Birkar
  7. 怎样写一篇优秀论文?看完受益匪浅!
  8. 改善 Python 程序的 91 个建议
  9. 圆周率π的计算历程及各种脑洞大开的估计方法
  10. 那些女程序员们的故事