IO 操作流源码

源码:
首先字符流就相当于一次读两个字节。
InputStreamReader 进行的字节转字符时就是覆写了 read ,一次读取两个字节。
下面看看字符流的主要类
1.Reader
1.1  继承关系
public abstract class Reader implements Readable, Closeable{}
// 下面是两个接口所需要实现的类
public interface Readable {
    public int read(java.nio.CharBuffer cb) throws IOException;
}
public interface Closeable {
    public void close() throws IOException;
}
1.2  部分方法
     // 两个未实现的方法
     abstract public void close() throws IOException;
     abstract public int read(char cbuf[], int off, int len) throws IOException;
    // 一个对象锁,这个会在构造方法中进行赋值。
    protected Object lock;
    protected Reader() {
    this.lock = this;
    }
    // 这个锁会在 skip 方法中出现
    synchronized (lock) {
    // 这里是读操作,读多少个字符。还有判断可能文件尾
    }
1.3  锁的基本介绍
上面方法中用到的锁在使用时,使用不带锁的方法是不受影响的,只有两个方法同时带锁才会受影响 .
下面是写的一个例子,了解锁起到的作用。
public static void main(String args[]) throws Exception {
    final Object w = new Object();
    Thread t = new Thread() {
        public void run() {
            try {
                System.out.println(" 线程开始,先休息 0.01s ,这样可以保证主线程的锁先启动 ");
                Thread.sleep(10);
                System.out.println(w+" 这里可以表明使用 w 对象的方法的 ");
                System.out.println(" 等待主线程的同步块完成 ");
                synchronized (w) {
                    System.out.println(" 进入线程同步块 ");
                    Thread.sleep(1000);
                    System.out.println(" 线程同步结束 ");
                }
                System.out.println(" 线程结束 ");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };
    t.start();
    synchronized (w) {
        System.out.println(" 主线程的同步开始 ");
        Thread.sleep(1000);
        System.out.println(" 主线程的同步结束 ");
    }
}
 
// 输出结果:
主线程的同步开始
线程开始,先休息 0.01s ,这样可以保证主线程的锁先启动
java.lang.Object@1fb8ee3 这里可以表明使用 w 对象的方法
等待主线程的同步块完成
主线程的同步结束
进入线程同步块
线程同步结束
线程结束
 
2.Writer
2.1  继承关系
public abstract class Writer implements Appendable, Closeable, Flushable {}
public interface Appendable {
    Appendable append(CharSequence csq) throws IOException;
    Appendable append(CharSequence csq, int start, int end) throws IOException;
    Appendable append(char c) throws IOException;
}
// 其他两个接口就不需要看了。
2.2  部分方法
首先是看一看 append 方法
    // 很明显是直接写入了。
    public Writer append(CharSequence csq) throws IOException {
    if (csq == null)
        write("null");
    else
        write(csq.toString());
        return this;
    }
 
再看看几个抽象方法
    abstract public void write(char cbuf[], int off, int len) throws IOException;
 
    abstract public void flush() throws IOException;
 
    abstract public void close() throws IOException;
2.3  与锁有关的
关于写操作的内容都会加锁。
3.BufferedReader
3.1  继承关系
public class BufferedReader extends Reader {
// 这个又是装饰模式
private Reader in;
}
3.2  构造方法
public BufferedReader(Reader in) {
    this(in, defaultCharBufferSize);
}
// 默认缓存数组的大小
private static int defaultCharBufferSize = 8192;
// 构造方法
public BufferedReader(Reader in, int sz) {
        // 这个方法可参考前面的 Writer 源码,只要是将锁赋值
    super(in);
    if (sz <= 0)
        throw new IllegalArgumentException("Buffer size <= 0");
    // 装饰模式。。
    this.in = in;
    cb = new char[sz];
    nextChar = nChars = 0;
}
// 两个上面用到的参数,用于缓存数据,是字符 (char) 数组,不是字节 (byte) 数组。
private char cb[];
private int nChars, nextChar;
3.3  标记有关
在看 read 方法之前先看一眼  标记 mark 有关的方法有点帮助。为看懂 read 做铺垫
// 标记流中的当前位置,带入的参数表示标记所占的空间
public void mark(int readAheadLimit) throws IOException {
    if (readAheadLimit < 0) {
        throw new IllegalArgumentException("Read-ahead limit < 0");
    }
    synchronized (lock) {
        ensureOpen();
        this.readAheadLimit = readAheadLimit;
        markedChar = nextChar;
        markedSkipLF = skipLF;
    }
}
// 回到标记位置
public void reset() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (markedChar < 0)
        throw new IOException((markedChar == INVALIDATED)
                      ? "Mark invalid"
                      : "Stream not marked");
            // 下面两个参数在读方法中会有详细解释
        nextChar = markedChar;
        skipLF = markedSkipLF;
    }
}


3.4 read
这个方法中 fill() 是重点 , 有点绕,但看懂后就觉得很清晰,能完全理解 bufferedReader 的原理。
看完这个方法再回去看 3.3 的标记部分,就很容易看懂。
public int read() throws IOException {
        // 锁,看来读得时候也只能一个方法读。
    synchronized (lock) {
        // 确保输入流不是空。
        ensureOpen();
        // 这个循环和 while 一样。
        for (;;) {
                // 下面的判断为是否下一个读取的字符超出了缓存数组中实际包含数据的大小。
        if (nextChar >= nChars) {
                    // 下一个字符超出或者等于缓存数组的大小
                    // 这个是核心的方法,里面有标记的内容,详细的看下面内容。
            fill();
                    // 如果还是超出,则表示输入流读完了。
            if (nextChar >= nChars)
            return -1;
        }
                // 如果下一个字符是换行符 . 这个变量只有在 readLine 里面才变为 true 。和 \n\r 有关,可忽略。针对不同的平台的
        if (skipLF) {
            skipLF = false;
            if (cb[nextChar] == '\n') {
            nextChar++;
            continue;
            }
        }
                // 返回当前读的字符,并将要读字符 +1
        return cb[nextChar++];
        }
    }
}
 
// 下面的变量是用于 fill 方法里的
// 下面两个变量是标记的状态,  -1 为未启动标记, -2 为标记失效。
private static final int INVALIDATED = -2;
private static final int UNMARKED = -1;
// 标记的位置
private int markedChar = UNMARKED;
//nChars 表示现在缓存数组中已经存在多少个字符。
//nextChar 表示下一个读取的位置,从 0 开始,这个只是缓存数组中的位置,并不是读取流的位置。
private int nChars, nextChar;
// 标记分配的空间大小。超出后,如果缓存数组重新处置,则标记失效。
private int readAheadLimit = 0;
 
// 将字符数组读满,然后直接返回数组中的某个值。里面主要考虑的是标记的问题。
// 这个和 BufferedInputStream 差不多,一个是 byte[], 这个是 char[]
private void fill() throws IOException {
        // 计算这次缓存数据的起始位置,起始位置之前保存的是标记的内容。
    int dst;
    if (markedChar <= UNMARKED) {
        // 这里表示没有使用标记,或者标记失效。
        dst = 0;
    } else {
        // 表示使用标记
        // 这个变量表示标记之后实际使用了多少空间
        int delta = nextChar - markedChar;
        if (delta >= readAheadLimit) {
            // 如果超过了标记初始的空间。
                // 标记失效
                markedChar = INVALIDATED;
                // 标记空间赋 0
        readAheadLimit = 0;
        // 缓存数据起点 0
        dst = 0;
        } else {
            // 如果未超过标记初始的空间。
            if (readAheadLimit <= cb.length) {
                    // 分配的标记空间小于缓存数组的长度
            // 将标记后实际使用长度复制到数组的开始。
            System.arraycopy(cb, markedChar, cb, 0, delta);
            // 将标记的位置赋 0 ,标记所占空间仍然是原来的空间,不会缩小。
            markedChar = 0;
            // 数据缓存的起点
            dst = delta;
        } else {
            // 长度不够,新建一个。
            char ncb[] = new char[readAheadLimit];
            // 和上面一样,复制标记到最前面
            System.arraycopy(cb, markedChar, ncb, 0, delta);
            // 将引用更新
            cb = ncb;
            markedChar = 0;
            dst = delta;
        }
                nextChar = nChars = delta;
        }
    }
    // 下面是读数据,读出一定长度,默认 cb 的长度 8192 cb BufferedReader 中是唯一的缓存数组。
    int n;
    // 这个地方读的方法中不可能返回 0. 所以只会执行一次
    do {
        // 从标签之后读,读满 cb 字符数组,注意,这里是调用 in 的读方法。
        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
        // 读到数据的情况,没有读到的话就不做任何操作。
    if (n > 0) {
            // 现在缓存空间中已有的真实缓存数量
        nChars = dst + n;
            // 下一个读取的位置。
        nextChar = dst;
    }
}
 
其它的 read 方法和这个类似。
一次读出很多字符的时候,处理的策略是:
a. 缓存数组不够 , 就用 in 直接读,不经过缓存 .
b. 缓存数组够 , 就将缓存中读出。
c. 缓存数组够 , 但读完后还没读满,则继续从 in 中接着读,不够的部分不过缓存数组。


3.5 readLine
这个是用的比较多的方法,所以列出来。这个方法在有上面的基础上,还是很好懂的。
String readLine(boolean ignoreLF) throws IOException {
        // 传入的布尔值默认为 false
    StringBuffer s = null;
    int startChar;
 
        synchronized (lock) {
            ensureOpen();
        boolean omitLF = ignoreLF || skipLF;
        // 这个是什么? goto
    bufferLoop:
            //while
        for (;;) {
                // 下一个字符超出缓存数组大小,这里 nextChar 是从 0 开始的,所以相等的时候就代表已经超出了缓存数组范围。
        if (nextChar >= nChars)
            fill();
        // 下面的 if 是判断流的末尾,读完了就返回 null ,或者将之前读的内容返回
        if (nextChar >= nChars) {
            if (s != null && s.length() > 0)
            return s.toString();
            else
            return null;
        }
        // 表示没有到末尾 .
        boolean eol = false;
        char c = 0;
        int i;
                // 这个是处理 \r\n 的情况,不进行两次判断,忽略
        if (omitLF && (cb[nextChar] == '\n'))
                    nextChar++;
        skipLF = false;
        omitLF = false;
 
        charLoop:
        // 遍历缓存数组,直到 \n 或者 \r
        for (i = nextChar; i < nChars; i++) {
            c = cb[i];
            if ((c == '\n') || (c == '\r')) {
            // 表示读取到了换行
            eol = true;
            break charLoop;
            }
        }
                // 记录读取开始的地方,
        startChar = nextChar;
        // 要读的下一个字符。
        nextChar = i;
        // 读取到换行,而不是读完缓存。
        if (eol) {
            String str;
            if (s == null) {
            str = new String(cb, startChar, i - startChar);
            } else {
            s.append(cb, startChar, i - startChar);
            str = s.toString();
            }
            nextChar++;
            if (c == '\r') {
            skipLF = true;
            }
            return str;
        }
        // 表示读完了缓存数组,还需要继续读。
        if (s == null)
            s = new StringBuffer(defaultExpectedLineLength);
        s.append(cb, startChar, i - startChar);
        }
        }
    }
3.6  其它方法
skip  就是先把缓存数组中跳过去,如果缓存数组不够,就再将数据读入缓存数组,再跳,一直循环。
ready  表示缓存是否读完了,没什么用处。
markSupported  是否支持标记
close  流等需要关闭的东西都关闭。


4.BufferedWriter
4.1  继承关系
public class BufferedWriter extends Writer {
    // 装饰模式
    private Writer out;
}
4.2  构造函数
将缓存数组初始化,并且根据平台初始化换行符号。
    public BufferedWriter(Writer out, int sz) {
    super(out);
    if (sz <= 0)
        throw new IllegalArgumentException("Buffer size <= 0");
    this.out = out;
    cb = new char[sz];
    nChars = sz;
    nextChar = 0;
        // 获取换行的符号 \n \r, 和方法 System.getProperty("line.separator") 一样
    lineSeparator = (String) java.security.AccessController.doPrivileged(
               new sun.security.action.GetPropertyAction("line.separator"));
    }
4.3 write 有关
public void write(int c) throws IOException {
    synchronized (lock) {
            // 判断是否有输出流
        ensureOpen();
            // 如果缓存数组写满了,就 flush 数组。
        if (nextChar >= nChars)
        flushBuffer();
            // 将内容写入缓存数组中
        cb[nextChar++] = (char) c;
    }
}
//flush ,这个方法就知道 flush 的重要性,
void flushBuffer() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (nextChar == 0)
        return;
            // 将数据写入流
        out.write(cb, 0, nextChar);
        nextChar = 0;
    }
}
 
用的比较多的写字符串。就是将字符串转变成字符数组。
public void write(String s, int off, int len) throws IOException {
    synchronized (lock) {
        ensureOpen();
            
        int b = off, t = off + len;
        while (b < t) {
        int d = min(nChars - nextChar, t - b);
                // 将字符串转为字符数组
        s.getChars(b, b + d, cb, nextChar);
                // 写入缓存数组
        b += d;
        nextChar += d;
        if (nextChar >= nChars)
                    // 如果写满了,就写入流中。
            flushBuffer();
        }
    }
}
4.4  其它
a.writeLine  写一个换行
public void newLine() throws IOException {
        // 同样写到缓存数组里
    write(lineSeparator);
}
b.flush, 这个也不多说了。
public void flush() throws IOException {
    synchronized (lock) {
        flushBuffer();
        out.flush();
    }
}
c.close  关闭所有该关闭的.
public void close() throws IOException {
    synchronized (lock) {
        if (out == null) {
        return;
        }
        try {
                // 最后还释放了一次。不过没有执行 flush 方法,所以在 close 前还是要执行一次 flush
            flushBuffer();
        } finally {
            out.close();
            out = null;
            cb = null;
        }
    }
}
5. 结束
看完这个字符流,清晰了好多,开始看的比较慢,但是后来越来越快,水到渠成。

你可能感兴趣的:(java)