Java字节流读取中文文本编码解码详解

1.字节流

  • 讲述的代码将被分开一点点讲,毕竟看见一大堆代码肯定是有点烦的。

1.1.读取字节

  • 新建一个txt文件,保存文本——“你好世界”

  • 简单写一个字节流,用read()方法读取txt文件前三个字节。

File file=new File(……);
InputStream is=new BufferedInputStream(new FileInputStream(file));
//每次读取一个字节
int a=is.read();
int b=is.read();
int c=is.read();
  • 我们先在这里查找"你"字对应的编码。
GB2312C4E3
BIG5A741
GBKC4E3
GB18030C4E3
Unicode00004F60
UTF-8E4BDA0
UTF-16BE4F60
UTF-16LE604F
  • 再将上面代码读取到的字节输出,可以得到 a=0xE4,b=0xBD,c=0xA0。(十六进制)
    可见读取到的字节是采用utf-8编码的。
System.out.println(a);
System.out.println(b);
System.out.println(c);

txt文本在计算机上是以GBK编码的字节进行存储,一开始我以为字节流的read()方法是直接读取文件所存储的字节(即GBK编码),但结果是utf-8。可能是编译器默认使用utf-8读取的缘故 ,大意了,没想到我的txt文本居然是UTF-8编码的。



1.2.转化字符

  • 我们读取到了字节,那应该怎么转化成我们能看懂的字符?

1.2.1.构建字节数组解码

  • 第一种办法是将读取到的字节构建byte[]字节数组进行utf-8解码。
  • read()方法返回的是int型,我们需要将其转化成byte型。
    虽然int是32位的,byte是8位的,但返回的文件内容是一个字节(即8位),所以这里int型转换成byte型不用担心精度损失。
byte[] bytes=new byte[3];
bytes[0]= (byte) a;
bytes[1]= (byte) b;
bytes[2]= (byte) c;
  • 然后通过String的构建方法用utf-8对字节数组byte[]进行解码即可得到正确的字符。
System.out.println(new String(bytes,StandardCharsets.UTF_8));

或许有人会问,那为什么read()方法不返回byte类型而是返回int型?

我们知道read()读取到-1会停止读取,1111 1111在byte类型中表示-1,而在文本文件中,这表示一个字符,在图片音频中也可能出现全1的字节,所以用byte类型程序就无法判断全1字节是表示结束还是文件内容。
而int型32位的全1才表示-1,1111 1111在32位中只占8位,其余用0补充,可以判断出是文件结束还是文件内容。


1.2.2.对字符串进行编码再解码

  • 第二种方法是构建字符串使用iso-8859-1编码再用utf-8解码。
    严格来说,这是对某类中文乱码问题的复现和处理的演示,有一句很经典的代码——
new String(text.getBytes("ISO-8859-1"),"utf-8或GBK");

在构建字符串之前,我们需要将int型强制转换成char类型,这个过程是怎么样的?

在java中,String和char类型都是使用Unicode值进行存储字符的,而java里的Unicode值是两个字节的。
也就是把int的后16位赋值给char型,而read()读取到的内容是单字节,所以char存储的Unicode值由一个全零字节和一个utf-8编码字节组成。


Unicode值是什么,是一种编码吗?

Unicode是一个字符集,定义了每个字符对应的唯一值,没有规定编码方案。
就理解而言,不要把它看成一种编码,把它看成字符即可。

  • 我们将abc转换成char型输出看一下是什么字符。
System.out.println((char)a);
System.out.println((char)b);
System.out.println((char)c);
  • 得到的字符分别是ä、½、 (空格)
    对应的Unicode值分别是0x00E4,0x00BD,0x00A0
    而前面读取到文件内容分别是0xE4,0xBD,0xA0

我们要把0xE4BDA0这三个字节用utf-8解码成"你"字(Unicode值为0x4F60),但现在这三个字节转换成Unicode值都被加了一个全零字节,这不是白费功夫吗?

这也是许多中文乱码问题的原因之一,事实上这对英文字符是没有影响的,因为它们是单字节编码并且在众多编码中的表示都是一样的。
比如"h",在utf-8和GBK中都是用0x68表示,而在双字节的Unicode中用0x0068表示,读取后转换成char型能正常显示。
但中文是多字节编码的,补充全零字节会导致解码错误,所以要通过iso-8859-1进行编码,去除全零字节获得原来的字节再解码。


为什么使用iso-8859-1进行编码?
read()读取到的都是单字节,而单字节用iso-8859-1解码得到的字符和一个全零字节+单字节组成的Unicode值对应的字符相同
假设我们读取到0x68这个字节,用iso-8859-1解码得到字符"h"
而Unicode值0x0068对应的字符为"h"
所以Unicode值(看作字符)0x0068用iso-8859-1编码就能得到0x68(字节)

为什么不用其他编码?
上面说到英文字符在众多编码中的表示都是一样,但还有一些西欧字符在不同编码中的表示是不同。(参考ASCII表字符,它们的Unicode都是一个全零字节+单字节)
比如字符"ÿ",iso-8859-1编码对应字节为0xFF,Unicode值为0x00FF,GBK没有这个字符的编码,UTF-8编码对应字节为0xC3BF。

有点绕,反正记住上面标红那句话就行了。

  • 紧接着我们先构建字符串。
StringBuilder s=new StringBuilder();
s.append((char)a);
s.append((char)b);
s.append((char)c);
  • 这里只要用iso-8859-1对字符串进行编码,即可去除所添加的全零字节,再对字节进行utf-8解码就能得到"你"字,它在字符串里是以0x4F60的Unicode值进行存储。
System.out.println(new String(s.toString().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));


END


文章的主要内容已经结束了,余下的都是一些小测试和搜到的知识点



2.碎碎念


2.1.byte转换成int型

  • 将字符串编码得到byte[]字节数组。
String str="你";
byte[] bytes=str.getBytes(StandardCharsets.UTF-8);
  • 如果直接输出byte类型——
for(byte b:bytes){
	System.out.println(b);	
}
  • 会得到-28,-67,-96三个数值,而UTF-8对应的编码是0xE4BDA0,即228,189,160。

byte的取值范围是-128到127,负数表示首位为1,要查看编码字节我们需要将其转换成int型。

  • 转换成int型不能用强制转换(int),这样转换之后还是表示的负数,应该将byte类型和全1字节(高3位字节全零)相与。
for(byte b1:bytes) {
            System.out.println(b1&0xff);
        }


2.2.Unicode与iso-8859-1相互转换

  • 字节通过iso-8859-1解码成Unicode值只需要在前面添加全零字节即可。
  • Unicode值用iso-8859-1编码的话,如果Unicode值范围在0~255之内,去除Unicode值前面的全零字节;如果在0~255之外的话,编码字节为63,对应字符"?"。



2.3.输出十六进制

System.out.printf("0x%x",255);
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值