开发者社区> 木洛> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

关于Java中bytes到String的转换

简介: 本片文章主要介绍在Java中byte array到String转换的细节,特别是在处理非utf-8字节流转换到String时容易踩到的一些坑。
+关注继续查看
    为什么想要写这个,是因为在上周,表格存储的一个客户,告知我们在将数据通过DataX从OTS导出到ODPS后,发现数据『丢失』了。而在调查过后,发现数据并不是所谓的『丢失』了,而是数据被『改变』了。
    什么原因导致数据发生了『改变』呢?却是因为一个大部分Java程序员都会忽略的问题导致的,所以我觉得有必要单独拿出来讲讲。

首先看下如下代码:
byte[] original1 = new byte[]{(byte)0xef, (byte)0x8f, (byte)0x8f};
byte[] transformed1 = new String(original1).getBytes();
System.out.println(Arrays.toString(original1));
System.out.println(Arrays.toString(transformed1));
System.out.println(Arrays.equals(original1, transformed1));
它的执行结果是:
[-17, -113, -113]
[-17, -113, -113]
true
这两个字节数组内容是完全相等的,第一个byte array在经过到String的转换,再到bytes的转换后,内容保持不变。
再看如下代码:
byte[] original2 = new byte[]{(byte)0xef, (byte)0x8f, (byte)0xff};
byte[] transformed2 = new String(original2).getBytes();
System.out.println(Arrays.toString(original2));
System.out.println(Arrays.toString(transformed2));
System.out.println(Arrays.equals(original2, transformed2));
它的执行结果是:
[-17, -113, -1]
[-17, -65, -67, -17, -65, -67]
false
这一次,两个byte array的结果不一样了,且结果差异很大。
这两段代码的唯一区别是,original1的最后一个字节值是0x8f, 而original2的最后一个字节值为0xff。
为何就这么一个微小的变更,就会导致结果差异这么大?

在Java中,byte[]是字节数组,而String是unicode的字符集合。字节到字符的转换规则,由编码决定(关于字节、字符和编码的概念解释,可以参考这篇文章。)。
在Java中,字节到String的转换,大部分人会选用new String(byte[] bytes)这个函数。但是这不是一个推荐的选择,因为在该函数中,会选择系统默认的字符集作为转换的编码。从而会导致同一段程序,在不同的执行环境下,结果可能是不同的。为了保证程序运行的确定性,尽量避免干扰因素,我们通常做法是在程序中显式指定一个编码,所以建议是使用new String(byte[] bytes, String charset)。

上面的示例代码的执行环境中,系统默认的字符集是UTF-8,所以字节到字符的转换,会按UTF-8编码来进行转换。
从表面看,original1和original2只是最后一个字节的值的差别,但是这带来的一个非常大的不同是,original1是一个标准的utf-8编码的字节,而original2却不是一个标准的utf-8编码的字节。
在Java String的实现中,bytes到String的转换,拆解步骤为:
byte[] original2 = new byte[]{(byte)0xef, (byte)0x8f, (byte)0xff};
// 根据指定的编码查找Charset
Charset charset = Charset.forName("utf-8");
// 初始化对应charset的decoder
CharsetDecoder decoder = charset.newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE);
// 使用decoder对字节进行编码转换
decoder.decode(ByteBuffer.wrap(original2));
为何遇到一个非标准的utf-8编码的字节流,会转换到一个完全不对应的字符?这与Java中CharsetDecoder的实现有关,CharsetDecoder在遇到非标准编码的字节时,会有三种对应的策略可选择:
IGNORE(忽略),REPLACE(使用一个默认的字符去替换)和REPORT(抛出异常告知编码错误)。在Java的String实现中,选择的策略是REPLACE,而在CharsetDecoder中,默认初始化选择REPLACE的字符是"\uFFFD"。
我们看一下"\uFFFD"这个字符对应的UTF-8编码字节是什么:
String replaceChar = "\uFFFD";
System.out.println(Arrays.toString(replaceChar.getBytes("utf-8")));

输出结果:
[-17, -65, -67]
知道了String类的实现细节后,我们就可以拆解下上面例子中的original2,为何经过解码再编码的过程,会输出这么一个结果。
  1. CharsetDecoder读取第一个字节为0xef(1110 1111),根据utf-8编码,这是一个三字节的字符,所以接下来的两个字节必须符合格式为(10xx xxxx, 10xx xxxx)。
  2. 读取第二个字节为0x8f(1000 1111),符合规则,继续读取下一个。
  3. 读取第三个字节为0xff(1111 1111),不符合规则。
  4. Decoder至此解析完毕前两个字节,在第三个字节的时候发现不能与前两个字节完整的解码出一个字符,故认为前两个字节构成一个不符合规则的字符。由于选择的不规则字符处理策略是REPLACE,所以将该字符替换为\uFFFD。
  5. 从第三个字节开始,重新开始解码。但是第三个字节不符合utf-8编码首字节的规则,故认为该字节为一个不规则的字符,替换为\uFFFD。
  6. 所有字节处理完毕,结束解码过程。
  7. 得到最终结果为\uFFFD\uFFFD
而\uFFFD\uFFFD再经过编码,得到的结果就是[-17, -65, -67, -17, -65, -67],这就是整个过程。
这个过程在使用者看来,像是Java偷偷的将数据给『改变』了。所以比较规范的做法应该,当遇到不符合编码规范的字节流就报错,而不是偷偷的做事。

byte[] original2 = new byte[]{(byte)0xef, (byte)0x8f, (byte)0xff};
Charset charset = Charset.forName("utf-8");
CharsetDecoder decoder = charset.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
decoder.decode(ByteBuffer.wrap(original2));

输出结果:
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 2
	at java.nio.charset.CoderResult.throwException(CoderResult.java:260)
	at java.nio.charset.CharsetDecoder.decode(CharsetDecoder.java:781)
......

那如果我们就想把一段bytes解码为String,再从String编码为bytes,要保证bytes能正确的转换回来,应该怎么做呢?
byte[] original2 = new byte[]{(byte)0xef, (byte)0x8f, (byte)0xff};
byte[] transformed2 = new String(original2, "iso-8859-1").getBytes("iso-8859-1");
System.out.println(Arrays.toString(original2));
System.out.println(Arrays.toString(transformed2));
System.out.println(Arrays.equals(original2, transformed2));
可以选择"iso-8859-1"编码,该编码是单字节编码,字节的范围是0x00-0xff,覆盖全范围,不会出现字节流中有不规则的字符。

最后贴上写该文章起因的问题:https://yq.aliyun.com/ask/44190

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
CheerpJ转换JAVA使用最简教程
CheerpJ转换JAVA使用最简教程
158 0
Java实现定时任务的三种方法(转)
  在应用里经常都有用到在后台跑定时任务的需求。举个例子,比如需要在服务后台跑一个定时任务来进行非实时计算,清除临时数据、文件等。在本文里,我会给大家介绍3种不同的实现方法: 普通thread实现 TimerTask实现 ScheduledExecutorService实现 普通thread 这是最常见的,创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果。
1099 0
java线程间通信[实现不同线程之间的消息传递(通信),生产者和消费者模型]
线程通信,线程之间的消息传递; 多个线程在操作同一个资源,但对共享资源的操作动作不同;它们共享同一个资源,互为条件,相互依赖,相互通信让任务向前推进。 线程的同步,可以解决并发更新同一个资源,实现线程同步;但不能用来实现线程间的消息传递。 线程通信生产者和消费者和仓库是个典型模型: 生产者:没有生产之前通知消费者等待,生产产品结束之后,马上通知消费者消费 消费者:没有消费之前通知
2396 0
JAVA实现AES加密
1. 因子        上次介绍了《JAVA实现DES加密》,中间提到近些年DES使用越来越少,原因就在于其使用56位密钥,比较容易被破解,近些年来逐渐被AES替代,AES已经变成目前对称加密中最流行算法之一;AES可以使用128、192、和256位密钥,并且用128位分组加密和解密数据。
887 0
Java实现中文算数验证码(算数运算+-*/)
为了防止,页面向数据库暴力注册入力,用户登录暴力破解,所以加入验证码,验证码无法被软件获取上边的内容(加入算数计算,更加安全),所以在现在技术,暂时安全。 先看效果图: 第一次加载比较慢,防止无法加载验证码显示,后台获取准备好的默认正在加载图片(静态图片),后台图片获取好后,替代。 验证码效果图:                后台Java图片实现类Verificatio
1614 0
java中treemap和treeset实现(红黑树)
TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。 TreeSet 和 TreeMap 的关系 为了让大家了解 TreeMap 和 TreeSet 之间的关系,下面先看 TreeSet 类的部分源代码: public class TreeSet<E> extends AbstractSe
1517 0
Java开发中的Memcache原理及实现
七、            Memcached 客户端程序 Memcached的java客户端已经存在三种了: ?  官方提供的基于传统阻塞io由Greg Whalin维护的客户端 ?  Dustin Sallings实现的基于java nio的Spymemcached ?  XMemcached 1. 三种API比较 1)      memcached client for j
1086 0
+关注
木洛
阿里云高级技术专家,表格存储(TableStore)研发,专注NoSQL领域技术和解决方案。
文章
问答
文章排行榜
最热
最新
相关电子书
更多
Java基础入门(四)-泛型、反射、注解
立即下载
JAVA反射原理以及一些常见的应用
立即下载
Java开发手册1.2.0
立即下载