说到IO就要对应到输入和输出,字节流和字符流,字符流也是通过字节流来实现的,我们来慢慢分析。
先说说字节流的输入类,输入我们就要拿到输入流来读取数据,对应到java中,java.io.InputStream这个类是一个抽象类,定义了字节输入的基本的方法,这个类
中有三个read方法,如下所示
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {}
public abstract int read() throws IOException;
三个抽象方法中第一个抽象方法是调用第二个抽象方法实现的,阅读第二个抽象方法的代码,这个方法的主要逻辑就是从byte数组的偏移量off开始读取len长度哥字节放入到byte数组中然后返回读取到的长度,他是一个字节一个字节循环读取的,那么一个字节怎么读取呢,他调用的时另一个read方法,也就是上面的第三个read方法,可以看到第三个read方法是一个抽象的方法,没有实现,那由谁来实现呢,自然是由子类来实现了,这个read方法当读取到末尾的时候就返回-1,java.io.InputStream类只是一个抽象,定义一些公共的方法,将具体的读取目标进行抽象,具体是从哪里读,磁盘还是网络由子类实现,我们接下来分析一下他的一个子类java.io.FileInputStream,见名知意,他就是读取本地磁盘的文件,FileInputStream继承了InputStream,我们看看他的read方法,代码如下:
public int read() throws IOException {
Object traceContext = IoTrace.fileReadBegin(path);
int b = 0;
try {
b = read0();
} finally {
IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1);
}
return b;
}
看到他是调用另一个read0方法是实现的,再看看read0方法:
private native int read0() throws IOException;
这是一个native的方法,读取本地磁盘文件的这个操作肯定不是我们JVM这个应用进程该干的事情,肯定是由操作系统来发起的,所以这个方法就发起了一个系统调用,这个过程可能需要一段时间来完成,这是一个阻塞的方法,在进行系统调用的这段时间里我们的这个线程会被挂起,不会获得CPU的执行时间,操作系统调用磁盘硬件厂商提供的驱动读取磁盘文件,将读取完的数据放入到操作系统内核的内存中,读取完成后再将数据移动到我们JVM进程的内存中,这就发生了一次从操作系统内核空间到我们JVM内存空间的一次内存拷贝。这段过程也是阻塞进行的,可以看到一次IO操作还是很耗费时间的,因为有很多的阻塞存在,这样子一个基于流的读取操作就完成了。
假如我们有一个file.txt文件,用UTF-8来编码的,我们在里面输入一个字符a,然后测试一下读取,测试代码如下:
public static void main(String[] args) throws Exception{
File file = new File("/Users/yuanzq/Desktop/work/liequ/io/src/bio/file.txt");
FileInputStream fis = new FileInputStream(file);
int b ;
while((b = fis.read()) != -1){
System.out.println((byte)b);
}
fis.close();
}
输出结果为:97
可见字符a一个字节来表示的,97正好是小写字母a的ascii码值,接着测试输入一个汉字’啊’,输出结果为:
-27
-107
-118
可见汉字’啊‘UTF-8是用三个字节来表示的,至于字符的处理等下我们分析Reader类的时候会详细说明。
上面测试的磁盘文件的IO,下面来看一下网络IO,java.io.FileInputStream有一个子类java.net.SocketInputStream,我们在编写一个网络应用程序的时候读取数据会调用socket.getInputStream()拿到InputStream对象来处理输入,自己跟一下代码就会发现返回的就是这个java.net.SocketInputStream对象,这个类又4个read方法
public int read() throws IOException
public int read(byte b[]) throws IOException
public int read(byte b[], int off, int length) throws IOException
int read(byte b[], int off, int length, int timeout) throws IOException
第一个read方法就是对InputStream的抽象的read方法实现,他调用了第三个read方法,而第二个和第三个read方法会最后调用到第四个read方法上,阅读第四个read方法的代码,会发现最后调用的是一个native的socketRead0方法,将byte数组传过去,这个方法就发起了一次系统调用和文件IO类似的阻塞由操作系统完成。
private native int socketRead0(FileDescriptor fd,
byte b[], int off, int len,
int timeout)throws IOException;
看来不论是磁盘IO还是网络IO对于我们乱说原理都是一样的,我们这里说的是输入,输出肯定也是类似的.
OutputStream定义了抽象的write方法
public abstract void write(int b) throws IOException;
FileOutputStream:
private native void write(int b, boolean append) throws IOException;
FileOutputStream的子类SocketOutputStream:
public void write(int b) throws IOException {
temp[0] = (byte)b;
socketWrite(temp, 0, 1);
}
private native void socketWrite0(FileDescriptor fd, byte[] b, int off,
int len) throws IOException;
下面我们来看看JDK中的字符流的实现,对于输入来说有一个抽象类java.io.Reader这个类和InputStream类相似的定义了读取的抽象,他的read方法代码如下:
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);
}
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;
}
abstract public int read(char cbuf[], int off, int len) throws IOException;
最终调用的都是最后的这个abstract的read方法,这个方法由子类来实现,注意看他们的参数已经不是byte了,而变成了char,下面来看一下他的子类,InputStreamReader和FileReader,其中FileReader是InputStreamReader的子类, FileReader的代码如下所示:
public FileReader(String fileName) throws FileNotFoundException {
super(new FileInputStream(fileName));
}
public FileReader(File file) throws FileNotFoundException {
super(new FileInputStream(file));
}
public FileReader(FileDescriptor fd) {
super(new FileInputStream(fd));
}
可以看出FileReader只是一个桥梁,他就像InputStreamReader字符输入流和InputStream字节输入流建立了一个桥梁而已。实际的读取实现方法封装在InputStreamReader类中
InputStreamReader也是通过StreamDecoder包装的InputStream,只是读取字节字节转换为一个字符,实际的转化要根据设置的编码类型,InputStreamReader类的构造方法如下所示,他初始化了一个StreamDecoder的本地变量:
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);
}
}
由此猜测InputStreamReader的read方法的实现应该也是调用这个StreamDecoder类实现的,看看源码
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
public int read() throws IOException {
return sd.read();
}
果然如此,那么StreamDecoder就变成了一个很重要的类,StreamDecoder也继承了Reader这个抽象类,下面来分析这个类,forInputStreamReader的代码如下所示:
public static StreamDecoder forInputStreamReader(InputStream in,
Object lock,
String charsetName)
throws UnsupportedEncodingException
{
String csn = charsetName;
if (csn == null)
csn = Charset.defaultCharset().name();
try {
if (Charset.isSupported(csn))
return new StreamDecoder(in, lock, Charset.forName(csn));
} catch (IllegalCharsetNameException x) { }
throw new UnsupportedEncodingException (csn);
}
首先创建了一个默认的字符Charset对象然后返回StreamDecoder对象,这个对象中真正包装了InputStream输入流对象,假设我们现在取到的charsetName是UTF-8,局部变量csn的值就为UTF-8,Charset.forName(csn)创建了一个Charset对象,我们跟下源码Charset.forName方法:
public static Charset forName(String charsetName) {
Charset cs = lookup(charsetName);
if (cs != null)
return cs;
throw new UnsupportedCharsetException(charsetName);
}
他调用了lookup方法,继续跟踪:
private static Charset lookup(String charsetName) {
if (charsetName == null)
throw new IllegalArgumentException("Null charset name");
Object[] a;
if ((a = cache1) != null && charsetName.equals(a[0]))
return (Charset)a[1];
// We expect most programs to use one Charset repeatedly.
// We convey a hint to this effect to the VM by putting the
// level 1 cache miss code in a separate method.
return lookup2(charsetName);
}
首先从cache1中提取,这个cache1是什么呢,是什么时候初始化的呢:
private static volatile Object[] cache1 = null; // "Level 1” cache
private static void cache(String charsetName, Charset cs) {
cache2 = cache1;
cache1 = new Object[] { charsetName, cs };
}
我们只找到cache这个方法对cache1进行了初始化,所以猜测他应该是查询完成之后初始化的,第一次调用肯定是null,那么从lookup就接着往后走,到了lookup2方法,代码如下:
private static Charset lookup2(String charsetName) {
Object[] a;
if ((a = cache2) != null && charsetName.equals(a[0])) {
cache2 = cache1;
cache1 = a;
return (Charset)a[1];
}
Charset cs;
if ((cs = standardProvider.charsetForName(charsetName)) != null ||
(cs = lookupExtendedCharset(charsetName)) != null ||
(cs = lookupViaProviders(charsetName)) != null)
{
cache(charsetName, cs);
return cs;
}
/* Only need to check the name if we didn't find a charset for it */
checkName(charsetName);
return null;
}
先从cache2中取,cache2又是什么的,看定义是第二个级别的缓存,和cache1一样都是使用一个数组实现的,也都是调用完成后初始化的,看上面代码的逻辑如果从cache2中匹配到了要查找的字符对象那么就cache1和cache2交换,这样子便提高了查找的速度,下次查找同样的字符集就lookup中即可完成:
private static volatile Object[] cache2 = null; // "Level 2" cache
这里第一次访问肯定都是null,接着往下走,看接下来这段代码:
if ((cs = standardProvider.charsetForName(charsetName)) != null ||
(cs = lookupExtendedCharset(charsetName)) != null ||
(cs = lookupViaProviders(charsetName)) != null)
调用了三个方法查找字符集,
找到之后就返回了一个Charset对象,回到StreamDecoder类继续跟踪他的构造方法,第一个构造方法通过前面创建好的Charset创建了另一个很重要的对象CharsetDecoder,接着调用下一个构造方法,最终的构造方法将创建好的CharsetDecoder,Charset对象保存为类变量
StreamDecoder(InputStream in, Object lock, Charset cs) {
this(in, lock,
cs.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
}
StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) {
super(lock);
this.cs = dec.charset();
this.decoder = dec;
// This path disabled until direct buffers are faster
if (false && in instanceof FileInputStream) {
ch = getChannel((FileInputStream)in);
if (ch != null)
bb = ByteBuffer.allocateDirect(DEFAULT_BYTE_BUFFER_SIZE);
}
if (ch == null) {
this.in = in;
this.ch = null;
bb = ByteBuffer.allocate(DEFAULT_BYTE_BUFFER_SIZE);
}
bb.flip(); // So that bb is initially empty
}
接下来的通过注释可以看出是不会走的路径,紧着像下走ch对象肯定是null的,就将InputStream对象保存到为实例变量(这里很重要,我们获取字节数据靠的就是这个InputStream对象),将this.ch设置为空,接着对创建一个8M空间的ByteBuffer对象保存为实例变量作为缓冲区。
接下来看看CharsetDecoder的几个read方法InputStreamReaader这个类的几个read方法都是调用的StreamDecoder来实现的,StreamDecoder的read方法如下,其他的read方也都是调用这个read方法实现的,下面我们就来分析这个read方法,offset代表偏移量就是从cbuf数组的哪个位置开始读,length代表要读取得字符长度:
public int read(char cbuf[], int offset, int length) throws IOException {
int off = offset;
int len = length;
synchronized (lock) {
ensureOpen();
if ((off < 0) || (off > cbuf.length) || (len < 0) ||
((off + len) > cbuf.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
}
if (len == 0)
return 0;
int n = 0;
if (haveLeftoverChar) {
// Copy the leftover char into the buffer
cbuf[off] = leftoverChar;
off++; len--;
haveLeftoverChar = false;
n = 1;
if ((len == 0) || !implReady())
// Return now if this is all we can produce w/o blocking
return n;
}
if (len == 1) {
// Treat single-character array reads just like read()
int c = read0();
if (c == -1)
return (n == 0) ? -1 : n;
cbuf[off] = (char)c;
return n + 1;
}
return n + implRead(cbuf, off, off + len);
}
}
进入synchronized锁,他所住的对象就是当前创建的InputStreamReader对象,在StreamDecoder的构造函数中调用父类Reader来持有的。经过一系列的检查之后,判断了一个boolean类型的haveLeftoverChar的值,默认第一次调用这个值肯定是false的,注意StreamDecoder有一个无参数的read()方法,这个方法代表的就是读取一个两个字节字符,他调用了read0方法,它的代码如下:
private int read0() throws IOException {
synchronized (lock) {
// Return the leftover char, if there is one
if (haveLeftoverChar) {
haveLeftoverChar = false;
return leftoverChar;
}
// Convert more bytes
char cb[] = new char[2];
int n = read(cb, 0, 2);
switch (n) {
case -1:
return -1;
case 2:
leftoverChar = cb[1];
haveLeftoverChar = true;
// FALL THROUGH
case 1:
return cb[0];
default:
assert false : n;
return -1;
}
}
}
他定义了一个两个字符的数组,调用上面所述的带有三个参数的read方法来实现读取,当读取的到的字符是两个字节的时候就将这个haveLeftoverChar赋值为true,将leftoverChar变量赋值为读取到的第二个字节,根据这些猜测这个boolean类型的值应该是为了兼容先前已经调用过这个read0方法的代码里面读取到的数据是两个字节的情况。
接下来继续分析,如果读取的长度是1,那么就调用上面所说的read0方法来实现,接下来的代码很重要:
return n + implRead(cbuf, off, off + len);
他调用了implRead方法,传入的参数分别是,字符数组,偏移量,偏移量+要读取的长度,这个方法的代码比较长,就不贴出来了,慢慢分析:
首先将字符数组包装成了一个CharBuffer对象,接着进入了一个自选的方法,首先调用创建的CharsetDecoder的decode方法解码处理目前有的字节数据,接着调用了一个readBytes方法,这个方法就是真正的调用InputStream对象的read方法读取数据了:
int lim = bb.limit();
int pos = bb.position();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
assert rem > 0;
int n = in.read(bb.array(), bb.arrayOffset() + pos, rem);
if (n < 0)
return n;
if (n == 0)
throw new IOException("Underlying input stream returned zero bytes");
assert (n <= rem) : "n = " + n + ", rem = " + rem;
bb.position(pos + n);
他会尽力去读取,直到将构造方法中创建的这个8M的ByteBuffer读满或者读取到了结尾,然后返回,看到这里豁然开朗,原来最终还是调用的InputStream方法处理的,读取完之后返回,剩下的就是返回的StreamDecoder方法中去将这些字节的数据转换为字符了。
说完了Reader,那么猜测Writer应该也是一样的逻辑,OutputStreamWriter类中包装了StreamEncoder对象将字符编码成一个个字节到一个ByteBuffer中最后还是调用的OutputStream的write方法实现的。