有一个文本文件text.txt,其编码为UTF-8,内容为:
1中A
这个文件在计算机中是以二进制形式存在的,其二进制内容用16进制表示为:
31 e4 b8 ad 41
这很好理解,看以下UTF-8编码对应关系:
字符 | UTF-8编码 | UTF-16编码 | Unicode代码点 |
---|---|---|---|
‘1’ | 0x31 | 0x0031 | 0x31 |
‘中’ | 0xE4B8AD | 0x4E2D | 0x4E2D |
‘A’ | 0x41 | 0x0041 | 0x41 |
如果我们用以下方式读取文件:
public static void main(String[] args) throws Exception {
File file = new File("text.txt");
FileInputStream fis = new FileInputStream(file);
byte[] barr = new byte[5];
fis.read(barr, 0 , 5);
fis.close();
for(byte b: barr) {
System.out.print(Integer.toHexString((int)b & 0xff) + " ");
// 31 e4 b8 ad 41
}
}
以上是进行的二进制读取,和编码无关。关键是看下面:
public static void main(String[] args) throws Exception {
File file = new File("text.txt");
FileReader fr = new FileReader(file);
char[] carr = new char[3];
fr.read(carr, 0, 3);
fr.close();
for (char c : carr) {
System.out.print(c + " ");
// 1 中 A
}
System.out.println();
String str = new String(carr);
System.out.println(str);
// 1中A
byte[] barr = str.getBytes("UTF-8");
for (byte b : barr) {
System.out.print(Integer.toHexString((int) b & 0xff) + " ");
// 31 e4 b8 ad 41
}
}
这段代码很有意思,里面涉及两次编码转换——
FileReader
进行的;String
类的getBytes
方法进行的。首先要明白,Java中的char
类型是怎么在内存存储的:Java中,一个char
类型,占用2个字节。
这就有个问题:'中'
的UTF-8编码为3个字节,而我们用FileReader
读取text.txt文件,只读取了3个char
,
其中'中'
只占用一个char
。这说明,'中'
在内存中不是以UTF-8存储的。
这有个重要事实就是:
The Java platform uses the UTF-16 representation in char arrays and in the String and StringBuffer classes.
也就是说:Java平台对字符实际上是以UTF-16编码的方式存储的。
所以text.txt被FileReader
读入char[] carr
后,实际上被进行了如下编码转换:
由UTF-8编码的:
31 e4 b8 ad 41
转为了UTF-16编码的(共3个char
,每个2字节):
00 31 4e 2d 00 41
使用String的构造函数String(char[] value)
,不需要进行编码转换,因为内部都是采用的UTF-16,所以char[]
和String
对象字符在内存里存储的二进制内容是一样的。
然后调用String对象的getBytes("UTF-8")
方法时,又会发生一次编码转换:
由UTF-16编码的:
00 31 4e 2d 00 41
转为了UTF-8编码的:
31 e4 b8 ad 41
java.io.Reader
类凡是类名为java.io.***Reader
的类都是继承自java.io.Reader
父类。Reader顾名思义就是读取,只有字符才是人可以直接读取的,所以这些类都是读取字符的。
同理,那些java.io.***Write
的类也一样,是写字符的。
当且仅当处理字符时需要涉及编码转换,所以这些类的对象都是包含了编码信息的。
我们知道读取文本文件的时候,经常会像下面这样,一层套一层的:
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("file.in"), "GBK"));
下面解释下每一层是干嘛的。
FileInputStream
:读取文件的二进制内容,不会进行任何转换。InputStreamReader
:把文件的二进制内容按照参数指定的编码方式进行解码转换,最后得到的是Java平台使用的UTF-16编码后的内容。BufferedReader
:这些类提供的方法都是数据只能按顺序读取一次。而此类加了一个缓冲区,并且实现了mark
和reset
方法,可以使用mark
方法标记数据流的当前位置,然后用reset
方法将该流重新定位到此点。InputStream
、FileInputStream
、InputStreamReader
和FileReader
都不支持mark
和reset
,BufferedReader
支持(利用内部的缓冲区),可以用boolean markSupported()
方法查看是否支持。
如果没有BufferedReader
层,每次使用InputStreamReader.read()
方法读取一次,都会导致从底层输入流(此处是文件中)读取字节,然后再进行编码转换后返回,效率极低。而BufferedReader
层提供的缓冲区可以实现高效读取。
另外:FileReader("file.in")
完全等效于InputStreamReader(new FileInputStream("file.in"))
,是采用默认字符编码读取文件的简便方式。