Java IO流之字符流【二】

1. 概述

如果使用字节流读取中文。当GBK编码时,中文占用2个字节,当使用UTF-8时,中文占用3个字节。
因此字节流无法读取完整的字符,有可能出现乱码。

在这样的基础上,java的字符流应运而生。

2.字符流类图结构

在这里插入图片描述

2.1字符输入流【Reader】

java.io.Reader是用于读取字符流的抽象类,是表示字符输入流的所有类的超类。它定义了字符输入流的基本共性功能方法。

子类必须实现的方法只有 read(char[], int, int)close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

public abstract  void close(): 关闭该流并释放与之关联的所有资源。

public int read():读取单个字符, 底层是 read(new char[1], 0, 1)public int read(char[] cbuf): 将字符读入数组,底层是read(cbuf, 0, cbuf.length)public abstract  int read(char[] cbuf, int off, int len): 将字符读入数组的某一部分。

public int read(CharBuffer target): 试图将字符读入指定的字符缓冲区。 
public boolean ready(): 判断是否准备读取此流。 
public void reset():重置该流。 
public long skip(long n) :跳过字符。 

java.io.Reader源码注释:

package java.io;
/**
 * 子类必须实现的方法只有 `read(char[], int, int)` 和 `close()`。
 * 但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能
 * @author      Mark Reinhold
 * @since       JDK1.1
 */
public abstract class Reader implements Readable, Closeable {

    //锁对象
    protected Object lock;
    //最大可跳过的字节数
    private static final int maxSkipBufferSize = 8192;
    // 跳过的字符缓冲区,默认为null
    private char skipBuffer[] = null;
    // 空参构造的锁对象是调用者本身
    protected Reader() {
        this.lock = this;
    }
    // 有参构造
    protected Reader(Object lock) {
        if (lock == null) {
            throw new NullPointerException();
        }
        this.lock = lock;
    }
    //尝试将字符读入指定的字符缓冲区。
    public int read(java.nio.CharBuffer target) throws IOException {
        int len = target.remaining();
        char[] cbuf = new char[len];
        int n = read(cbuf, 0, len);
        if (n > 0)
            target.put(cbuf, 0, n);
        return n;
    }
    /**
     * 读取单个字符。
     * 返回读取的字符,作为0到65535之间的整数,如果到达流末尾,则返回-1
     */
    public int read() throws IOException {
        char cb[] = new char[1];
        if (read(cb, 0, 1) == -1)
            return -1;
        else
            return cb[0];
    }
    //将字符读入数组。
    public int read(char cbuf[]) throws IOException {
        return read(cbuf, 0, cbuf.length);
    }
    //将字符读入数组的某一部分。
    abstract public int read(char cbuf[], int off, int len) throws IOException;
    // 跳过读取n个字符
    public long skip(long n) throws IOException {
        if (n < 0L)
            throw new IllegalArgumentException("skip value is negative");
        // 比较n和maxSkipBufferSize的最小值,并返回(如果n>8192,则只跳过8192个字符)
        int nn = (int) Math.min(n, maxSkipBufferSize);
        synchronized (lock) {
            if ((skipBuffer == null) || (skipBuffer.length < nn))
                //初始化跳过的字符数组
                skipBuffer = new char[nn];
            long r = n;
            while (r > 0) {
            	// 尝试向缓冲字符数组中读取字符,返回实际读取的字符数
                int nc = read(skipBuffer, 0, (int)Math.min(r, nn));
                if (nc == -1)
                    break;
                r -= nc;
            }
            // 返回实际跳过的字符数量
            return n - r;
        }
    }
    // 流对象是否就绪(Reader永远返回false)
    public boolean ready() throws IOException {
        return false;
    }
    // 是否支持标记操作(默认false)
    public boolean markSupported() {
        return false;
    }
	// 标记流位置(Reader不支持该方法)
    public void mark(int readAheadLimit) throws IOException {
        throw new IOException("mark() not supported");
    }
	// 重置流对象(Reader不支持该方法)
    public void reset() throws IOException {
        throw new IOException("reset() not supported");
    }
	// 关闭流对象(子类应重写该方法)
    abstract public void close() throws IOException;

}

2.1.1 【文件字符输入流】FileReader

java.io.FileReader类是读取字符文件的便利类,使用系统默认的字符编码和字节缓冲区。

java.io.FileReader本身只有三个构造方法,没有自己定义的方法。

public FileReader(File file) : 创建一个新的 FileReader 流对象 ,指向要读取的File对象。
public FileReader(FileDescriptor fd) : 创建一个新的 FileReader 流对象 ,指向要读取的FileDescriptor对象。
public FileReader(String fileName) : 创建一个新的 FileReader 流对象,指向要读取的文件名称。

java.io.FileReader源码注释:

package java.io;

public class FileReader extends InputStreamReader {
    // 创建一个新的 FileReader 流对象,指向要读取的文件名称。
    public FileReader(String fileName) throws FileNotFoundException {
        super(new FileInputStream(fileName));
    }
	// 创建一个新的 FileReader 流对象 ,指向要读取的File对象。
    public FileReader(File file) throws FileNotFoundException {
        super(new FileInputStream(file));
    }
	// 创建一个新的 FileReader 流对象 ,指向要读取的FileDescriptor对象。
    public FileReader(FileDescriptor fd) {
        super(new FileInputStream(fd));
    }
}

java.io.FileReader的源码可以看出,所谓的字符流的底层,竟然是用字节流实现的。

而且FileReader继承自InputStreamReader,并没有扩展任何功能,就连构造方法也是调用父类的。可见FileReader对象其实是通过InputStreamReader工作的。

InputStreamReader源码注释:

package java.io;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import sun.nio.cs.StreamDecoder;

/**
  * InputStreamReader 是字节流通向字符流的桥梁:它使用指定的字符集读取字节并将其解码为字符。
  * 它使用的字符集可以由名称指定或显式给定,也可以使用平台默认的字符集。 
  * 每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节。            
  * 要启用从字节到字符的有效转换,可以提前从流中读取更多字节。 
  * 为了达到最高效率,可要考虑在 BufferedReader 内包装 InputStreamReader。例如: 
  * BufferedReader in= new BufferedReader(new InputStreamReader(System.in));
  */

public class InputStreamReader extends Reader {

	// 流解码器对象
    private final StreamDecoder sd;
    
    // 使用给定的字节输入流对象创建一个InputStreamReader对象(使用系统默认字符集)
    public InputStreamReader(InputStream in) {
        // 将传入的InputStream参数作为锁对象
        super(in);
        try {
        	// 使用系统默认字符集构建解码器对象(使用this作为锁对象)
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); 
        } catch (UnsupportedEncodingException e) {
            // 默认字符集应当总是可用的
            throw new Error(e);
        }
    }
	// 使用给定的字符集名称,创建流解码器对象
    public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException
    {
        super(in);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
    }
	// 使用给定的字符集,创建流解码器对象
    public InputStreamReader(InputStream in, Charset cs) {
        super(in);
        if (cs == null)
            throw new NullPointerException("charset");
        sd = StreamDecoder.forInputStreamReader(in, this, cs);
    }
	// 使用给定的字符集解码器对象,创建流解码器对象
    public InputStreamReader(InputStream in, CharsetDecoder dec) {
        super(in);
        if (dec == null)
            throw new NullPointerException("charset decoder");
        sd = StreamDecoder.forInputStreamReader(in, this, dec);
    }
    // 返回此流使用的字符编码的名称。
    public String getEncoding() {
        return sd.getEncoding();
    }
	//读取单个字符
    public int read() throws IOException {
        return sd.read();
    }
	// 将字符读入数组中的某一部分。
    public int read(char cbuf[], int offset, int length) throws IOException {
        return sd.read(cbuf, offset, length);
    }
	// 判断此流是否已经准备好用于读取。
    public boolean ready() throws IOException {
        return sd.ready();
    }
	// 关闭该流并释放与之关联的所有资源。
    public void close() throws IOException {
        sd.close();
    }
}

java.io.InputStreamReader的源码可以看出,其实工作的也不是它,而是sun.nio.cs.StreamDecoder这个流解码器对象。

StreamDecoder读取字符源码注释

package sun.nio.cs;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

public class StreamDecoder extends Reader {
    // 流的关闭状态
    private volatile boolean isOpen;
    // 是否有剩余字符
    private boolean haveLeftoverChar;
    // 剩余的字符
    private char leftoverChar;
        
    // 读取单个字符,并返回其整数形式
	public int read() throws IOException {
        return this.read0();
    }

    private int read0() throws IOException {
    	// 获取锁对象
        Object var1 = this.lock;
        synchronized(this.lock) {
            if (this.haveLeftoverChar) {
            	// 判断是否有剩余的字符。如果有就直接返回,不再读取
                this.haveLeftoverChar = false;
                return this.leftoverChar;
            } else {
                char[] var2 = new char[2];
                // 一次性读取2个字符,并获取实际读到的字符个数
                int var3 = this.read(var2, 0, 2);
                switch(var3) {
                case -1: //读到流末尾
                    return -1;
                case 0:
                default:
                	// 断言,返回-1
                    assert false : var3;
                    return -1;
                case 2:
                 	// 如果读取到2个字符,就把第二个字符赋值给leftoverChar,以便下次读取时直接返回
                 	// 由于case2没有返回值,所以返回的是case 1中的 var2[0];
                    this.leftoverChar = var2[1];
                    this.haveLeftoverChar = true;
                case 1:
                	// 如果只读取到1个字符,直接返回
                    return var2[0];
                }
            }
        }
    }
	// 将字符读入数组中的某一部分。 
    public int read(char[] var1, int var2, int var3) throws IOException {
    	// 偏移量
        int var4 = var2;
        // 读取的个数
        int var5 = var3;
        Object var6 = this.lock;
        synchronized(this.lock) {
        	// 判断流是否已经关闭
            this.ensureOpen();
            // 判断偏移量是否不小于0,并且不大于数组长度,并且读取的字符个数大于0....巴拉巴拉一堆
            if (var4 >= 0 && var4 <= var1.length && var5 >= 0 && var4 + var5 <= var1.length && var4 + var5 >= 0) {
                if (var5 == 0) {
                    return 0;
                } else {
                    byte var7 = 0;
                    if (this.haveLeftoverChar) {
                    	// 判断是否有剩余字符,如果有,就把剩余的字符放到数组中偏移量的位置上。
                    	// 即,偏移的起始位置时剩余字符的位置。
                        var1[var4] = this.leftoverChar;
                        ++var4;
                        --var5;
                        this.haveLeftoverChar = false;
                        var7 = 1;
                        if (var5 == 0 || !this.implReady()) { // 判断是否满足读取的个数,或者当前字符区是否还存在字符,或者该流是否已经关闭
                            return var7;
                        }
                    }
					// 只读取一个字符
                    if (var5 == 1) {
                    	// 读取一个
                        int var8 = this.read0();
                        if (var8 == -1) { 
                        	// 读取到流末尾
                            return var7 == 0 ? -1 : var7;
                        } else { 
                        	//向数组中存入读到的字符,并返回1
                            var1[var4] = (char)var8;
                            return var7 + 1;
                        }
                    } else {
                    	// 读取多个字符(大于1),并返回读取到的字符个数
                        return var7 + this.implRead(var1, var4, var4 + var5);
                    }
                }
            } else {
                throw new IndexOutOfBoundsException();
            }
        }
    }

从以上的源码可以看出,read()在API中说是读取单个字符,实际上是读取2个字符,但是只返回读取到的第一个字符。

简单的代码Demo演示

package com.hanyxx.io;

import java.io.FileReader;
import java.io.IOException;

/**
 * 字符流(我是一个粉刷匠,粉刷本领强!)
 * @author layman
 */
public class Demo04 {
    private static FileReader fr ;
    public static void main(String[] args) throws IOException {
        char[] var2  = new char[13];
        int offset = 1;
        int length = 3;
        //readSingleChar();
        //readCharSJ(var2);
        readCharSJByLength(var2,offset,length);
    }
    // 读取单个字符
    public static void readSingleChar() throws IOException {
        fr = new FileReader("layman.txt");
        int read;
        // 读取单个字符,并返回读取到的字符,以整数形式
        while((read = fr.read()) != -1){
            // 我是一个粉刷匠,粉刷本领强!
            System.out.print((char) read);
        }
        fr.close();
    }
    // 将字符读入数组
    public static void readCharSJ(char[] var2) throws IOException {
        fr = new FileReader("layman.txt");
        System.out.println("编码方式:" + fr.getEncoding());

        int length;
        // 将字符读入数组,并返回读取的字符个数
        while((length = fr.read(var2)) != -1){
            // 我是一个粉刷匠,粉刷本领强!
            System.out.print(new String(var2,0,length));
        }
        fr.close();
    }
    // 将字符读入数组中的某一部分。
    public static void readCharSJByLength(char[] var2,int offset,int length) throws IOException {

        fr = new FileReader("layman.txt");
        System.out.println("编码方式:"+fr.getEncoding());
        int len;
        // 将字符读入数组,并返回读取的字符个数
        while((len = fr.read(var2,offset,length)) != -1){
            System.out.print(new String(var2,0,len));
        }
        fr.close();
    }
}

2.2 【字符输出流】Writer

java.io.Writer是写入字符流的抽象类。是表示字符输出流的所有类的超类。它定义了字符输出流的基本共性功能方法。

子类必须实现的方法仅有write(char[], int, int)flush()close()

但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。

java.io.Writer常用方法:

public abstract  void close(): 关闭此流,但要先刷新它。 
public abstract  void flush() :刷新该流的缓冲。将缓冲区的字符立刻写入预期目标。 
public void write(char[] cbuf) :写入字符数组。 
public abstract  void write(char[] cbuf, int off, int len) : 写入字符数组的某一部分。 
public void write(int c) :写入单个字符。 

java.io.Writer源码注释

package java.io;

/**
 * 写入字符流的抽象类。
 * 子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。
 * 但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。 
 */
public abstract class Writer implements Appendable, Closeable, Flushable {

    //用于保存字符串和单个字符的临时缓冲区
    private char[] writeBuffer;

   	//缓冲区容量1024
    private static final int WRITE_BUFFER_SIZE = 1024;

    // 锁对象
    protected Object lock;

    // 空参构造
    protected Writer() {
        this.lock = this;
    }
	// 有参构造,指定锁对象
    protected Writer(Object lock) {
        if (lock == null) {
            throw new NullPointerException();
        }
        this.lock = lock;
    }

    // 写入单个字符,只写入C的16个低阶位,剩余的16个高阶位将被舍弃
    public void write(int c) throws IOException {
        synchronized (lock) {
            if (writeBuffer == null){
                writeBuffer = new char[WRITE_BUFFER_SIZE];
            }
            writeBuffer[0] = (char) c;
            write(writeBuffer, 0, 1);
        }
    }
	// 写入字符数组
    public void write(char cbuf[]) throws IOException {
        write(cbuf, 0, cbuf.length);
    }

    // 子类必须重写该方法
    abstract public void write(char cbuf[], int off, int len) throws IOException;

    // 写一个字符串
    public void write(String str) throws IOException {
        write(str, 0, str.length());
    }

    // 写一个字符串的一部分
    public void write(String str, int off, int len) throws IOException {
        synchronized (lock) {
            char cbuf[];
            // 初始化字符数组,如果写入的字符个数比1024小,那么就初始化容量为1024的字符数组
            // 如果写入的字符个数比1024大,就用写入的字符个数当作容量
            if (len <= WRITE_BUFFER_SIZE) {
                if (writeBuffer == null) {
                    writeBuffer = new char[WRITE_BUFFER_SIZE];
                }
                cbuf = writeBuffer;
            } else {    
                cbuf = new char[len];
            }
            // 将字符从字符串复制到目标字符数组中。
            str.getChars(off, (off + len), cbuf, 0);
            write(cbuf, 0, len);
        }
    }

    // 将指定字符序列添加到此 writer。底层调用的是write(String str)
    public Writer append(CharSequence csq) throws IOException {
        if (csq == null)
            write("null");
        else
            write(csq.toString());
        return this;
    }

    // 将指定字符序列的子序列添加到此 writer。底层调用的是write(String str)
    public Writer append(CharSequence csq, int start, int end) throws IOException {
        CharSequence cs = (csq == null ? "null" : csq);
        write(cs.subSequence(start, end).toString());
        return this;
    }

    //将指定字符添加到此 writer。底层调用的是write(int c)
    public Writer append(char c) throws IOException {
        write(c);
        return this;
    }

    // 刷新流,并写入缓冲区的所有内容。(子类必须重写该方法)
    abstract public void flush() throws IOException;

    // 关闭流并释放资源
    abstract public void close() throws IOException;
}

2.2.1 【文件字符输出流】FileWriter

java.io.FileWriter是用来写入字符文件的便捷类。它只有五个构造方法。

public FileWriter(File file) :该构造方法等同于FileWriter(File file, false)
public FileWriter(File file, boolean append) :根据给定的 File 对象构造一个 FileWriter 对象。 如果第二个参数为 true,则是追加写入,若为false,则是覆盖写入。

public FileWriter(FileDescriptor fd): 构造与某个文件描述符相关联的 FileWriter 对象。 

public FileWriter(String fileName) :该构造方法等同于FileWriter(String fileName, false)
public FileWriter(String fileName, boolean append) : 根据给定的文件名构造 FileWriter 对象。 如果第二个参数为 true,则是追加写入,若为false,则是覆盖写入。

java.io.FileWriter源码

package java.io;

public class FileWriter extends OutputStreamWriter {

    public FileWriter(String fileName, boolean append) throws IOException {
        super(new FileOutputStream(fileName, append));
    }

    public FileWriter(File file) throws IOException {
        super(new FileOutputStream(file));
    }

    public FileWriter(File file, boolean append) throws IOException {
        super(new FileOutputStream(file, append));
    }

    public FileWriter(FileDescriptor fd) {
        super(new FileOutputStream(fd));
    }
}

java.io.FileWriter的源码可以看出,它的底层,也是用字节流实现的。

而且FileWriter继承自OutputStreamWriter,并没有扩展任何功能,就连构造方法也是调用父类的。可见FileWriter对象其实是通过OutputStreamWriter工作的。

java.io.OutputStreamWriter源码注释

package java.io;

import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import sun.nio.cs.StreamEncoder;

/**
 * OutputStreamWriter 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。
 * 它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。 
 * 每次调用 write() 方法都会导致在给定字符(或字符集)上调用编码转换器。
 * 在写入底层输出流之前,得到的这些字节将在缓冲区中累积。可以指定此缓冲区的大小,
 * 不过,默认的缓冲区对多数用途来说已足够大。注意,传递给 write() 方法的字符没有缓冲。
 * 为了获得最高效率,可考虑将 OutputStreamWriter 包装到 BufferedWriter 中,以避免频繁调用转换器。 
 * 例如:Writer out  = new BufferedWriter(new OutputStreamWriter(System.out));
 */

public class OutputStreamWriter extends Writer {

    // 流编码器对象
    private final StreamEncoder se;
	// 使用指定的字符集名称构建OutputStreamWriter 对象
    public OutputStreamWriter(OutputStream out, String charsetName)
        throws UnsupportedEncodingException
    {
        super(out);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
    }
	// 使用系统默认字符集构建OutputStreamWriter 对象
    public OutputStreamWriter(OutputStream out) {
        super(out);
        try {
            se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
    }
	// 使用指定的字符集构建OutputStreamWriter 对象
    public OutputStreamWriter(OutputStream out, Charset cs) {
        super(out);
        if (cs == null)
            throw new NullPointerException("charset");
        se = StreamEncoder.forOutputStreamWriter(out, this, cs);
    }
	// 使用指定的字符编码器构建OutputStreamWriter 对象
    public OutputStreamWriter(OutputStream out, CharsetEncoder enc) {
        super(out);
        if (enc == null)
            throw new NullPointerException("charset encoder");
        se = StreamEncoder.forOutputStreamWriter(out, this, enc);
    }

    // 返回此流使用的字符编码的名称。
    public String getEncoding() {
        return se.getEncoding();
    }

    // 刷新缓冲区,并不刷新流本身。此方法可以被PrintStream调用。
    void flushBuffer() throws IOException {
        se.flushBuffer();
    }

    // 写单个字符
    public void write(int c) throws IOException {
        se.write(c);
    }

    // 写入字符数组的某一部分。
    public void write(char cbuf[], int off, int len) throws IOException {
        se.write(cbuf, off, len);
    }

    // 写字符串
    public void write(String str, int off, int len) throws IOException {
        se.write(str, off, len);
    }

    // 刷新流
    public void flush() throws IOException {
        se.flush();
    }
	// 关闭流并释放与之相关的系统资源
    public void close() throws IOException {
        se.close();
    }
}

通过阅读源码可以得知:

  • OutputStreamWriter(文件字符输出流)底层起作用的是StreamEncoder(流编码器)对象。

  • InputStreamReader (文件字符输入流)底层起作用的是StreamDecoder(流解码器)对象。

字符输出流使用步骤

  1. 创建FileWriter对象,构造方法中绑定要写入数据的目的地。
  2. 使用FileWriter中的write方法,将数据写入到内存缓冲区中(字符转换为字节)。
  3. 使用FileWriter中的flush方法,将内存缓冲区中的数据,刷新到文件中
  4. 释放资源(会将内存缓冲区中的数据刷新到文件中)

Q:flush方法和close方法的区别

A:flush方法刷新缓冲区,流对象仍然可以使用。close方法刷新缓冲区,然后关闭流对象,不能再被使用了。

简单Demo演示

package com.hanyxx.io;

import java.io.FileWriter;
import java.io.IOException;

/**
 * @author layman
 */
public class Demo05 {
    private static FileWriter fw;
    private static char[] chars = new char[]{'这','把','我','必','C'};
    private static String str = "葫芦小金刚";
    public static void main(String[] args) throws IOException {
        //writeSingleChar();
        //writeCharArray(chars);
        //writeCharArray02(chars,1,3);
        writeString(str);
    }
    // 写单个字符 write(int c) 底层实际上调用的是write(new char[]{(char)c}, 0, 1);
    private static void writeSingleChar() throws IOException {
        // 如果文件不存在,会创建该文件
        fw = new FileWriter("layman01.txt");
        char a = '哈';
        fw.write(a);
        fw.flush();
        fw.close();
    }
    // 写字符数组
    private static void writeCharArray(char[] chars) throws IOException {
        fw = new FileWriter("layman01.txt");
        fw.write(chars);
        fw.flush();
        fw.close();
    }
    // 写字符数组的一部分
    private static void writeCharArray02(char[] chars,int offset,int length) throws IOException {
        fw = new FileWriter("layman01.txt");
        fw.write(chars,offset,length);
        fw.flush();
        fw.close();
    }
    //写字符串
    private static void writeString(String str) throws IOException {
        fw = new FileWriter("layman01.txt");
        fw.write(str);
        fw.flush();
        fw.close();
    }
}
posted @ 2021-03-08 22:03  layman~  阅读(64)  评论(0编辑  收藏  举报