文本文件的编码,解码与乱码——Java控制台

    我写了挺多Python程序,其中涉及到了读取文本文件并处理。用过Python的人都知道这个过程会出现一些让人摸不着头脑的事情,概括的说,就是字符的编码,解码以及不期而至的乱码问题。

    在应付Python的str对象与unicode对象的过程中,我找到了Ned Batchelder的一篇文章/演讲稿“我怎么能止疼啊?当我在Python中用unicode的时候。”,里面关于Python的字符编程的事情,讲解得很详细,关于字符以及编码的事情,讲得甚至更详细。也是在用Python之后,我更关注字符,编码集的处理。

    今天我在解决某个问题的时候,需要用Java从UTF-8文本文件中读取内容,并输出到另一个文件中,为了调试,也需要输出到Java控制台。第一次实现的时候,我使用了Java中的FileReader类读取文件,并使用FileWriter写文件,输出到控制台当然只能用System.out.println()语句。算法过程很简单:

(1)读取源文件内容, 通过FileReader字符流完成;

(2)向目标文件写内容,通过FileWriter字符流完成;

(3)向控制台输出内容;

    最后的结果是,目标文件为UTF-8编码格式,一切正常;控制台得到的输出,中文字符为乱码,英文字母,标点符号一切正常。为了解决这个问题,我把MyEcplise的控制台默认编码(GBK)修改为UTF-8,具体修改位置为右键要执行的main函数,选择“Run As”-->"Run Configurations...",在弹出的窗口中,选择"common"标签页,即可发现更改项。修改完成后,再一次运行,目标文件完全正常,控制台输出也完全正常。

    但是这不算解决问题,在Windows 7的cmd窗口下,不可能更改默认编码,那么该如何让控制台输出的字符显示正常?

解码过程

    我查阅了Java API文档,官方在线文档在这里。结合之前理解的Java输入流与输出流的知识,找到了使用另一种方法,在不更改控制台编码集的情况下,得到正常输出。

    Java的FileReader字符输入流,是Java中读取文本文件的重要类,为了理解文本文件读取具体过程,查阅API doc,发现FileReader类有如下继承关系

  • java.io.Reader
    • java.io.InputStreamReader
      •   java.io.FileReader

    InputStreamReader是一个“转换流”,将字节输入流转换成字符输入流,FileReader继承了此类。说明FileReader类在读取文本文件时,首先将文件作为字节流读入,然后使用InputStreamReader的类似功能,将字节流转换为字符流。

     这里需要解释下,所有内容在计算机中的存储形式,都是二进制形式,原始的二进制文件的基本读取单位是字节(byte),而对于文本文件,字符使用一个整数来表示的,unicode中,把这个表示字符的整数称为“code point”。encode(编码)和decode(解码)的过程,可用下面的方式理解:

字符.encode()------>二进制字节

二进制字节.decode()--------->字符

    InputStreamReader转换流所实现的功能,就是一个decode的过程。    

    API doc 中,FileReader类的描述中,有这样一句:“The constructors of this class assume that the default character encoding and the default byte-buffer size are appropriate”,意即“该类构造器假定默认字符编码和默认缓冲字节大小是合适的”。说明FileReader在将字节与字符进行encode(编码)和decode(解码)的过程中,是假定了一个默认的,“合适”的字符集。

 

    而FileReader的父类InputStreamReader,是这样描述的“It reads bytes and decodes them into characters using a specified charset. The charset that it uses may be specified by name or may be given explicitly, or the platform's default charset may be accepted.”,意即“InputStreamReader读取字节,并将它们解码为字符,解码过程中使用指定的字符集(charset)……如果没有显示指定字符接,使用平台的默认字符集。”Windows平台的默认字符集是GBK(ANSI标准),Linux平台的默认字符集是“UTF-8”。而如果一个UTF-8字节编码的文本文件,使用GBK字符集解码,那当然就会产生乱码,反过来也一样。而英文字母、符号永远不会出现乱码的原因,是ASCII字符表是UTF-8的一个子集,也是GBK的一个子集,UTF-8中英文字母的字节码与GBK字符集中英文字母的字节码完全相同。

    因此,另外一种解决方式,是使用InputStreamReader转换流,将一个字节流转换为字符流,并在转换过程中,指定使用“UTF-8”字符集,对字节进行解码。核心代码如下:

InputStreamReader ipr = new InputStreamReader(new FileInputStream("HelloTest.java"),Charset.forName("UTF-8");
//使用BufferedReader进行封装,是为了调用readLine()函数,方便操作 BufferedReader br
= new BufferedReader(ipr);
//其余操作......

    指定解码字符集后,目标文件正常,控制台输出也正常。

等等,那编码过程呢?

    在指定解码字符集之后,就能得到正常输出。但是有另外一个问题,第一种解决办法,是更改了MyEcplise控制台的默认编码,才得到正常输出。同时由于Windows的cmd窗口无法更改默认编码,只能使用GBK,依然是乱码。为什么在Java代码中,更改了InputStreamReader的一个参数,在GBK编码的控制台,依然能正常输出“UTF-8”字符呢?

    Java的标准输出流System.out,将字符内容输出到控制台。查阅Java Doc文档,可以看到,out其实是System类中的一个静态成员。而out的类型为PrintStream。因此,关键是要理解PrintStream类中的方法成员println()/print()的执行原理。在Java Doc文档中,有描述“All characters printed by a PrintStream are converted into bytes using the platform's default character encoding. ”意即“被PrintStream打印(print)的所有字符,都使用平台默认的字符编码集,转换为字节。”。也就是说,System.out.print()/pinrtln()方法,使用平台默认字符集,将字符串encode(编码)为字节,然后传给了控制台,控制台接收到字节后,当然会使用平台默认的字符集,将字节decode(解码)为字符串。

    即Java会将程序中的字符,先编码,再放入标准输出流System.out,编码字符集为平台默认字符集。因此,只要在读取输入流的过程中,将字节正确地解码为字符,后续的标准输出其实是与原文件编码格式无关的。

    但是之前说过,计算机中所有数据都是二进制字节,文本文件的存储方式也是二进制字节;那么,在Java程序中,字符是以什么形式保存的?再次查阅Java Doc,发现Java中,char数据类型是用如前所述的 UNICODE 表示,UNICODE标准给每个字符分配一个唯一的整数,使用16进制编码方式表示,一共可以表示65536个字符,可以理解为无符号的16位整数。因此Java程序中,char类型是可以和Int类型相互转换的。

所以,被更改的并不是控制台的编码

    通过以上的整理,可以发现,使用第一种方法——更改控制台编码格式——解决控制台输出乱码的问题时,我们并不是因为让控制台可以输出"UTF-8"字符,所以消灭了乱码。而是通过修改控制台编码,连带将整个平台的默认字符集编码改为"UTF-8",导致FileReader在读取字符输入流时,使用了UTF-8字符集进行解码。这一效果其实和

1 InputStreamReader ipr = new InputStreamReader(new FileInputStream("HelloTest.java"),Charset.forName("UTF-8");

完全相同。

原文地址:https://www.cnblogs.com/nomorewzx/p/4670650.html