最近的项目由于要和多方欧洲ISV集成,进行数据报文的接收和发送,对于java字符编码以及处理方式略有一些心得,遂记之。
这篇引子先用一些直观的代码说话,给出一些直观的结果。
1. 写文件
如果要写一个简单的文件,就会有以下的代码:
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(fileName), charset));
bw.write("中文123");
bw.flush();
bw.close();
其中charset主要有几类:ISO-8859-1(ASCII),UTF-8(Unicode),UTF-16(Unicode),GB-18030(ANSI local),GB2312(ANSI local)等等。
PS1: 如果OutputStreamWriter换成无charset构造方法,则charset默认为JVM的编码集。
本地的JVM编码集可通过System.getProperty("file.encoding")得知。我公司的台式机是GB18030(环境是xp英文,改了默认语言),而家里的笔试本是GBK(环境是xp中文)。所以这个值是由多方配置一起决定的,比如JVM setting,OS,OS setting等。
如果你的机器是ISO-8859-1,那么写入中文应该是会乱码的。
PS2: GB18030从字符集上来看基本是与Unicode一一对应的,但是从写入文件系统的种类和用途来分,它更多是用作本地ANSI。
PS3: 这里的中文字符串直接写到了程序中,并不是一种优雅的方法,通常的I18N做法是把涉及到国际化的文字写到资源文件里。
这个写文件的过程首先构造了一个JVM编码集的字符串,然后将它以某种charset转换,最后以byte[]并写到文件系统中。
2. 读文件
java i/o库提供两种读的方式,基于byte和char。而String也就是char[],所以java i/o提供的类似readLine()这样的方法实际上做了一个从byte到char的转换。
2.1 以byte方式读文件
int length = 0;
InputStream ins = new FileInputStream(fileName);
while (ins.read() != -1) {
length++;
}
System.out.println(length);
注:此处虽然用ins.available()也可以得出同样的结果(因为文本很小,基本可以认为ins.available()就是inputstream的字节大小)。而实际上这个方法只是返回在无阻塞情况下当前所能读的长度,譬如大于8k的文件它也只会返回8192。
|
charset |
中 |
文 |
123 |
total |
|
UTF-8 |
3 |
3 |
3 |
9 |
|
UTF-16 |
2 |
2 |
6 |
10+2 |
|
GB18030 |
2 |
2 |
3 |
7 |
|
default(JVM charset) |
2 |
2 |
3 |
7 |
以byte方式读文件并不涉及解码过程,故这个结果是文件中字符串的真实存储长度。
其中UTF-8对中文字符以3byte编码,而对ASCII前128个字符优化处理,以1byte表示。UTF-16标准就是通常我们说的Unicode,对每个字符都采用2byte编码。GB18030是本地ANSI编码,中文字符占2byte,ASCII占1byte。default因机器而异。
PS: 你可能注意到这里有个10+2,+2的问题与Big Endian和Little Endian(字节序)有关,我们等到以后再说:D
2.2 以char方式读文件
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream(fileName), charset));
char[] buffer = new char[1024];
int readNum = 0;
StringBuffer sb = new StringBuffer();
while ((readNum = br.read(buffer)) != -1)
sb.append(buffer, 0, readNum);
String s = sb.toString();
System.out.println(s.length());
System.out.println(s.getBytes().length);
System.out.println(s.getBytes(charset).length);
其实读文件的本质还是以byte流读,只不过byte到char用了一个InputStreamReader这么一个Adapter,并指定了解码方式。需要注意的是InputStreamReader的这个charset指定的是从文件读取byte[]的解码方式,也就是说它只是保证以byte流的方式把文件内容正确读出来,而之后读出的byte[]转换到char[](String)的过程依旧是通过JVM charset转换的。所以这段代码的输出结果就是:s.length()输出char[]的长度为5;s.getBytes().length输出JVM charset decoded byte[]的长度为7(因为我本机是GB18030);而s.getBytes(charset).length则输出JVM_charset decoded byte[] –> charset decoded byte[]的长度。当然这些结果都是基于正确解码的基础,否则会抛sun.io.MalformedInputException或者解出乱码(例如以utf8编码,又以GB18030解码)。
