PrintWriter装饰FileWriter后,对字符串的默认编码方式

import java.io.*;

public class test2 {
    public static void main(String[] args) throws IOException {
        PrintWriter out = new PrintWriter(
                new BufferedWriter(new FileWriter("BasicFileOutput.out")));
        out.println("我是第一行");
        out.close();
        // Show the stored file:
        System.out.println(new BufferedReader(new FileReader("BasicFileOutput.out")).readLine());
    }
}

上面程序对一个文件进行写入,我们知道Reader是处理字符的,但最终存入到文件里是需要通过编码把字符变成对应若干字节的。

  • 我们知道IO体系使用了装饰器模式,而PrintWriter和BufferedWriter都是装饰类,都是为了拓展功能的。
  • 通过对out.printlnctrl+点击追踪源码,能发现装饰类最终都会调用到自身一个Writer类型的成员的write函数上。主要过程就是:PrintWriter对象去调用BufferedWriter的write,BufferedWriter对象去调用FileWriter的write。所以最终应该看FileWriter的write实现。
  • 去查看FileWriter的源码发现根本没有write函数,原来write函数在其父类OutputStreamWriter里就写好了。发现其调用了StreamEncoder类型成员变量se的write函数。
//OutputStreamWriter
	private final StreamEncoder se;

    public void write(char cbuf[], int off, int len) throws IOException {
        se.write(cbuf, off, len);
    }
  • 再去看StreamEncoder的write实现:
//StreamEncoder
    public void write(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if ((off < 0) || (off > cbuf.length) || (len < 0) ||
                ((off + len) > cbuf.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return;
            }
            implWrite(cbuf, off, len);//调用下面的函数
        }
    }

    void implWrite(char cbuf[], int off, int len)
        throws IOException
    {
        CharBuffer cb = CharBuffer.wrap(cbuf, off, len);

        if (haveLeftoverChar)
        flushLeftoverChar(cb, false);

        while (cb.hasRemaining()) {
        CoderResult cr = encoder.encode(cb, bb, false);//关键。调用了encoder成员的encode函数。这里打断点
        if (cr.isUnderflow()) {
           assert (cb.remaining() <= 1) : cb.remaining();
           if (cb.remaining() == 1) {
                haveLeftoverChar = true;
                leftoverChar = cb.get();
            }
            break;
        }
        if (cr.isOverflow()) {
            assert bb.position() > 0;
            writeBytes();
            continue;
        }
        cr.throwException();
        }
    }

这句CoderResult cr = encoder.encode(cb, bb, false)打完断点的截图如下,可以看到encoder是UTF-8。似乎这样就可以结束分析,但是我们还是没有搞清楚UTF-8到底怎么来的。所以接着分析。
在这里插入图片描述
既然encoder是StreamEncoder的成员变量,那么我们看一下它的构造器是否为encoder赋了值:

//StreamEncoder
    private StreamEncoder(OutputStream out, Object lock, CharsetEncoder enc) {//在这里打断点
        super(lock);
        this.out = out;
        this.ch = null;
        this.cs = enc.charset();
        this.encoder = enc;

        // This path disabled until direct buffers are faster
        if (false && out instanceof FileOutputStream) {
                ch = ((FileOutputStream)out).getChannel();
        if (ch != null)
                    bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE);
        }
            if (ch == null) {
        bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
        }
    }

发现构造器会为其赋值,所以再回到OutputStreamWriter,看看它的StreamEncoder类型成员变量se是怎么来的:

//FileWriter
    //本文程序用的是这个重载版本的FileWriter构造器
    public FileWriter(String fileName) throws IOException {
        super(new FileOutputStream(fileName));//调用FileWriter的父类OutputStreamWriter构造器
    }
//OutputStreamWriter
    //根据上面,会调用到这个重载版本的FileWriter构造器
    public OutputStreamWriter(OutputStream out) {
        super(out);
        try {
            se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);//关键。这里为se变量赋值
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
    }

再次追踪到StreamEncoder的forOutputStreamWriter里:

//StreamEncoder
    public static StreamEncoder forOutputStreamWriter(OutputStream out,
                                                      Object lock,
                                                      String charsetName)//根据上面,这个参数为null
        throws UnsupportedEncodingException
    {
        String csn = charsetName;
        if (csn == null)//会进入此分支
            csn = Charset.defaultCharset().name();
        try {
            if (Charset.isSupported(csn))
                return new StreamEncoder(out, lock, Charset.forName(csn));
        } catch (IllegalCharsetNameException x) { }
        throw new UnsupportedEncodingException (csn);
    }

追踪到Charset的defaultCharset方法:

//Charset
    public static Charset defaultCharset() {
        if (defaultCharset == null) {
            synchronized (Charset.class) {
                String csn = AccessController.doPrivileged(
                    new GetPropertyAction("file.encoding"));
                Charset cs = lookup(csn);
                if (cs != null)
                    defaultCharset = cs;
                else
                    defaultCharset = forName("UTF-8");
            }
        }
        return defaultCharset;
    }

终于真相大白,原来写入文件编码时用到的字符集是"file.encoding"(它一般就设置为UTF-8),如果jvm不支持该字符集,则再使用"UTF-8"

总结一下:

  1. new PrintWriter( new BufferedWriter( new FileWriter("BasicFileOutput.out")))这句代码,外面的PrintWriter和BufferedWriter都只是为了装饰,为了拓展功能,它们只是在和程序的内存打交道。
  2. 而FileWriter则真正与文件打交道,它将每个char字符按照某个字符集的标准进行encode,然后将encode得到的字节写入到文件中。
  3. 多讲一下PrintWriter和DataOutputStream,它们要写入文件,就需要直接或间接地装饰到别的FileReader。要写入文件,就必须一个字节一个字节的存。
  4. 对于PrintWriter来说,它利用了字符集,因为字符集提供的映射关系就刚好是“字符<===>若干字节”;
  5. 对于DataOutputStream来说,它利用了映射关系“Java数据类型<===>Java数据类型在内存中的存储”。
posted @ 2020-02-13 21:19  allMayMight  阅读(1074)  评论(0编辑  收藏  举报