关于Java中bytes到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,覆盖全范围,不会出现字节流中有不规则的字符。

最后贴上写该文章起因的问题:

时间: 2024-09-17 04:51:46

关于Java中bytes到String的转换的相关文章

java中基本类型之间的转换

问题描述 java中基本类型之间的转换,笔试中常见的考题及答案,亲们,有能力的就帮忙解答下呗!(额的基础不好呀!) 解决方案 解决方案二:主要是3种,分别如下: 1.字符串和基础数据类型的互相转换 2.基础数据类型和其相对应的包装类的互相转换 3.字符串和基础数据类型的包装类的互相转换  1. (1)字符串转换成为基础数据类型 String s = "200";  int i = Integer.parseInt("s"); 或者 int si2 = new Int

python3中bytes与string的互相转换

首先来设置一个原始的字符串, Python 3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> website = 'http://www.cnblogs.com

Java中Date与String相互转换的方法_java

我们在注册网站的时候,往往需要填写个人信息,如姓名,年龄,出生日期等,在页面上的出生日期的值传递到后台的时候是一个字符串,而我们存入数据库的时候确需要一个日期类型,反过来,在页面上显示的时候,需要从数据库获取出生日期,此时该类型为日期类型,然后需要将该日期类型转为字符串显示在页面上,Java的API中为我们提供了日期与字符串相互转运的类DateForamt.DateForamt是一个抽象类,所以平时使用的是它的子类SimpleDateFormat.SimpleDateFormat有4个构造函数,

java中数字与字符串的转换

这段时间看Java2,进展不是太理想,看的很慢,今天看到了Java库的Java.lang部分 了.今天看的遇到点问题:这个程序是关于数字与字符串的转换.但是我输入1 2 3,结 果是抛出异常,输出"Invide number".错误在哪里?程序如下: 数字与字符串的转换 import java.io.*; public class Parse { public static void main(String args[]) throws IOException { BufferedRe

Java中Date与String的相互转换的例子

我们在注册网站的时候,往往需要填写个人信息,如姓名,年龄,出生日期等,在页面上的出生日期的值传递到后台的时候是一个字符串,而我们存入数据库的时候确需要一个日期类型,反过来,在页面上显示的时候,需要从数据库获取出生日期,此时该类型为日期类型,然后需要将该日期类型转为字符串显示在页面上,Java的API中为我们提供了日期与字符串相互转运的类DateForamt.DateForamt是一个抽象类,所以平时使用的是它的子类SimpleDateFormat.SimpleDateFormat有4个构造函数,

JAVA中StringBuffer与String的区别解析_java

看到这个讲解的不错,所以转一下 在java中有3个类来负责字符的操作. 1.Character 是进行单个字符操作的, 2.String 对一串字符进行操作,不可变类. 3.StringBuffer 也是对一串字符进行操作,是可变类. String:    是对象不是原始类型.    为不可变对象,一旦被创建,就不能修改它的值.    对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.String 是final类,即不能被继承. StringBuffer:   

【源码】java中图片和Base64互相转换源码

package cn.com.css.misps.graph.util; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import sun.misc.BASE64Decoder; import sun.misc.BASE64Encoder; /

java中字符串与日期的转换实例_java

复制代码 代码如下: import java.sql.Timestamp;import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateIO { public static void main(String[] args) { Date date= new  DateIO().strTo

java中char和Character...的转换问题

问题描述 先上代码:publicclassTest{/*方法1*/publicstaticvoidf(Characterl,Character...args){System.out.println("Characterl,Character...args");}/*方法2*/publicstaticvoidf(Character...args){System.out.println("Character...args");}/*方法3*/publicstaticvo