[Java]I/O底层原理之一:字符流字节流及其源码分析
Posted TengYunhao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Java]I/O底层原理之一:字符流字节流及其源码分析相关的知识,希望对你有一定的参考价值。
关于 I/O 的类可以分为四种:
- 关于字节的操作:InputStream 和 OutPutStream;
- 关于字符的操作:Writer 和 Reader;
- 关于磁盘的操作:File;
- 关于网络的操作:Socket( Socket 类不在 java.io 包中)。
在本篇博客中主要讲述前两种 I/O,即字符流与字节流,并会提及磁盘IO。首先我们来看一下字节流与字符流的实现关系,如下图
一、字节流
在字节流的类中,最顶层的是 Inputstream 抽象类和 OutputStream 抽象类,两者定义了一些关于字节数据读写的基本操作。他们的实现类有
- ByteArrayInputStream/ByteArrayOutputStream // 都对“字节数组”进行操作(写入/读出),两者可单独使用
- FileInputStream/FileOutputStream // 都对“文件”进行操作(写入/读出),两者可单独使用
- ObjectInputStream/ObjectOutputStream // 用于“对象”与“流”之间的转换,两者可单独使用
- PipedInputStream/PipedOutputStream // 建立一个链接(像管道),用于双方传输流数据,两者要一起使用
- FilterInputStream/FilterOutputStream // 装饰类,可以为其他类增加功能,应用了装饰者模式
- BufferedInputStream/BufferedOutputStream // 可以为被装饰的类增加缓冲功能
- DataInputStream/DataOutputStream // 可以为被装饰的类增加写读指定数据类型的功能
- LineNumberInputStream // 可以为被装饰的类增加获取行数的功能,现已被 LineNumberReader 替代
- PushbackInputStream // 可以将当前读取的数据推回到缓冲区
- PrintStream
- SequenceInputStream
- StringbufferInputStream // 将字符转换为字节,推荐使用 StringReader
1 InputStream类与OutputStream类
InputStream 抽象类中的方法如下:
int read() //读取一个字节 int read(byte b[]) //最多读取 b.length 长度的字节,并储存到数组 b 中,返回实际读取长度,利用 read() 实现 int read(byte b[], int off, int len) //同上,最多读取 len 个字节,从 off 开始保存 int available() //返回可以读取的字节数目,不受阻塞 long skip(long n) //跳过n个字节 void close() //关闭输入流,并释放与此流有关的所有资源,未实现,方法体为空 synchronized void mark(int readlimit) //标记当前位置,以便可以使用 reset() 方法复位到该标记的位置 synchronized void reset() //将当前位置复位到上次使用 mark() 方法标记的位置,未实现,调用抛出IO异常 boolean markSupported() //判断次输入流是否支持 mark() 和 reset() 方法
OutputStream 抽象类中的方法如下:
void write(int b) // 抽象的写入方法 void write(byte b[]) // 写入一个字节数组,利用 write(int b)实现 void write(byte b[], int off, int len) // 同上 void flush() void close()
1.1 ByteArrayInputStream类与ByteArrayOutputStream类
ByteArrayInputStream 与 ByteArrayOutputStream 都对“字节数组”的操作,将数据写入数组、或从数组读出数据。两者并无直接关系,都可以单独使用。
1) ByteArrayInputStream
创建 ByteArrayInputStream 对象时,需要一个“字节数组”,并且会存入其内部的缓冲区 (byte数组) 以待读取,我们可以通过 read() 方法来获取。
在 ByteArrayInputStream 内部还有几个属性:
- int pos 当前读取到的位置
- int mark 记录复位的位置
- int count 带读取字符总长度
本类中的方法相对其父类的方法也没有增加,使用也较为简单,就不再描述。另外,我们可以利用 ByteArrayInputStream 来读取 ByteArrayOutputStream 写入的数据,如下图
2) ByteArrayOutputStream
创建 ByteArrayOutputStream 时,可以指定其内部缓冲区 (byte数组) 的大小 (默认32字节),我们可以通过 wirte() 方法向缓冲区写入数据,如果缓冲区长度不够时,会自动扩大至少一倍。
在 ByteArrayOutputStream 内部还有几个属性:
- int count 用来记录缓冲区的大小
- static final int MAX_ARRAY_SIZE 缓冲区的最大值
如果需要使用这些数据时我们可以通过 toString() 、toByteArray() 从缓冲区中获取,也可以通过 writeTo() 将缓冲区数据其写入其它 OutputStream 类中,如下图
ByteArrayOutputStream 的部分方法如下:
synchronized void write(int b) // 向缓冲区写入数据 synchronized void write(byte b[], int off, int len) synchronized void writeTo(OutputStream out) //将缓冲区数据写入到其他输出流中 synchronized void reset() //重置缓冲区 synchronized byte toByteArray()[]synchronized String toString() synchronized String toString(String charsetName) synchronized String toString(int hibyte) // @Deprecated 已弃用
1.2 FileInputStream类与FileOutputStream类
FileInputStream 与 FileOutputStream 都对“文件”的操作,将数据写入文件、或从文件读出数据。两者并无直接关系,都可以单独使用。与 ByteArrayInputStream 和 ByteArrayOutputStream 类似,但不同的是一个操作文件,一个操作字符数组。
1) FileInputStream
创建 FileInputStream 对象时,我们可以传入文件的路径、对象或者描述符对象。此时会检查文件是否为空、是否有效、是否可读 (使用 System.getSecurityManager() 获取安全管理器来检查),然后调用本地方法打开文件。
在 FileInputStream 中还有几个属性用来描述文件信息:
- FileDescriptor 文件的描述符
- String path 存储了文件的路径
- FileChannel
当我们从文件中读取数据时,FileInputStream 会再去调用本地方法去实现此操作。FileInputStream 部分方法如下:
int read() // 读取1个字节,如果已经读完返回-1,调用native read0()来实现,不会阻塞 int read(byte b[]) // 读取到字节数组中,调用native readBytes()来实现,不会阻塞 int read(byte b[],int off, int len) native long skip(long n) // 跳过n个字节 native int available() FileDescriptor getFD() // 返回表示到文件系统中实际文件的连接的 FileDescriptor 对象。 FileChannel getChannel() // 返回与此文件输入流有关的唯一FileChannel 对象 void close() //关闭流,用closeLock变量作为锁在synchronized代码块中改变closed属性,再调用本地方法关闭 void finalize() // 确保在不再引用文件输入流时调用其close()方法
2) FileOutputStream
创建 FileOutputStream 对象时,我们可以传入文件的路径、对象或者描述符对象,并且可以再传入一个 boolean 值,判断是否要以追加的方式写入。同样,要判断文件是否为空、是否有效、是否可写,然后调用本地方法打开文件。
在 FileOutputStream 中的部分属性如下:
- 描述符 FileDescriptor
- 路径
- FileChannel
当我们向文件中写入数据时,FileOutputStream 会再去调用本地方法去实现此操作。FileOutputStream 部分方法如下:
void write(int b) // 向文件中写入一个字节,调用native write()来实现 void write(byte b[]) // 向文件中写入一个字节数组,调用native writeBytes()来实现 void write(byte b[], int off, int len) // 同上 void close() //关闭流,用closeLock变量作为锁在synchronized代码块中改变closed属性,再调用本地方法关闭
FileDescriptor getFD()
FileChannel getChannel()
void finalize() //
1.3 ObjectInputStream类和ObjectOutputStream类
ObjectInputStream 和 ObjectOutputStream 用于“对象”与“流”之间的转换,从流中读出对象或将对象写入流中(这里的流可能是来自文件,也可能是来自网络)。另外,该类所读写的对象必须实现Serializable接口.
1) ObjectInputStream
创建 ObjectInputStream 对象时,需要传入一个流(如 FileInputStream),如下图
在 ObjectInputStrean 中的内部类
- private static class Caches
- public static abstract class GetField
- private class GetFieldImpl extends GetField
- private static class ValidationList
- private static class PeekInputStream extends InputStream
- private class BlockDataInputStream extends InputStream implements DataInput
- private static class HandleTable
ObjectInputStream 中的部分方法如下
Object readObject()
Object readUnshared()
void defaultReadObject()
ObjectInputStream.GetField readFields()
void registerValidation(ObjectInputValidation obj, int prio)int read()
int read(byte[] buf, int off,int len)
int available()
void close()
boolean readBoolean()
byte readByte()
int readUnsignedByte()
char readChar()
short readShort()
int readUnsignedShort()
int readInt()
long readLong()
float readFloat()
double readDouble()
void readFully(byte[] buf)
void readFully(byte[] buf, int off,int len)
int skipBytes(int len)
public String readUTF()
2) ObjectOutputStream
创建 ObjectOutputStream 对象时,传入一个流(如 FileOutputStream),如下图
在 ObjectOutputStream 中的内部类
- private static class Caches
- public static abstract class PutField
- private class PutFieldImpl extends PutField
- private static class BlockDataOutputStream extends OutputStream implements DataOutput
- private static class HandleTable
- private static class ReplaceTable
- private static class DebugTraceInfoStack
1.4 PipedInputStream类与PipedOutputStream类
PipedInputStream 和 PipedOutputStream 的实现原理类似于“生产者-消费者”原理,PipedOutputStream 是生产者,PipedInputStream 是消费者,在 PipedInputStream 中有一个 buffer 字节数组作为缓冲区,默认大小为1024,存放“生产者”生产出来的东西。还有两个变量 in 和 out,in 是用来记录“生产者”生产了多少,out 是用来记录“消费者”消费了多少,in 为-1表示消费完了,in==out 表示生产满了。当消费者没东西可消费的时候,也就是当 in 为-1的时候,消费者会一直等待,直到有东西可消费。
PipedInputStream 和 PipedOutputStream 数据传输的过程:
- 首先两者要建立一个完整管道,实现方式是“输入流”调用“输出流”的 connect() 方法并将自己传入。在这个方法中“输出流”拿到了“输入流”的对象,然后将其传入 sink 变量,并设置“输入流”的 connected 属性为 true。
- 建立好完整管道后就可以传输数据了,因为刚建立好的管道里并没有缓存数据,所以首先向管道中写入数据。我们使用“输出流”的 write() 方法来写入数据,在此方法中会调用“输入流”的 receive() 方法并将数据传入。如果此时“输入流”线程在等待状态,可以调用“输出流”的 flush() 方法,这个方法中会调用“输入流”的 notifyAll() 方法将其唤醒。当数据通过“输入流”的 receive() 方法进入“输入流”后,将其放入缓冲区中以待读取。
- 当数据写入到“输入流”的缓冲区后,“输入流”就可以使用 read() 方法读取数据了。当“输入流”与“输出流”使用完毕后,应调用双方的 close() 方法断开链接。
1) PipedInputStream
创建 PipedInputStream 对象时,可以根据 PipedOutputStream 对象来建立一个完整的管道链接,并设置缓冲区大小(默认1024)。如果创建对象时不建立连接,那么也要在使用前将链接建立好。
我们来看一下部分属性
- boolean closedByWriter 与 volatile boolean closedByReader 用来记录管道是否关闭
- connected 用来记录当前管道是否链接
- Thread readSide 与 Thread writeSide;
- byte buffer[] 缓冲区
- int in 初始为-1,表示从输出管道读出下一个字节后在缓冲区储存的位置,in==-1表示缓冲区为空,in==out表示缓冲区已满
- int out 初始为0,表示从缓冲区读取下一个字节的位置
PipedInputStream相关方法:
private void initPipe(int pipeSize) //初始化缓冲区 public void connect(PipedOutputStream src) //与输出管道建立连接 protected synchronized void receive(int b) //接收一个字节的数据,没有数据输入则阻塞 synchronized void receive(byte b[], int off, int len) //接收大小为len的数据到数组,不足则阻塞 private void checkStateForReceive() //判断是否建立连接,读写是否已关闭 private void awaitSpace()synchronized void receivedLast() //通知所有等待的线程,最后一个字符已经接收 public synchronized int read() //从缓冲区读取字节,返回读取的字节,如果缓冲区没有数据则会阻塞 public synchronized int read(byte b[], int off, int len) //同上 public synchronized int available()
public void close()
2) PipedOutputStream
创建 PipedOutputStream 对象时,同样可以传入一个 PipedInputStream 对象,然后来建立一个完整的管道连接,如果不建立连接,那要在使用前将链接建立好。PipedOutputStream 没有缓存区,写入的数据会进入到 PipedInputStream 的缓存区中。
PipedOutputStream 只有一个属性:PipedInputStream sink 用来存放所链接的输入流对象。
PipedOutputStream相关方法:
public synchronized void connect(PipedInputStream snk) // 与输入流建立连接,需要先获取到对象锁 public void write(int b) // 调用连接的输入流的receive方法,向其写入一个字节 public void write(byte b[], int off, int len) // 同上,写入一个字节数组 public synchronized void flush() // 唤醒所连接的输入流 public void close()
关于 PipedInputStream 和 PipedOutputStream 的几点要注意:
- 两者建立连接时,不能既调用 PipedInputStream 的 connect() 方法又调用 PipedOutputStream 的 connect() 方法,否则会抛出IO异常。且 PipedInputStream 实现 connect() 方法是去调用 PipedOutputStream 的 connect() 方法。
- 在一个线程里使用 PipedInputStream 和 PipedOutputStream 容易造成卡死(“死锁”)。例如,当我们调用 PipedOutputStream 的 write() 方法写入1024字节数据时,该方法通过调用 PipedInputStream 的 receive() 方法将字符数组中的数据写入到缓冲区,我们在读取128字节数据后,继续再向缓冲区写入1024字节数据,会发现程序卡在写入数据的这个过程中,造成死锁。原因是我们第二次向缓冲区写入数据时,缓冲区满了,这时候 PipedInputStream 的 awaitSpace() 方法会去执行 notifyAll() 试图通知 read() 方法去读取数据,在缓冲区的数据被读取之前,write() 方法会一直阻塞下去。又因为 PipedInputStream 和 PipedOutputStream 在一个线程里,write() 方法的阻塞导致 read() 方法不能去缓冲区读取数据,从而形成“死锁”。
产生“死锁”的代码: 输出结果:
1.5 FilterInputStream类与FilterOutputStream类及它们的子类
FilterInputStream 与 FilterOutputStream 及子类是使用了装饰者设计模式。为什么要使用装饰者设计模式?如果我们觉得某几个类都缺少一些功能的时候,根据开闭原则,我们不去修改代码,那我们可以利用继承对功能进行扩展,但是如果几个类都需要扩展相同的功能,那么就必须分别对这个几个类扩展。如果使用装饰者设计模式,我们就可以解决这个问题。举个例子,当我们使用 ByteArrayInputStream 的时候只能读取到以 byte 为单位的数据,我们可以按需要把读取出来的字节进行转码,转成我们需要的数据,那么能不能直接读取int、long等数据呢?答案是可以的,我们利用 DataInputStream 类对其进行装饰即可拥有 readInt()、readLong() 这个方法。
1) FilterInputStream
创建 FilterInputStream 时需要传入被修饰的输入流对象,并使用 volatile InputStream in 记录该对象。FilterInputStream 是所有输入流修饰类的父类,但其本身并没有修饰作用,而是直接使用被修饰类的方法,如下是类中的方法
public int read() public int read(byte b[]) public int read(byte b[], int off, int len) public long skip(long n) public int available() public void close() public synchronized void mark(int readlimit) public synchronized void reset() public boolean markSupported()
2) FilterOutputStream
创建 FilterOutputStream 时需要传入被修饰的输出流对象,并使用 OutputStream out 记录该对象。FilterOutputStream 是所有输出流修饰类的父类,其本身也没有修饰作用,如下是类中的方法
public void write(int b) public void write(byte b[]) public void write(byte b[], int off, int len) public void flush() public void close()
1.5.1 DataInputStream类与DataOutputStream类
DataInputStream 与 DataOutputStream 是装饰类,可以对继承了 InputStream 的类进行装饰,使其用于读取或写入指定类型内容的数据。
1) DataInputStream
创建时需要传入被包装的输入流对象,相关方法如下
public final int read(byte b[]) // 调用被装饰对象的read()方法 public final int read(byte b[], int off, int len) // 同上 public final void readFully(byte b[]) // 读取数据到数组,直到存满,当流中数据不足时,readFully()会阻塞等待 public final void readFully(byte b[], int off, int len) // 同上 public final int skipBytes(int n) // 跳过n个字节 public final boolean readBoolean() public final byte readByte() public final int readUnsignedByte() public final short readShort() // 连续调用read()两次,然后将结果拼接后强转short类型,(short)((ch1<<8)+(ch2<<0)) public final int readUnsignedShort() // 同上,但不需要进行强转 public final char readChar() public final int readInt() public final long readLong() // 利用readFully()读取8个字节到readBuffer数组,然后拼接转换成long类型 public final float readFloat() // 使用Float.intBitsToFloat(readInt()) public final double readDouble() // 使用Double.longBitsToDouble(readLong()) public final String readLine() // 弃用的 public final String readUTF() public final static String readUTF(DataInput in) // 待研究
2) DataOutputStream
DataOutputStream 相关方法如下
private void incCount(int value) // 记录写入到流的字节数 public synchronized void write(int b) public synchronized void write(byte b[], int off, int len) public void flush() // 让缓存中的数据传入到输入流中 public final void writeBoolean(boolean v) public final void writeByte(int v) public final void writeShort(int v) public final void writeChar(int v) public final void writeInt(int v) public final void writeLong(long v) public final void writeFloat(float v) // writeInt(Float.floatToIntBits(v)) public final void writeDouble(double v) // writeLong(Double.doubleToLongBits(v)); public final void writeBytes(String s) public final void writeChars(String s) public final void writeUTF(String str) static int writeUTF(String str, DataOutput out) public final int size()
1.5.2 BufferedInputStream类与BufferedOutputStream类
当我们在使用 FileInputStream 和 FileOutputStream 时,要用一个 byte 数组用来接收或写入数据,硬盘存取的速度远低于内存中的数据存取速度,为了减少对硬盘的存取,通常从文件中一次读入一定长度的数据,而写入时也是一次写入一定长度的数据,这可以增加文件存取的效率。
1) BufferedInputStream
BufferedInputStream 有一个字节数组 buf 作为缓冲区,默认为8192字节。当使用 read() 方法读取数据时,实际上是先读取 buf 中的数据,当 buf 中的数据不足时,BufferedInputStream 会再从被装饰的 InputStream 对象的 read() 方法中读取数据填满 buf ,然后再从 buf 中读取所需大小的数据。如果一次读取的数据大小超过 buf 缓冲区的大小,则放弃缓冲直接调用被装饰对象的 read() 方法。
另外,BufferedInputStream 利用原子更新字段类 (AtomicReferenceFieldUpdater) 对缓冲区 volatile byte buf[] 进行包装。当我们对缓冲区扩容时,得到一个新的缓冲区数组后替换旧的缓冲区数组,此时就要利用原子更新字段类更新。
/* AtomicReferenceFieldUpdater 位于Atomic包中,是一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新(注意这个字段不能是private的)。通过静态方法newUpdater就能创建它的实例,三个参数分别是:包含该字段的对象的类、将被更新的对象的类、将被更新的字段的名称。 */ private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = AtomicReferenceFieldUpdater.newUpdater (BufferedInputStream.class, byte[].class, "buf"); protected int count; // 缓冲区内容大小 protected int pos; // 缓冲区当前位置 protected int markpos = -1; protected int marklimit; // 该类中的方法: private InputStream getInIfOpen() // 检查流是否存在 private byte[] getBufIfOpen() // 检查缓冲区是否存在 private void fill() // public synchronized int read() private int read1(byte[] b, int off, int len) public synchronized int read(byte b[], int off, int len) public synchronized long skip(long n) public synchronized int available() public synchronized void mark(int readlimit) public synchronized void reset() public boolean markSupported() public void close()
2) BufferedOutputStream
BufferedOutputStream 也有一个字节数组 buf 作为缓冲区,默认同样为8192字节,并用一个变量 count 记录缓冲区内数据大小。当使用 write() 方法写入数据时,实际上会先将数据写至 buf 中,当 buf 已满时,BufferedOutputStream 才会利用被装饰的 OutputStream 对象的 write() 方法写入数据。如果一次写入的数据大小超过 buf 缓冲区的大小,则放弃缓冲直接调用被装饰对象的 write() 方法。
BufferedOutputStream 的相关方法如下
private void flushBuffer() // 将缓冲区的数据写入流,调用了被装饰对象的write方法 public synchronized void write(int b) // 将数据写入缓冲,如果缓冲满了,调用flushBuffer()方法 public synchronized void write(byte b[], int off, int len) // 同上 public synchronized void flush()
1.5.3 PushbackInputStream类
对输入流进行装饰,可以将当前读取的字节数据推回到缓存区,一般用不到的。
// 该类中的属性: protected byte[] buf; protected int pos; // 该类中的方法: private void ensureOpen() public int read() public int read(byte[] b, int off, int len) public void unread(int b) public void unread(byte[] b, int off, int len) public void unread(byte[] b) public int available() public long skip(long n) public boolean markSupported() // 不支持mark功能 public synchronized void mark(int readlimit) public synchronized void reset() public synchronized void close()
1.5.4 LineNumberInputStream类
对输入流进行装饰,可以获取输入流的行数或设置行数,已过时。已经被LineNumberReader替代。
1.5.5 PrintStream类
对输出流进行装饰,
// 构造方法: private PrintStream(boolean autoFlush, OutputStream out) // 传入一个需装饰的对象 private PrintStream(boolean autoFlush, OutputStream out, Charset charset) private PrintStream(boolean autoFlush, Charset charset, OutputStream out) public PrintStream(OutputStream out) public PrintStream(OutputStream out, boolean autoFlush) public PrintStream(OutputStream out, boolean autoFlush, String encoding) public PrintStream(String fileName) public PrintStream(String fileName, String csn) public PrintStream(File file) public PrintStream(File file, String csn) // 该类中的属性: private final boolean autoFlush; private boolean trouble = false; private Formatter formatter; private BufferedWriter textOut; private OutputStreamWriter charOut; private boolean closing = false; // 该类中的方法: private static <T> T requireNonNull(T obj, String message) private static Charset toCharset(String csn) private void ensureOpen() public void flush() public void close() public boolean checkError() protected void setError() protected void clearError() public void write(int b) public void write(byte buf[], int off, int len) private void write(char buf[]) private void write(String s) private void newLine() public void print(boolean b) public void print(char c) public void print(int i) public void print(long l) public void print(float f) public void print(double d) public void print(char s[]) public void print(String s) public void print(Object obj) public void println() public void println(boolean x) public void println(char x) public void println(Java之I/O流