文章

Java IO

之前也不是没总结过Java IO的内容,主要受限于水平,写的总是有限。很多很多年前,Java 字节流 字符流 转换流写得就比较寒碜,第一次接触java io,想把自己看到的东西赶紧记下来,实在是囿于初学这水平,现在都不忍直视。(说实话Java IO这一套封装对于初学者来说是有点儿晕……)后来这一篇Java IO的实现倒是好了不少,介绍了一下Java IO里的包装流,也就是装饰器模式,但是说实话写得过于随意,纯属给自己看的。今天再好好总结一下,顺便把字节流和字符流的区别好好介绍一下。

IO概述

Java 字节流 字符流 转换流里的图倒是不错(还带个360,原来我以前也用过360啊 :D 浏览器没记错的话应该是百度浏览器……现在都已经不存在了……):

Java IO主要从两个方面去学习:

  • 字节流:读写字节,比如复制图片、读写其他服务的消息等;
  • 字符流:直接读写字符,比如读文本文件;

无论字节流还是字符流,又分为读、写两部分,不过两部分完全就是镜像结构,只介绍读就行了。

当需要读的时候,可用的实现类分为两部分:

  • 介质流:真正从某介质读取数据的类,比如从内存读、从文件读;
  • 过滤流、或者说包装流:自己不真正读取,内嵌了一个包装流,真正的读写工作交给介质流去做,自己做一些更高层的封装,比如创建缓冲区,满了再写文件;

所以学Java IO只要学会两方面内容就行:

  1. 介质流和包装流的区别;
  2. 字节流和字符流的区别;

字节流(仅介绍读)

只介绍读,所以看一下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,主要就是两个方面

  1. 看看它是怎么利用InputStream读byte的
  2. 看看它是怎么把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读取文件:

  1. 本质上调用系统调用,读的是byte,所以要依赖FileInputStream从文件里读byte;
  2. 读出的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));

这样看起来简洁又合理:

  1. 创建一个FileReader读文件;
  2. 再把它交给一个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
本文由作者按照 CC BY 4.0 进行授权