Java I/O源码分析 - InputStream,Outpu
- 说明
- IntputStream,OutputStream 简介
- 助于理解
- ByteArrayInputStream
- ByteArrayOutputStream
- PipedInputStream,PipedOutputStream
- PipedOutputStream
- FilterInputStream,FilterOutputStream
- BufferedInputStream,BufferedOutputStream
- DataInputStream,DataOutputStream
- PrintStream
说明
整个系列的文章全部参考或直接照搬下面两位作者的文章,这里只是根据自己需要对原作者的文章梳理的总结,仅给自己日后复习时提供思路,如有读者看到学习时建议移步原作。再次重申并非我所写
- 潘威威:Java8 I/O源码-目录
- skywang12345:Java I/O系列
另一篇本人总结的IO系列
HikariCP:Java IO源码分析 - Reader,Writer系列(一)
HikariCP:Java IO源码分析 - Reader,Writer系列(二)
IntputStream,OutputStream 简介
- 所有字节输入流的类的父类 IntputStream。
- 所有字节输出流的类的父类 OutputStream。
助于理解
无论是输入流还是输出流,都是相对于内存的,即内存数据的输入还是输出,所以InputStream就是往内存输入数据的输入流。对于内存的动作就是read读取。相对的OutputStream就是从内存中往外输出数据,对于内存的动作的就是write操作。
public abstract class InputStream implements Closeable {
}
public abstract class OutputStream implements Closeable, Flushable {
}
所有字节输入流的父类 InputStream 有这样一个抽象方法:
public abstract int read() throws IOException;
所以字节输入流必须提供返回下一个输入字节的read()方法。
ByteArrayInputStream
- ByteArrayInputStream 支持 mark/reset。
- ByteArrayInputStream的close方法无效,无法关闭此输入流。
public void mark(int readAheadLimit) {
// 设置流中的当前标记位置
mark = pos;
}
public synchronized void reset() {
// 将缓冲区的位置重置为标记位置
pos = mark;
}
public void close() throws IOException {
}
实现了父类InputStream的read方法。
/*
* 返回一个 0 到 255 范围内的 int 字节值。
* 负数用补码进行计算
*/
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
需要注意的是,如果buf数组中有负数的话,负数在取出时&与
运算用负数的补码(除符号位全部取反并+1)进行计算。
ByteArrayOutputStream
头
public class ByteArrayOutputStream extends OutputStream {
/**
* The buffer where data is stored.
* 存储数据的缓冲区
*/
protected byte buf[];
/**
* The number of valid bytes in the buffer.
* 缓冲区中的有效字节数
*/
protected int count;
}
// 缓冲区容量初始化为32,如有必要可扩容。通过ensureCapacity
public ByteArrayOutputStream() {
this(32);
}
public ByteArrayOutputStream(int size) {
if (size < 0) {
throw new IllegalArgumentException("Negative initial size: "
+ size);
}
buf = new byte[size];
}
ensureCapacity,grow,hugeCapacity
// 确保缓冲区可以存放多少元素,必要时扩容
private void ensureCapacity(int minCapacity) {
// overflow-conscious code
if (minCapacity - buf.length > 0)
grow(minCapacity);
}
// 增加缓冲区容量,使其至少可以存放minCapacity个元素
// minCapacity : 期望最小容量
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = buf.length;
// 扩容2倍
int newCapacity = oldCapacity << 1;
if (newCapacity - minCapacity < 0)
// 如果扩容两倍还是小,那么容量赋值成该期望容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 如果还是不够,那么最大提升到 Integer.MAX_VALUE 舍弃到了头信息
newCapacity = hugeCapacity(minCapacity);
buf = Arrays.copyOf(buf, newCapacity);
}
// 计算允许分配给byte数组的最大容量
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// 一些虚拟机会存一些头信息到数组中,如数组的地址类型等,提升性能
// JVM默认规定数组最大容量就是Integer.MAX_VALUE,再打会内存溢出
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
write,writeTo
// 将指定的字节写入输出流
public synchronized void write(int b) {
ensureCapacity(count + 1);
buf[count] = (byte) b;
count += 1;
}
implement了父类OutputStream抽象类的write方法
public abstract void write(int b) throws IOException;
// 将指定byte数组中从偏移量off开始的len个字节写入输出流
public synchronized void write(byte b[], int off, int len) {
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) - b.length > 0)) {
throw new IndexOutOfBoundsException();
}
ensureCapacity(count + len);
System.arraycopy(b, off, buf, count, len);
count += len;
}
// 将此byte数组输出流的全部内容写入到指定的输出流参数out中
public synchronized void writeTo(OutputStream out) throws IOException {
out.write(buf, 0, count);
}
重要函数
// 将输出流的count字段重置为零,从而丢弃输出流中目前已累积的所有输出
public synchronized void reset() {
count = 0;
}
// 使用指定的charsetName,通过解码字节将缓冲区内容转换为字符串并返回
public synchronized String toString(String charsetName) throws UnsupportedEncodingException {
return new String(buf, 0, count, charsetName);
}
public void close() throws IOException {
}
总结
- ByteArrayOutputStream中的数据被写入到一个byte数组里。byte数组会随着被写入其中的数据的增长而增长。
- 表示字节输出流的类必须提供至少一种可写入一个输出字节的方法。ByteArrayOutputStream提供了两种。加上继承自父类OuputStream类的write方法是3种
- ByteArrayOutputStream可以将缓冲区中的数据转化为byte数组或字符串并返回。
- ByteArrayOutputStream可以通过writeTo( OutputStream out)实现输出流之间数据的复制
- ByteArrayOutputStream 的close方法无效,无法关闭此输出流。
PipedInputStream,PipedOutputStream
PipedInputStream与PipedOutputStream分别为管道输入流和管道输出流。管道输入流通过连接到管道输出流实现了类似管道的功能,用于线程之间的通信。
通常,由某个线程向管道输出流中写入数据。根据管道的特性,这些数据会自动发送到与管道输出流对应的管道输入流中。这时其他线程就可以从管道输入流中读取数据,这样就实现了线程之间的通信。
public class PipedInputStream extends InputStream
initPipe,connect,
/**
* 初始化PipedInputStream的缓冲区大小
*
* @param pipeSize 管道缓冲区容量
*/
private void initPipe(int pipeSize) {
if (pipeSize <= 0) {
throw new IllegalArgumentException("Pipe Size <= 0");
}
buffer = new byte[pipeSize];
}
/**
* 将PipedInputStream连接到指定的PipedOutputStream。
*
* 如果 PipedInputStream 已经被连接到了其他 PipedOutputStream,
* 或者PipedOutputStream 已经被连接到其他PipedInputStream
* 抛出IOException。
*/
public void connect(PipedOutputStream src) throws IOException {
src.connect(this);
}
receive,awaitSpace,checkStateForReceive
// 接收一个数据字节,将其插入到缓冲区。如果没有可用的输入,方法会阻塞
protected synchronized void receive(int b) throws IOException {
// 接受前的状态检查
checkStateForReceive();
// 设置负责向管道缓冲区输入数据的线程是当前线程
writeSide = Thread.currentThread();
// 如果缓冲区被塞满的时候
if (in == out)
// 缓冲区被写入线程塞满的时候,唤醒读取线程,并阻塞当前写入线程
awaitSpace();
// 初次接受前初始化
if (in < 0) {
in = 0;
out = 0;
}
buffer[in++] = (byte) (b & 0xFF);
// 从头复入
if (in >= buffer.length) {
in = 0;
}
}
// 检查PipedInputStream是否可以接收数据
private void checkStateForReceive() throws IOException {
if (!connected) {
throw new IOException("Pipe not connected");
} else if (closedByWriter || closedByReader) {// 输入输出流都不能被关闭
throw new IOException("Pipe closed");
} else if (readSide != null && !readSide.isAlive()) {// switSpace 需要用到读线程,读线程不能为空且不alive
throw new IOException("Read end dead");
}
}
// 等待可用缓冲区
private void awaitSpace() throws IOException {
while (in == out) {
checkStateForReceive();
/* full: kick any waiting readers */
notifyAll();
try {
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
}
// 接受指定字节数组的数据
synchronized void receive(byte b[], int off, int len) throws IOException {
// 检查接受状态
checkStateForReceive();
// 身份
writeSide = Thread.currentThread();
// 写入总量
int bytesToTransfer = len;
while (bytesToTransfer > 0) {
// 判断缓冲区满没满
if (in == out)
awaitSpace();
// 下次插入量
int nextTransferAmount = 0;
if (out < in) {
nextTransferAmount = buffer.length - in;
} else if (in < out) {
// 初次写入
if (in == -1) {
in = out = 0;
nextTransferAmount = buffer.length - in;
} else {
// 再次写入
nextTransferAmount = out - in;
}
}
// 如果 可插入的量 > 要插入的量,那么一次插入结束,此次插入的量就是写入线程要插入数据的总量,
// 否则off记录偏移量,in记录存入位,bytesToTransfer记录剩余插入量,while循环批次执行。
if (nextTransferAmount > bytesToTransfer)
nextTransferAmount = bytesToTransfer;
assert (nextTransferAmount > 0);
// 写入
System.arraycopy(b, off, buffer, in, nextTransferAmount);
bytesToTransfer -= nextTransferAmount;
off += nextTransferAmount;
in += nextTransferAmount;
if (in >= buffer.length) {
in = 0;
}
}
}
receivedLast
// 管道输出流关闭时(PipedOutputStream.close()中会调用此方法),通知其已经关闭。
synchronized void receivedLast() {
// 状态设置
closedByWriter = true;
notifyAll();
}
read
public synchronized int read() throws IOException {
// 状态检测
if (!connected) {// 输入输出流是否连接上
throw new IOException("Pipe not connected");
} else if (closedByReader) {// 管道输入流被关闭
throw new IOException("Pipe closed");
} else if (writeSide != null && !writeSide.isAlive() // 写线程存在但不alive,管道输出流没关 且现在管道中没数据
&& !closedByWriter && (in < 0)) {
throw new IOException("Write end dead");
}
// 状态设置,当前线程为读取线程
readSide = Thread.currentThread();
// 尝试次数
int trials = 2;
// 管道中没数据
while (in < 0) {
// 如果管道输出流关闭,且此时in<0 管道缓冲区没机会再写入内容了,read 返回 return -1
if (closedByWriter) {
/* closed by writer, return EOF */
return -1;
}
// 如果写入数据的线程不为null且不活跃且trials<=0,说明管道损坏,抛出异常
if ((writeSide != null) && (!writeSide.isAlive()) && (--trials < 0)) {
throw new IOException("Pipe broken");
}
/* might be a writer waiting */ // 唤醒写入
notifyAll();
try {
wait(1000);
} catch (InterruptedException ex) {
throw new java.io.InterruptedIOException();
}
}
int ret = buffer[out++] & 0xFF;
// 读完一轮,复位
if (out >= buffer.length) {
out = 0;
}
// 管道缓冲区为空,读完
if (in == out) {
/* now empty */
in = -1;
}
return ret;
}
public synchronized int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
/* possibly wait on the first character */
// 尝试读一个字节,看缓冲区情况
int c = read();
if (c < 0) {
return -1;
}
b[off] = (byte) c;
// readLength
int rlen = 1;
while ((in >= 0) && (len > 1)) {
int available;
if (in > out) {
// 可读数
available = Math.min((buffer.length - out), (in - out));
} else {
available = buffer.length - out;
}
// A byte is read beforehand outside the loop
// 可读数 > 要读数
if (available > (len - 1)) {
available = len - 1;
}
System.arraycopy(buffer, out, b, off + rlen, available);
// 一次读取影响到的变量统一变更
out += available;
rlen += available;
len -= available;
// 继续从头读
if (out >= buffer.length) {
out = 0;
}
// 缓冲区读完
if (in == out) {
/* now empty */
in = -1;
}
}
return rlen;
}
available,close
public synchronized int available() throws IOException {
if (in < 0)
return 0;
else if (in == out)// 读完被置为-1 所以这里肯定是满了
return buffer.length;
else if (in > out)
return in - out;
else
return in + buffer.length - out;
}
/**
* 关闭此管道输入流,并释放与该流相关的所有系统资源。
*/
public void close() throws IOException {
// 状态设置
closedByReader = true;
synchronized (this) {
in = -1;//
}
}
PipedOutputStream
public class PipedOutputStream extends OutputStream {
private PipedInputStream sink; // 与PipedOutputStream相连接的管道输入流
// 创建连接到指定输入流的管道输出流
public PipedOutputStream(PipedInputStream snk) throws IOException {
connect(snk);
}
// 创建没有连接到输入流的管道输出流。
// 在使用前,它必须连接到管道输入流。
public PipedOutputStream() {
}
}
connect,write
public synchronized void connect(PipedInputStream snk) throws IOException {
if (snk == null) {
throw new NullPointerException();
} else if (sink != null || snk.connected) {// 此管道输出流已经与某管道输入流连接 或 该管道输入流已经被连接
throw new IOException("Already connected");
}
sink = snk;
snk.in = -1;
snk.out = 0;
snk.connected = true;
}
// 将指定数据字节写入管道输出流
public void write(int b) throws IOException {
if (sink == null) {
throw new IOException("Pipe not connected");
}
sink.receive(b);
}
// 将指定字节数组的数组写入到管道缓冲区中
public void write(byte b[], int off, int len) throws IOException {
// 数据校验
if (sink == null) {
throw new IOException("Pipe not connected");
} else if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
// 调用管道输入流的receive函数处理
sink.receive(b, off, len);
}
close,receivedLast,flush
public void close() throws IOException {
if (sink != null) {
sink.receivedLast();
}
}
// PipedInputStream类的receivedLast函数
synchronized void receivedLast() {
// 状态设置,负责缓冲区数据写入的流被关闭了。
closedByWriter = true;
notifyAll();
}
/**
* 刷新此输出流并强制写出所有缓冲的输出字节。
* 这将通知所有读取数据的线程,告知它们管道中的字符处于读取等待中。
*/
public synchronized void flush() throws IOException {
if (sink != null) {
synchronized (sink) {
sink.notifyAll();
}
}
}
总结
首先在看PipedInputStream和PipedOutputStream的时候,我刚开始没搞懂为什么PipedInputStream技能read又能recieve。后来看了PipedOutputStream的源码的时候才知道,原来PipedInputStream类中的recieve函数是给PipedOutputStream类中的write函数调用的,后来才串明白,这是一个“管道”的输入输出流,用一个容量默认为1024的byte数组来做管道的缓冲容量,最终的输入输出实现都落实到了PipedInputStream类中,这样状态都由一个类来控制才能做到,某个线程通过管道输出流向管道中写入数据,另一端管道输入流能立马从管道中取出对应存储到的数据。
- PipedInputStream与PipedOutputStream分别为管道输入流和管道输出流。管道输入流通过连接到管道输出流实现了类似管道的功能,用于线程之间的通信。
- 通常,由某个线程向管道输出流中写入数据。根据管道的特性,这些数据会自动发送到与管道输出流对应的管道输入流中。这时其他线程就可以从管道输入流中读取数据,这样就实现了线程之间的通信。
- 不建议对这两个流对象尝试使用单个线程,因为这样可能死锁线程。
- PipedOutputStream是数据的发送者;PipedInputStream是数据的接收者。
- PipedInputStream缓冲区大小默认为1024,写入数据时写入到这个缓冲区的,读取数据也是从这个缓冲区中读取的。
- PipedInputStream通过read方法读取数据。PipedOutputStream通过write方法写入数据,write方法其实是调用PipedInputStream中的receive方法来实现将数据写入缓冲区的的,因为缓冲区是在PipedInputStream中的。
- PipedOutputStream和PipedInputStream之间通过
connect()
方法连接。 - 使用后要关闭输入输出流
FilterInputStream,FilterOutputStream
FilterInputStream、FilterOutputStream是过滤器字节输入输出流。它们的主要用途在于封装其他的输入输出流,为它们提供一些额外的功能。 - 装饰者模式
package java.io;
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.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 {
return in.read(b, off, len);
}
public long skip(long n) throws IOException {
return in.skip(n);
}
public int available() throws IOException {
return in.available();
}
public void close() throws IOException {
in.close();
}
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
public synchronized void reset() throws IOException {
in.reset();
}
public boolean markSupported() {
return in.markSupported();
}
}
可以从源码看出,FilterInuptStream类本身并没有对构造时传入的InputStream抽象类的实例进行装饰,只是简单的重写了父类InputStream类的所有方法。
可以看出FilterInputStream在这里做的是装饰抽象类。而InputStream做的是抽象构建。
根据装饰模式的设计思想我们可以得知,虽然FilterInutStream类并不为具体构建提供装饰功能。但其子类在装饰模式中充当的是具体装饰类,可以进一步重写这些方法中的一些方法,来提供装饰功能。它的常用子类有BufferedInputStream
和DataInputStream
。比如,BufferedInputStream的作用就是为它装饰的输入流提供缓冲功能。
至此:
- InputStream:抽象构建
- ***InputStream:具体构建
- FilterInputStream:抽象装饰类
- BufferedInputStream:具体装饰类
filterOutputStream
public class FilterOutputStream extends OutputStream {
protected OutputStream out;
public FilterOutputStream(OutputStream out) {
this.out = out;
}
public void write(int b) throws IOException {
out.write(b);
}
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
if ((off | len | (b.length - (len + off)) | (off + len)) < 0)
throw new IndexOutOfBoundsException();
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
public void flush() throws IOException {
out.flush();
}
@SuppressWarnings("try")
public void close() throws IOException {
try (OutputStream ostream = out) {
flush();
}
}
}
需要注意的是看了FilterInputStream源码后不要想当然的认为FilterOutputStream也和它一样全篇方法调用装饰的OutputStream的子类的原方法。其也重写的父类的OutputStream全部方法,并有一些赋予了自己的处理逻辑。
总结
- FilterInputStream、FilterOutputStream是过滤器字节输入输出流。它们的主要用途在于封装其他的输入输出流,为它们提供一些额外的功能。
- FilterInputStream、FilterOutputStream并没有提供什么装饰功能。FilterInputStream、FilterOutputStream的子类可进一步重写这些方法中的一些方法,来提供装饰功能。
- FilterInputStream装饰功能的实现的关键在于类中有一个InputStream字段,依赖这个字段它才可以对InputStream的子类提供装饰功能。FilterOutputStream也是如此。
BufferedInputStream,BufferedOutputStream
public class BufferedInputStream extends FilterInputStream {
private static int DEFAULT_BUFFER_SIZE = 8192;// 1024 << 3; 缓冲区默认的默认大小
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;// 分派给arrays的最大容量
protected volatile byte buf[];// 存放数据的内部缓冲数组。,如果有必要,它可能被不同大小的数组替代
// 当前缓冲区的有效字节数。
// 注意,这里是指缓冲区的有效字节数,而不是输入流中的有效字节数。
protected int count;
// 当前缓冲区的位置索引
// 注意,这里是指缓冲区的位置索引,而不是输入流中的位置索引。
protected int pos;
// 当前缓冲区的标记位置
// markpos和reset()配合使用才有意义。操作步骤:
// (01) 通过mark() 函数,保存pos的值到markpos中。
// (02) 通过reset() 函数,会将pos的值重置为markpos。接着通过read()读取数据时,就会从mark()保存的位置开始读取。
// 可以理解为,mark位置之后的数据是保留数据,即有效数据。mark确立了有效数据和无效数据。
protected int markpos = -1;
// 相当于从输入流中一次读取数据的大小。当buffer.length小于这个值的时候就需要频繁的扩容,当大于这个值的时候就可以直接从输入流中读取数据。
protected int marklimit;
// 缓存数组的原子更新器。
// 该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现,
// 即,在多线程中操作BufferedInputStream对象时,buf和bufUpdater都具有原子性(不同的线程访问到的数据都是相同的)
private static final
AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
AtomicReferenceFieldUpdater.newUpdater
(BufferedInputStream.class, byte[].class, "buf");
}
BufferedInputStream的作用是为其它输入流提供缓冲功能。创建BufferedInputStream时,我们会通过它的构造函数指定某个输入流为参数。BufferedInputStream会将该输入流数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从输入流中读取下一部分的数据。(即对应read时发现缓冲区数据不够时调用fill函数,fill函数内部调用read函数读取输入流中的数据再填充buf缓冲区。)
为什么需要缓冲呢?原因很简单,效率问题!缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘或NandFlash等存储介质中;而我们知道,从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。
那干嘛不干脆一次性将全部数据都读取到缓冲中呢?第一,读取全部的数据所需要的时间可能会很长。第二,内存价格很贵,容量不像硬盘那么大。
该类最关键的函数及fill()
方法,其他方法都很好理解。该方法负责读取输入流的数据来填充buf缓冲区。具体解释可参考http://www.cnblogs.com/skywang12345/p/io_12.html
。
BufferedOutputStream
看过BufferedInputStream源码之后BufferedOutputStream就看起来很简单了,和BufferedInputStream一样,BufferedOutputStream通过字节数组来缓冲数据(1024*8)。BufferedOutputStream当缓冲区满或者用户调用flush()函数时,它就会将缓冲区的数据写入到输出流中。
总结
- BufferedInputStream是缓冲输入流,作用是为另一个输入流添加一些功能,比如缓冲输入功能以及支持mark和reset方法的能力。
- BufferedOutputStream是缓冲输出流,通过设置这种输出流,应用程序就可以将单个或字节数组缓冲的写入底层输出流中,而不必针对每次字节写入调用底层系统。
DataInputStream,DataOutputStream
DataInputStream
- DataInputStream为数据输入流,它允许应用程序以与机器无关方式从底层输入流中读取基本Java数据类型。
- DataOutputStream为数据输出流,它允许应用程序以适当方式将基本 Java数据类型写入输出流中。
public final String readUTF() throws IOException {
return readUTF(this);
}
public final static String readUTF(DataInput in) throws IOException {
int utflen = in.readUnsignedShort();
byte[] bytearr = null;
char[] chararr = null;
if (in instanceof DataInputStream) {
DataInputStream dis = (DataInputStream)in;
if (dis.bytearr.length < utflen){
dis.bytearr = new byte[utflen*2];
dis.chararr = new char[utflen*2];
}
chararr = dis.chararr;
bytearr = dis.bytearr;
} else {
bytearr = new byte[utflen];
chararr = new char[utflen];
}
int c, char2, char3;
int count = 0;
int chararr_count=0;
in.readFully(bytearr, 0, utflen);
// 由于UTF-8的单字节和ASCII相同,所以这里就将它们进行预处理,直接保存到“字符数组chararr”中。
// 对于其它的UTF-8数据,则在后面进行处理。
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
if (c > 127) break;
count++;
chararr[chararr_count++]=(char)c;
}
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
/* 0xxxxxxx*/
count++;
chararr[chararr_count++]=(char)c;
break;
case 12: case 13:
/* 110x xxxx 10xx xxxx*/
count += 2;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count-1];
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException(
"malformed input around byte " + count);
chararr[chararr_count++]=(char)(((c & 0x1F) << 6) |
(char2 & 0x3F));
break;
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx */
count += 3;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = (int) bytearr[count-2];
char3 = (int) bytearr[count-1];
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
throw new UTFDataFormatException(
"malformed input around byte " + (count-1));
chararr[chararr_count++]=(char)(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
default:
/* 10xx xxxx, 1111 xxxx */
throw new UTFDataFormatException(
"malformed input around byte " + count);
}
}
// The number of chars produced may be less than utflen
return new String(chararr, 0, chararr_count);
}
readUTF的执行流程相当于把UTF-8编码的输入流中的字节数据先读到了bytearr数组中,然后根据UTF-8编码的特殊性,判断数据是几个字节,根据情况又往chararr数组中转。保证了每个字符转化的正确率,最后所有的字符都正确的转化到了chararr数组中,然后返回string值。
readUnsignedShort,readBoolean,readUnsignedByte
// UTF-8输入流的前2个字节是数据的长度
public final int readUnsignedShort() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
if ((ch1 | ch2) < 0)
throw new EOFException();
return (ch1 << 8) + (ch2 << 0);
}
// 此方法适用于读取用接口DataOutput的 writeBoolean方法写入的字节
public final boolean readBoolean() throws IOException {
//从输入流中读取一个字节
int ch = in.read();
//如果达到输入流末尾,抛出异常
if (ch < 0)
throw new EOFException();
//如果读取的字节不是零,则返回true;如果是零,则返回false
return (ch != 0);
}
// 读取一个无符号为byte输入字节,将它数值位左侧补零转变为int类型(覆盖符号位),并返回结果,所以结果的范围是0到255
public final int readUnsignedByte() throws IOException {
int ch = in.read();
if (ch < 0)
throw new EOFException();
return ch;
}
最后我们落实到该类的特点,它允许应用程序以与机器无关方式从底层输入流中读取基本Java数据类型。
其实就是通过构造时传入的InputStream对象来根据要读取的数据类型来读取对应的字节数,比如readByte就调用一次in.read。然后把读取出来的数据强转成(byte)。readChar的话就调用两次,然后因为两个字节连续起来表示一个字符,那么就将先读出来的字节适当的移位并将两个字节+起来。这样就相当于一次性读出来两个字节,然后对该数据强转即可得到最真实的数据。
需要注意的是,因为我们从输入流中读出的内容返回的是int类型的,默认除了数据位,把我们原数据的符号位覆盖掉了。然后我们强转就可以就可以恢复(暂时先那么理解有符号位和无符号位的计算方式,方便记忆,虽然肯定不是这样,以后再研究。)
DataOutputStream
incCount,flush,size
// 到目前为止写入到输出流中的字节数 最大值为Integer.MAX_VALUE
protected int written;
// 增加wirtten的值。最大值为Integer.MAX_VALUE
private void incCount(int value) {
int temp = written + value;
//int允许的最大值为Integer.MAX_VALUE,即2147483647,2147483647+1即为负数
if (temp < 0) {
temp = Integer.MAX_VALUE;
}
written = temp;
}
// 清空此数据输出流。这迫使所有缓冲的输出字节被写出到流中。
public void flush() throws IOException {
out.flush();
}
// 返回written的当前值,即到目前为止写入此数据输出流的字节数。最大值为Integer.MAX_VALUE。
public final int size() {
return written;
}
writeShort,writeChar,writeFloat
public final void writeChar(int v) throws IOException {
out.write((v >>> 8) & 0xFF);
out.write((v >>> 0) & 0xFF);
incCount(2);
}
public final void writeChars(String s) throws IOException {
int len = s.length();
for (int i = 0 ; i < len ; i++) {
int v = s.charAt(i);
out.write((v >>> 8) & 0xFF);
out.write((v >>> 0) & 0xFF);
}
incCount(len * 2);
}
// 使用Float类中的floatToIntBits方法将float参数转换为一个int值
public final void writeFloat(float v) throws IOException {
writeInt(Float.floatToIntBits(v));
}
可以看到DataOutputStream和DataInputStream的处理方式是一样的,要输入到输出流的数据有可能是1个字节或多个字节的,所以对应不同的数据定义了不同的函数,然后针对这些数值一个字节一个字节的进行输入就好。
总结
- DataInputStream提供了一系列从二进制流中读取字节,并根据所有Java基本类型数据进行重构的readXXXX方法。同时还提供根据UTF-8编码格式的数据写入输入流的方式,即readUTF方法。
- DataOutputStream提供了一系列将数据从任意Java基本类型转换为一系列字节,并将这些字节写入二进制流的writeXXXX方法。同时还提供了一个将String转换成UTF-8修改版格式并写入所得到的系列字节的工具,即writeUTF方法。
PrintStream
由于学习的时候,我在学习字节流的时候跳过了PrintStream,先看的PrintWriter所以看过PrintWriter后再来看PrintStream就感觉很简单了,所以简单记录下。
- PrintStream 是打印输出流,它继承于FilterOutputStream。
- PrintStream 是用来装饰其它输出流。它能为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。为底层输出流提供了缓存池(BufferedWriter)。
- 与其他输出流不同,PrintStream 永远不会抛出 IOException;它产生的IOException会被自身的函数所捕获并设置错误标记, 用户可以通过 checkError() 返回错误标记,从而查看PrintStream内部是否产生了IOException。
- 另外,PrintStream 提供了自动flush 和 字符集设置功能。所谓自动flush,就是往PrintStream写入的数据会立刻调用flush()函数。
public class PrintStream extends FilterOutputStream
implements Appendable, Closeable
{
// 自动flush
// 所谓“自动flush”,就是每次执行print(), println(), write()函数,都会调用flush()函数;
// 而“不自动flush”,则需要我们手动调用flush()接口。
private final boolean autoFlush;
// PrintStream是否右产生异常。当PrintStream有异常产生时,会被本身捕获,并设置trouble为true
private boolean trouble = false;
// 用于格式化的对象
private Formatter formatter;
// BufferedWriter对象,用于实现“PrintStream支持字符集”。
// 因为PrintStream是OutputStream的子类,所以它本身不支持字符串;
// 但是BufferedWriter支持字符集,因此可以通过OutputStreamWriter创建PrintStream对应的BufferedWriter对象,从而支持字符集。
private BufferedWriter textOut;
private OutputStreamWriter charOut;
private static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
}
==需要注意的是== :很明显,该类和PrintWriter还有个最大的区别。继承自FilterOutputStream,也就是它做的是装饰模式中的具体装饰类。至于Appendable,Closeable接口和PrintWriter则没区别,PrintWriter其父类Writer抽象也早实现了。
public class PrintStream extends FilterOutputStream
implements Appendable, Closeable
{
PrintStream和DataOutputStream异同点
相同点:都是继承与FileOutputStream,用于包装其它输出流。
不同点:
- PrintStream和DataOutputStream 都可以将数据格式化输出;但它们在“输出字符串”时的编码不同。
- PrintStream是输出时采用的是用户指定的编码(创建PrintStream时指定的),若没有指定,则采用系统默认的字符编码。而DataOutputStream则采用的是UTF-8。
- 它们的写入数据时的异常处理机制不同。
- DataOutputStream在通过write()向“输出流”中写入数据时,若产生IOException,会抛出。
- 而PrintStream在通过write()向“输出流”中写入数据时,若产生IOException,则会在write()中进行捕获处理;并设置trouble标记(用于表示产生了异常)为true。用户可以通过checkError()返回trouble值,从而检查输出流中是否产生了异常。
- 构造函数不同
- DataOutputStream的构造函数只有一个:DataOutputStream(OutputStream out)。即它只支持以输出流out作为“DataOutputStream的输出流”。
- 而PrintStream的构造函数有许多:和DataOutputStream一样,支持以输出流out作为“PrintStream输出流”的构造函数;还支持以“File对象”或者“String类型的文件名对象”的构造函数。
- 而且,在PrintStream的构造函数中,能“指定字符集”和“是否支持自动flush()操作”。
- 目的不同
- DataOutputStream的作用是装饰其它的输出流,它和DataInputStream配合使用:允许应用程序以与机器无关的方式从底层输入流中读写java数据类型。
- 而PrintStream的作用虽然也是装饰其他输出流,但是它的目的不是以与机器无关的方式从底层读写java数据类型;而是为其它输出流提供打印各种数据值表示形式,使其它输出流能方便的通过print(),println()或printf()等输出各种格式的数据。