在前几篇文章中一直讲的都是InputStream,这是操作字节流的类,然而我们在程序中往往要从文件等stream中读取字符信息,如果只用InputStream能否读取字符信息呢?当然可以。但是这涉及到了一个编码和解码的问题,传输双方必须才用同一种编码方式才能正确接收,这就导致每次在传输时,传输方需要做这么几件事:
1)将需要传输的字符编码成指定字节
2)传输字节
接收方需要做这么几件事:
1)接收字节
2)将字节解码成对应的字符
我们看一下下面的例子:
我在对应目录有一个文件,这个文件是按照utf-8编码的,现在利用InputStream读取到一个byte数组中,如果我们想要读取到文件的内容,还需要继续转码成utf-8格式的字符串。
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; /** * Created by zhaohui on 16-10-14. */ public class Code { public static void main(String[] args) { try { FileInputStream inputStream = new FileInputStream("/home/zhaohui/tmp/zhaohui"); byte[] buf = new byte[100]; int length = inputStream.read(buf); System.out.println("the length of bytes is " + length); // 将字节数组中指定位置的字节转码成对应的字符串 String content = new String(buf, 0, length, Charset.forName("utf-8")); System.out.println("the content is " + content); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
输出:
the length of bytes is 16
the content is 你好吗?
从上面的例子中,我们看到只有InputStream就能解决传输字符串的问题了,但是每次都要先读成byte字节,再进行转码,麻烦,能不能直接传字符呢?????
答案是:不能!!!
计算机只认识0和1,也就是byte,只能传输byte。
但是别人的博客都说Reader和Writer神马的能传啊?这是理解角度的不同,我就认为不能传字符,爱咋咋地!
好的,我现在就正式介绍这个“能”传字符的Reader(Writer类似,我就不说了)。
先用一个例子说明,如果我们直接用Reader读取文件,会是怎样的?
import java.io.*; import java.nio.charset.Charset; /** * Created by zhaohui on 16-10-14. */ public class Code { public static void main(String[] args) { try { InputStream in = new FileInputStream("/home/zhaohui/tmp/zhaohui"); InputStreamReader reader = new InputStreamReader(in, Charset.forName("utf-16")); char [] buf = new char[100]; int length = reader.read(buf); System.out.println("the length is "+length); for (int i =0;i<length;i++){ System.out.println("char ["+i+"] is "+buf[i]); } System.out.println("the content is " + String.valueOf(buf, 0 ,length)); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
输出:the length is 5
char [0] is 你
char [1] is 好
char [2] is 吗
char [3] is ?
char [4] is
the content is 你好吗?
这样一来,是不是清爽了,也就是在你读取文件的时候,使用Reader可以直接指定解码方式,这样就可以直接读字符内容了。关于编码的问题,比较复杂,有兴趣的请参考网上其他内容,比如java中的char,是两个字节,但是如果你的文件是utf-8,读取字符时可能就会出现问题,因为utf-8的字符是变长的,有的字符是一个字节,有的是两个,有的是三个。
不是说计算机只能传输字节么,为什么这里能直接读取字符了,好,下面我带大家深入剖析一下Reader类。
废话少说,先上类图:
Java几乎为每一个InputStream都设计了一个对应的Reader,比如如果你想直接读取文件里的字符,可以用FileReader来代替FileInputStream。BufferedReader也是一个装饰者模式的reader,接收一个Reader作为参数,从而对Reader提供缓存功能。
但是这众多的Reader中,却有一部分没什么用(个人观点),先从Reader的源码看起:
public abstract class Reader implements Readable, Closeable { // 每次读取一个字符 public int read() throws IOException { char cb[] = new char[1]; if (read(cb, 0, 1) == -1) return -1; else return cb[0]; } abstract public int read(char cbuf[], int off, int len) throws IOException; // ...... 省略 ...... }
我这里只列出了Reader的两个灵魂函数,即read()和read(char cbuf[], int off, int len)
read()和InputStream中的read()相似,不过这里是只读取一个字符,而这个方法通过调用read(char cbuf[], int off, int len) 来实现,这个方法是抽象方法,Reader的子类通过实现这个方法达到读取不同介质的目的。
接下来就说说这个Reader家族中最重要的实现类,InputStreamReader类。
先看看这个类的结构:
接着还是先放部分代码上来。
public class InputStreamReader extends Reader { private final StreamDecoder sd; public InputStreamReader(InputStream in) { super(in); try { sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object } catch (UnsupportedEncodingException e) { // The default encoding should always be available 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 int read() throws IOException { return sd.read(); } public int read(char cbuf[], int offset, int length) throws IOException { return sd.read(cbuf, offset, length); } // ..... 省略 ..... }
从上面的代码可以看出,InputStreamReader有一个重要的域,就是这个
private final StreamDecoder sd;
就是这个域帮助InputStreamReader解决了编码的问题。其实这个StreamDecoder 类也是Reader的子类,从后面的read()方法也能看出,InputStreamReader的read()其实就是这个sd的read()方法,在剖析StreamDecoder 之前,我们再看一眼InputStreamReader的构造方法。
InputStreamReader有四个构造函数,我这里只说前两个,第一个接收一个InputStream作为参数。第二个多了一个charsetName,这就是指定了编码方式,第一种为什么不指定?如果不指定就采用系统默认的编码方式。这在后面的StreamDecode的源码中马上就能看出来。
现在我们再看看StreamDecode的源码:
public class StreamDecoder extends Reader { // StreamDecoder的静态构造方法,如果不指定编码,就采用默认的编码 public static StreamDecoder forInputStreamReader(InputStream var0, Object var1, String var2) throws UnsupportedEncodingException { String var3 = var2; if (var2 == null) { var3 = Charset.defaultCharset().name(); } try { if (Charset.isSupported(var3)) { return new StreamDecoder(var0, var1, Charset.forName(var3)); } } catch (IllegalCharsetNameException var5) { ; } throw new UnsupportedEncodingException(var3); } public static StreamDecoder forInputStreamReader(InputStream var0, Object var1, Charset var2) { return new StreamDecoder(var0, var1, var2); } public int read() throws IOException { return this.read0(); } // 由于在java中每个字符都是两个字节,因此这里每次读取两个字节,转成char类型 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]; int var3 = this.read(var2, 0, 2); switch (var3) { case -1: return -1; case 0: default: assert false : var3; return -1; case 2: this.leftoverChar = var2[1]; this.haveLeftoverChar = true; case 1: return var2[0]; } } } } }
这个类的核心就是read()这个方法,由于这里直接操作InputStream进行read(),因此可以读取出2个字节,java中每两个字节转成一个字符。
这就是Reader可以读取字符的原因,只不过是利用InputStream先将字节读取出来,再按照一定的编码方式转码,因此这就是我前面所说的Reader也不能读取字符的原因,因为它只是读字节,转字符而言。
最后再说一说这个BufferedReader,和BufferedInputStream类似,它也是一个装饰者模式的类,接收一个Reader类,提供缓存功能。看看它的源码:
public class BufferedReader extends Reader { private Reader in; // 如果不指定缓存长度,就使用默认值 public BufferedReader(Reader in) { this(in, defaultCharBufferSize); } 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++]; } } } }
我们这里只研究它最简单的构造方法,它的构造函数接收一个Reader对象,并建立一个缓存,如果未指定缓存长度,就使用默认的长度。
BufferedReader的灵魂方法read()和BufferedInputStream的read()方法类似,都是采用了一个fill方法,可以参考 java io -- FilterInputStream 与 装饰者模式这篇文章。
如果没有数据就用fill去读取一块数据,放在缓存里,如果缓存里有数据,直接从缓存里读就OK了。
总结:这篇文章总结了Reader类的用法与原理,但是本文没有具体涉及java的编码问题,这是一个较大的话题,有兴趣的可以去网上参考其他文章。