Java IO
之前也不是没总结过Java IO的内容,主要受限于水平,写的总是有限。很多很多年前,Java 字节流 字符流 转换流写得就比较寒碜,第一次接触java io,想把自己看到的东西赶紧记下来,实在是囿于初学这水平,现在都不忍直视。(说实话Java IO这一套封装对于初学者来说是有点儿晕……)后来这一篇Java IO的实现倒是好了不少,介绍了一下Java IO里的包装流,也就是装饰器模式,但是说实话写得过于随意,纯属给自己看的。今天再好好总结一下,顺便把字节流和字符流的区别好好介绍一下。
IO概述
Java 字节流 字符流 转换流里的图倒是不错(还带个360,原来我以前也用过360啊 :D 浏览器没记错的话应该是百度浏览器……现在都已经不存在了……):
Java IO主要从两个方面去学习:
- 字节流:读写字节,比如复制图片、读写其他服务的消息等;
- 字符流:直接读写字符,比如读文本文件;
无论字节流还是字符流,又分为读、写两部分,不过两部分完全就是镜像结构,只介绍读就行了。
当需要读的时候,可用的实现类分为两部分:
- 介质流:真正从某介质读取数据的类,比如从内存读、从文件读;
- 过滤流、或者说包装流:自己不真正读取,内嵌了一个包装流,真正的读写工作交给介质流去做,自己做一些更高层的封装,比如创建缓冲区,满了再写文件;
所以学Java IO只要学会两方面内容就行:
- 介质流和包装流的区别;
- 字节流和字符流的区别;
字节流(仅介绍读)
只介绍读,所以看一下InputStream即可。这是一个抽象类,唯一的抽象方法是:
1
public abstract int read() throws IOException;
只有这一个抽象方法,说明只有这一个方法跟真正的读有关系,其他读方法都是通过该方法间接读取的。
虽然read返回的是个int,但是它是字节,所以只有低8bit用来存储数据,只能表示0-255。
介质流
InputStream有很多实现类,有些实现类真正实现了read这一抽象方法,比如:
ByteArrayInputStream
从内存数组中读取数据。它的read实现就是简单地返回数组中的下一个字节:
1
2
3
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
FileInputStream
从文件中读取数据,它的read实现必然涉及到操作系统层面的事情(文件是由操作系统管理的),所以在Java里它的read调用了一个native方法。但是不用太关心这个native方法,知道调用read一次就能读出来一个int(实际是byte)就行了。
过滤流
还有一类InputStream的实现类,他们本身对read的实现很“投机”:并不是自己想办法去read,而是交给别人去read。
FilterInputStream
它只有一个构造函数,接收并保存一个InputStream:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* The input stream to be filtered.
*/
protected volatile InputStream in;
/**
* Creates a <code>FilterInputStream</code>
* by assigning the argument <code>in</code>
* to the field <code>this.in</code> so as
* to remember it for later use.
*
* @param in the underlying input stream, or <code>null</code> if
* this instance is to be created without an underlying stream.
*/
protected FilterInputStream(InputStream in) {
this.in = in;
}
之后所有的活包括read,都交给这个底层的小弟去做了。它就像是一个wrapper,或者说一个静态代理,不是真正干活的人。
它底层的小弟未必就是介质流,也可以是过滤流,这样就相当于层层转包,A把活交给底层的B,结果B也有自己的小弟C,最终干活的是C。
你可能看出来了,FilterInputStream其实没卵用,毕竟它实现的所有InputStream的方法都是直接交给底层的InputStream去做,并没有加什么新功能。更多的意义在于它是一个标志,其他所有的包装流都以该包装流为父类,并覆盖某些方法,以实现不同的包装功能。
BufferedInputStream
它是非常常用的包装类,以FilterInputStream为父类。所以它在构造的时候,也要有一个InputStream作为底层干活的人。它内部还放了一个默认大小为8192的byte数组,作为读数据的缓冲区。
看一下它override父类FilterInputStream的read方法:
1
2
3
4
5
6
7
8
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
现在这个read真正实现了包装流“包装”出来的功能:“预读”一批数据到内部的byte数组,当需要read一个byte的时候,如果内部byte数组还有数据,直接返回一个byte。如果底层的介质流是FileInputStream,这效率就比每次都调用操作系统接口从文件读一个byte快多了。
DataInputStream
它倒没有override FilterInputStream的什么方法来增加新的“包装”功能,不过它还实现了DataInput接口。
DataInput接口定义了readByte/readChar/readDouble等功能,DataInputStream利用底层的InputStream将他们一一实现。比如readChar:
1
2
3
4
5
6
7
public final char readChar() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (char)((ch1 << 8) + (ch2 << 0));
}
就是读两次byte,两个byte转成一个char。
DataInputStream主要是利用底层InputStream读byte,自己加工一下提供了更高级的功能:能直接读成一个char、一个int了,不再只是读出来一个byte。
不过DataInputStream的这些方法都属于DataInput接口,不属于InputStream。所以new DataInputStream的时候如果用的是InputStream引用,使用的时候记得强制转型为DataInputStream或者DataInput。
还有其他的介质流和过滤流就不介绍了。尤其是介质流,可以互相包装,以实现更多的功能组合。其实本质上就是“层层转包”。但无论怎么包装,最底层一定要有一个介质流,承载真正的读写工作。
字符流(以读为例)
读字符流的父类是Reader,和InputStream不同的是,InputStream读的是byte,它读的是字符char。
但是仔细想一想,所有的数据都是以bit的形式存储在介质上的。所以读一个字符,本质上也得先从介质上读出字节,然后再把字节转成字符!而说到读字节,不就是上面介绍的InputStream那一套吗?
所以学习Reader,主要就是两个方面:
- 看看它是怎么利用InputStream读byte的;
- 看看它是怎么把byte转为char的。
同样看一下它的抽象方法read:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Reads characters into a portion of an array. This method will block
* until some input is available, an I/O error occurs, or the end of the
* stream is reached.
*
* @param cbuf Destination buffer
* @param off Offset at which to start storing characters
* @param len Maximum number of characters to read
*
* @return The number of characters read, or -1 if the end of the
* stream has been reached
*
* @exception IOException If an I/O error occurs
*/
abstract public int read(char cbuf[], int off, int len) throws IOException;
和InputStream不太像的是,它是把char读到char数组里。
而读一个char的read方法利用了该方法,限定数组长度为1:
1
2
3
4
5
6
7
public int read() throws IOException {
char cb[] = new char[1];
if (read(cb, 0, 1) == -1)
return -1;
else
return cb[0];
}
返回值同样为int,不过使用了低16bit。InputStream读的是char,使用的是int的低8bit。
过滤流
先说一下Reader的过滤流,和InputStream相比,Reader也有一个类似于FilterInputStream的类,FilterReader。但是这个类貌似并没有什么子类。
可能Java终于发现了这个类并没有什么卵用?就像FilterInputStream一样,充其量是个标记作用。
BufferedReader
类似于BufferedOutputStream的过滤流。它并没有把FilterReader当做自己的父类。
和BufferedOutputStream类似,它内部包含一个Reader,再搞一个默认大小为8192的char数组,读的时候如果数组里有字符,直接返回:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
for (;;) {
if (nextChar >= nChars) {
fill();
if (nextChar >= nChars)
return -1;
}
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
continue;
}
}
return cb[nextChar++];
}
}
}
BufferedReader还新加了一个readLine()方法,读文件时非常有用。
介质流
和InputStream一样,介绍一个以内存为介质的,一个以文件为介质的。
CharArrayReader
和ByteArrayInputStream类似,构造时内部放置一个char数组,读的时候直接返回数组里的char:
1
2
3
4
5
6
7
8
9
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
if (pos >= count)
return -1;
else
return buf[pos++];
}
}
FileReader extends InputStreamReader
FileReader和FileInputStream类似,后者读byte,它读char。
但是如前所述,在文件里,数据是以bit存储的,所以操作系统提供的读文件的接口就是读一个byte出来。FileReader不可能通过操作系统调用直接从文件里读出来一个char,只能先读出byte再转成char。所以Java抽象出一个InputStreamReader承载这个工作:从文件里读出char。
所以InputStreamReader(和OutputStreamWriter)又有了一个特殊的新名字:转换流。将InputStream转换为Reader(或者将Writer转换为OutputStream)。
InputStreamReader的第一个重要任务就是内置一个InputStream,用于读取byte。FileReader作为InputStreamReader的子类,创建的时候选用的InputStream是FileInputStream。
读出byte后,第二个重要任务就是转换为char。
这又涉及到一个很复杂的问题:文件编码。文件存储时候的编码不同,字节表示也就不同。比如UTF-8,存储ascii字符是一个字节,存储汉字就是三个字节。如果是以UTF-16编码存储,所有的字符都是两个字节。关于编码具体可以参考Unicode & UTF-n。
Java内部表示char就是用的UTF-16,即每个char都是两个字节表示。将文件读为char,本质上就是将文件中的字节从文件本身的编码转换成UTF-16编码。
所以InputStreamReader读char的时候,实际是交给StreamDecoder去做的:
1
2
3
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
StreamDecoder也是Reader的子类,从这个角度看,可以把InputStreamReader也认为是一个包装流,它包装了StreamDecoder这个流,真正的读char的任务是交给后者去做的。
StreamDecoder利用InputStream读取byte,之后使用CharsetDecoder将bytedecode为char。这个decode过程就是上面说的将文件编码转为UTF-16(char的编码)的过程。
StreamDecoder也可以认为是包装流,包装了一个InputStream。
CharsetDecoder又是什么?InputStreamReader在创建的时候可以指定一个CharsetDecoder或者一个Charset,或者不指定Charset使用系统默认的Charset,系统返回该Charset对应的CharsetDecoder。
Java内置了很多Charset,每个Charset都有一个CharsetEncoder和一个CharsetDecoder。
想想不同字符集之间怎么转换?直接互转吗?这样就太麻烦了,得保证任意两个字符集之间都有一个互相转化的过程。如果新加一个字符集,原有的所有字符集都要和它重新加一套转换的过程,这是不现实的。最简单的办法就是使用某个中间字符集,大家都保证能和它互转,这样所有的字符集之间都能通过这个中间字符集互相转换了。这个中间字符集就是Java内部使用的UTF-16,也就是表示char的字符集。
所以,所有的字符集的CharsetEncoder/CharsetDecoder都只需要实现一个方法:encodeLoop(CharBuffer src, ByteBuffer dst)
/decodeLoop(ByteBuffer src, CharBuffer dst)
,用于将该字符集表示的字节和char(UTF-16)互转。
比如UTF-8的CharsetDecoder在将byte数组decode为char时(看代码的注释就够了):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
private CoderResult decodeArrayLoop(ByteBuffer src,
CharBuffer dst)
{
// This method is optimized for ASCII input.
byte[] sa = src.array();
int sp = src.arrayOffset() + src.position();
int sl = src.arrayOffset() + src.limit();
char[] da = dst.array();
int dp = dst.arrayOffset() + dst.position();
int dl = dst.arrayOffset() + dst.limit();
int dlASCII = dp + Math.min(sl - sp, dl - dp);
// ASCII only loop
while (dp < dlASCII && sa[sp] >= 0)
da[dp++] = (char) sa[sp++];
while (sp < sl) {
int b1 = sa[sp];
if (b1 >= 0) {
// 1 byte, 7 bits: 0xxxxxxx
if (dp >= dl)
return xflow(src, sp, sl, dst, dp, 1);
da[dp++] = (char) b1;
sp++;
} else if ((b1 >> 5) == -2 && (b1 & 0x1e) != 0) {
// 2 bytes, 11 bits: 110xxxxx 10xxxxxx
// [C2..DF] [80..BF]
if (sl - sp < 2 || dp >= dl)
return xflow(src, sp, sl, dst, dp, 2);
int b2 = sa[sp + 1];
// Now we check the first byte of 2-byte sequence as
// if ((b1 >> 5) == -2 && (b1 & 0x1e) != 0)
// no longer need to check b1 against c1 & c0 for
// malformed as we did in previous version
// (b1 & 0x1e) == 0x0 || (b2 & 0xc0) != 0x80;
// only need to check the second byte b2.
if (isNotContinuation(b2))
return malformedForLength(src, sp, dst, dp, 1);
da[dp++] = (char) (((b1 << 6) ^ b2)
^
(((byte) 0xC0 << 6) ^
((byte) 0x80 << 0)));
sp += 2;
} else if ((b1 >> 4) == -2) {
// 3 bytes, 16 bits: 1110xxxx 10xxxxxx 10xxxxxx
int srcRemaining = sl - sp;
if (srcRemaining < 3 || dp >= dl) {
if (srcRemaining > 1 && isMalformed3_2(b1, sa[sp + 1]))
return malformedForLength(src, sp, dst, dp, 1);
return xflow(src, sp, sl, dst, dp, 3);
}
int b2 = sa[sp + 1];
int b3 = sa[sp + 2];
if (isMalformed3(b1, b2, b3))
return malformed(src, sp, dst, dp, 3);
char c = (char)
((b1 << 12) ^
(b2 << 6) ^
(b3 ^
(((byte) 0xE0 << 12) ^
((byte) 0x80 << 6) ^
((byte) 0x80 << 0))));
if (Character.isSurrogate(c))
return malformedForLength(src, sp, dst, dp, 3);
da[dp++] = c;
sp += 3;
} else if ((b1 >> 3) == -2) {
// 4 bytes, 21 bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
int srcRemaining = sl - sp;
if (srcRemaining < 4 || dl - dp < 2) {
b1 &= 0xff;
if (b1 > 0xf4 ||
srcRemaining > 1 && isMalformed4_2(b1, sa[sp + 1] & 0xff))
return malformedForLength(src, sp, dst, dp, 1);
if (srcRemaining > 2 && isMalformed4_3(sa[sp + 2]))
return malformedForLength(src, sp, dst, dp, 2);
return xflow(src, sp, sl, dst, dp, 4);
}
int b2 = sa[sp + 1];
int b3 = sa[sp + 2];
int b4 = sa[sp + 3];
int uc = ((b1 << 18) ^
(b2 << 12) ^
(b3 << 6) ^
(b4 ^
(((byte) 0xF0 << 18) ^
((byte) 0x80 << 12) ^
((byte) 0x80 << 6) ^
((byte) 0x80 << 0))));
if (isMalformed4(b2, b3, b4) ||
// shortest form check
!Character.isSupplementaryCodePoint(uc)) {
return malformed(src, sp, dst, dp, 4);
}
da[dp++] = Character.highSurrogate(uc);
da[dp++] = Character.lowSurrogate(uc);
sp += 4;
} else
return malformed(src, sp, dst, dp, 1);
}
return xflow(src, sp, sl, dst, dp, 0);
}
按照utf8字符集的定义,一个字符对应的byte可能有1-4个,比如碰到汉字,需要读三个byte才能decode出一个char。
总结一下,FileReader读取文件:
- 本质上调用系统调用,读的是byte,所以要依赖FileInputStream从文件里读byte;
- 读出的byte转成char:InputStreamReader使用StreamDecoder,StreamDecoder使用CharsetDecoder,将从文件里读出来的byte转为char;
其他
FileReader读文件的问题
byte转char要按照文件原有编码转,所以要明确指定要读的文件的字符集,否则就用系统默认字符集。但是新问题来了:FileReader并没有一个能指定Charset的构造函数!!!这是什么迷幻操作……
所以如果使用FileReader读取文件,只能使用系统默认的Charset,比如UTF-8。但是系统要读的文件可以以任何编码存储,比如UTF-16!这样就相当于使用UTF-8编码标准去读一个UTF-16编码的文件,就会存在乱码问题!
实际使用中,读文件最好直接用InputStreamReader:
1
inputStream = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/java/CopyLines.java"), StandardCharsets.UTF_8));
缺点就是这太长了!我第一次看到Java的这个写法,直接懵了……
否则能少写个InputStreamReader和一个FileInputStream:
1
inputStream = new BufferedReader(new FileReader("src/main/java/CopyLines.java", StandardCharsets.UTF_8));
这样看起来简洁又合理:
- 创建一个FileReader读文件;
- 再把它交给一个BufferedReader搞个读缓存。
多合理!看起来也不晕。不知道Java怎么想的,到现在也没给FileReader加个构造函数。
FileWriter同理。
示例
最后贴一个读文件的例子,注意三个TODO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// hello皮卡丘
package example.io.fileio;
/**
* @author puppylpg on 2020/10/28
*/
import java.io.*;
import java.nio.charset.StandardCharsets;
public class CopyLines {
public static void main(String[] args) throws IOException {
BufferedReader inputStream = null;
PrintWriter outputStream = null;
try {
// TODO: Java没有带charset的FileReader构造函数
// inputStream = new BufferedReader(new FileReader("src/main/java/example/io/fileio/CopyLines.java", StandardCharsets.UTF_8));
// TODO: 如果用US_ASCII读该文件(以utf-8保存),文件里的汉字就不能被正确读取并转为char。字母可以,两个字符集的ascii字母通用
// TODO: 如果用UTF-16读该文件,凉凉,每一个字符能读对的
inputStream = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/java/example/io/fileio/CopyLines.java"), StandardCharsets.UTF_16));
outputStream = new PrintWriter(new FileWriter("CopyLines.txt"));
String l;
while ((l = inputStream.readLine()) != null) {
outputStream.println(l);
}
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
}
Ref
- https://docs.oracle.com/javase/tutorial/essential/io/charstreams.html