字节流
基本输入输出流
抽象类 InputStream
和 OutputStream
为字节流输入和输出的基类。
InputStream
InputStream
定义了读取单个字节的方法。
// InputStream.java
public abstract int read() throws IOException;
在读写单字节的基础上,还封装了读取多个字节的方法。
// InputStream.java
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public 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;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
读取多个字节的方法的原理还是一个一个字节的读,但是在实现类中,往往因为输入流的类型的不同而覆写这个方法。
OutputStream
OuputStream
,定义了写入单个字节的方法
public abstract void write(int b) throws IOException;
同时也定义了写入多个字节的方法
public void write(byte b[]) throws IOException {
write(b, 0, b.length);
}
public void write(byte b[], int off, int len) throws IOException {
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;
}
for (int i = 0 ; i < len ; i++) {
write(b[off + i]);
}
}
多字节写入的原理是一个一个字节的写,但是在实现类中,往往因为输出流的类型的不同而覆写这个方法。
字节数组输入输出流
ByteArrayInputStream
ByteArrayInputStream
代表输入源为字节数组的输入流,因此接受一个字节数组作为输入,并用一个字节缓存数组保存输入源的数据。
protected byte buf[];
public ByteArrayInputStream(byte buf[]) {
this.buf = buf;
this.pos = 0;
this.count = buf.length;
}
public ByteArrayInputStream(byte buf[], int offset, int length) {
this.buf = buf;
this.pos = offset;
this.count = Math.min(offset + length, buf.length);
this.mark = offset;
}
当读取一个字节的时候,就会从缓存的字节数组中取出字节
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
从
synchronized
关键字可以看出,ByteArrayInputStream
读取字节是线程安全的,与之对应的ByteArrayOuputStream
写字节也是线程安全的。
ByteArrayOuputStream
ByteArrayOutputStream
代表输出源为字节数组的输出流,因此与 ByteArrayInputStream
一样,内部有持有一个字节数组。
protected byte buf[];
它把读取到的字节存放到这个字节数组中。
public synchronized int read() {
return (pos < count) ? (buf[pos++] & 0xff) : -1;
}
ByteArrayOuputStream
能使用 toByteArray()
返回字节数组,还可以通过 toString()
返回字符串
public synchronized byte toByteArray()[] {
return Arrays.copyOf(buf, count);
}
public synchronized String toString() {
return new String(buf, 0, count);
}
举例
这里我们以 ByteArrayInputStream
作为输出流,以 ByteArrayOuputStream
作为输入流举例。
String s = "Hello David!";
ByteArrayInputStream is = new ByteArrayInputStream(s.getBytes());
ByteArrayOutputStream os = new ByteArrayOutputStream();
int b;
while ((b = is.read()) != -1) {
os.write(b);
}
String s1 = os.toString();
System.out.println(s1);
这个例子并没有去手动关闭流,是因为 ByteArrayInputStream
和 ByteArrayOuputStream
是用内部数组实现的,关闭这两个流也没有太大意义,查看源码也可以发现 close()
为一个空方法。
文件输入输出流
FileInputStream
和 FileInputOuputStream
定义了文件的输入输出流。
FileInputStream
FileInputStream
代表输入源为文件作的输入流,它可以传入一个文件路径,也可以传入一个 File
对象。
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
public FileInputStream(File file) throws FileNotFoundException {}
如果更接近操作系统一点,也可以传入一个文件描述符
public FileInputStream(FileDescriptor fdObj){}
FileInputStream
读取字节的方式依赖于操作系统,这里就不深追究原理了,只要明白是从文件读取字节即可。
FileOuputStream
FileOutStream
代表输出源为文件的输出流,用来向文件写数据(字节)。
与 FileInputStream
的构造一样,FileOuputStream
可以用文件路径,File
对象,文件描述符来指定文件。
但是在构造函数中有一点需要注意,它还有一个参数,表明向文件写数据的方式是追加还是覆盖,默认的是覆盖。
public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}
public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}
举例
文件一般都是比较大的,如果每次读写一个字节,那太慢了,如果每次读写多个字节,那就可以提高效率,尤其在读写大文件的时候特别明显。
private void readByBuffer(String srcFile, String dstFile) {
long start = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
byte[] buff = new byte[1024];
int length;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(dstFile);
while ((length = fis.read(buff, 0, buff.length)) != -1) {
fos.write(buff, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("copy time: " + (System.currentTimeMillis() - start));
}
private void readByByte(String srcFile, String dstFile) {
long start = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
int b;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(dstFile);
while ((b = fis.read()) != -1) {
fos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("copy time: " + (System.currentTimeMillis() - start));
}
管道输入输出流
有时候一个线程需要不断的发送数据到另外一个线程,如果不采用管道,就需要对这个数据加锁进行控制访问,才能达到线程安全,基于此原理,就有了PipedInputStream
和 PipedOuputStream
。
PipedInputStream 和 PipedOutputStream
PipedInputStream
和 PipedOutputStream
的原理就是不同线程访问一个临界资源,这个临界资源就是字节数组
protected byte buffer[];
PipedOutputStream
负责写出数据
public void write(int b) throws IOException {
if (sink == null) {
throw new IOException("Pipe not connected");
}
sink.receive(b);
}
而实际是调用 PipedInputStream
的 receiver()
方法接收数据
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
中读取数据也是从这个字符数组中读取的
public synchronized int read() throws IOException {
// ...
int ret = buffer[out++] & 0xFF;
// ...
return ret;
}
byte buffer[]
就是线程之间的临界资源,因此 PipedInputStream
和 PipedOuputStream
就用了 synchronized
关键字保证了线程安全。
为了知道写到哪个管道,因此管道输入流必须和管道输出流相连,这个可以用构造函数来相连,也可以通过 connect()
方法来相连
// PipedInputStream.java
public PipedInputStream(PipedOutputStream src) throws IOException {
this(src, DEFAULT_PIPE_SIZE);
}
// PipedOuputStream.java
public PipedOutputStream(PipedInputStream snk) throws IOException {
connect(snk);
}
// PipedInputStream.java
public void connect(PipedOutputStream src) throws IOException {
src.connect(this);
}
// PipedOutputStream.java
public synchronized void connect(PipedInputStream snk) throws IOException {
}
可以注意到 PipedInputStream
的 connect()
方法实际是调用的 PipedOuputStream
的 connect()
方法,因此两个方法都是线程安全的。
在同一线程使用管道输入输出流容易造成死锁,因为如果不写入数据,而一直读数据的话,线程就会一直阻塞。
举例
首先写一个用 PipedOuputStream
发送数据的任务
public class Sender implements Runnable {
private PipedOutputStream pos = new PipedOutputStream();
public PipedOutputStream getPos() {
return pos;
}
@Override
public void run() {
String s = "Hello World!!!";
int length = s.getBytes().length;
if (length < 1024) {
try {
pos.write(s.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
然而写一个用 PipedInputStream
接收数据的任务
public class Receiver implements Runnable {
private PipedInputStream pis = new PipedInputStream();
public PipedInputStream getPis() {
return pis;
}
@Override
public void run() {
byte[] buffer = new byte[1024];
try {
int length = pis.read(buffer);
if (length > 0) {
String s = new String(buffer, 0, length);
System.out.println("read string: " + s);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
现在把这两个任务提交到线程池来执行。
public class Test {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
Sender sender = new Sender();
Receiver receiver = new Receiver();
try {
sender.getPos().connect(receiver.getPis());
executorService.execute(sender);
executorService.execute(receiver);
} catch (IOException e) {
e.printStackTrace();
}
}
}
这只是一个简单的例子,实际情况中到底如何写数据,定多少数据,如何接收数据,接收后如何转换数据,都要根据实际情况来写代码,这里只是抛砖引玉。
集合流
SequenceInputStream
SequenceInputStream
是把许多流放到一个集合中人,然后按照顺序依次从这个流中读取字节。 这个原理比较简单,看看 API 就能使用,略过~
序列化流和反序列化流
ObjectInputStream 和 ObjectOuputStream
查阅序列化相关的文章(https://blog.csdn.net/javazejian/article/details/52665164).
装饰流
FilterInputStream & FilterOutputStream
FilterInputStream
和 FilterOutputStream
分别继承自 InputStream
和 OutputStream
。但是它们底层的读写操作分别是用另外一个输入和输出流。
它们就相当于一个最简单的装饰类或者代理类。例如 FilterInputStream
的读取操作如下:
public class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
}
虽然 FilterInputStream
和 FilterOutputStream
并没有实质的用处,但是它们的子类在它的基础上添加了一些功能,子类功能如下
-
DataInputStream
和DataOutputStream
封装了可以读取基本类型数据的方法,例如readInt()
-
BufferedInputStream
和BufferedOuputStream
的带了缓冲的输入输出流。 -
PushBackInputStream
(FilterInputStream
子类) 可以回退一个字节,再重新读取 -
PrintStream
(FilterOuputStream
子类) 可以很方便的写出各种格式化的数据,例如println()
方法可以在写出数据后添加一个换行符。
DataInputStream & DataOuptStream
如前面所说,DataInputStream
和 DataOuputStream
封装了读取基本类型的数据的方法,它们的原理就是根据要读取数据的类型,读取相应的字节数,例如 readInt()
就是读取四个字节,然后转换成 int
类型。
public final int readInt() throws IOException {
readFully(readBuffer, 0, 4);
return Memory.peekInt(readBuffer, 0, ByteOrder.BIG_ENDIAN);
}
public final void readFully(byte b[], int off, int len) throws IOException {
if (len < 0)
throw new IndexOutOfBoundsException();
int n = 0;
while (n < len) {
int count = in.read(b, off + n, len - n);
if (count < 0)
throw new EOFException();
n += count;
}
}
举例
public class Test {
public static void main(String[] args) {
DataOutputStream dos = null;
DataInputStream dis = null;
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
dos = new DataOutputStream(bos);
dos.writeInt(110);
dos.writeUTF("Hello");
dis = new DataInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("read int value : " + dis.readInt());
System.out.println("read UTF string : " + dis.readUTF());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (dos != null) {
dos.close();
}
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
DataOuputStream
用一个 ByteArrayOuputStream
构造,writeInt()
等一些方法写出的数据,实际是写入到了 ByteArrayOuputStream
的内部字节数组中。
DataInputStream
用一个 ByteArrayInputStream
构造,readInt()
等一些方法其实是从 ByteArrayInputStream
中读取的。而 ByteArrayInputStream
的数据源其实就来源于 ByteArrayOuputStream
的内部字节数组。
缓存输入输出流
BufferedInputStream
BufferedInputStream
在创建的时候,会一起创建一个字节缓冲数组,这个缓冲字节数组是可以指定大小的。
private static final int DEFAULT_BUFFER_SIZE = 8192;
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
可以看到,默认的缓冲字节数组是 8192
个字节,也就是 8k
。
当读取数据的时候,如果缓存字节数组中没有数据可读,就会先进行填充
private void fill() throws IOException {
// ...
// 从底层输入流中读取字节到 buffer
int n = in.read(buffer, pos, buffer.length - pos);
// ...
}
然而,再从缓存数组中读取
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return buffer[pos++] & 0xff;
}
从这里就可以看出,BufferedInputStream
实际就是调用底层输入流的 read(byte[] buff, int off, int length)
方法,那为何还要封装成一个类呢? 理由如下:
- 它提醒我们开发者,带缓冲的输入流才是正常的输入流。而一般我们使用的输入流并不是带缓冲的。
- 它的所有方法都是线程安全的。从上面的
read()
方法可以看出。 - 它支持
mark()
和reset()
方法来读取指定位置的缓存字节。
BufferedOuputStream
BufferedOuputStream
在创建的时候,也会创建一个缓冲字节数组,与 BufferedInputStream
一样,是在构造函数中指定。
那看看它是如何写一个字节的
public synchronized void write(int b) throws IOException {
// 如果写入的字节数超过缓存长度,就刷新缓存
if (count >= buf.length) {
flushBuffer();
}
// 把字节放入到缓存
buf[count++] = (byte)b;
}
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
从实现我们惊讶的看到,如果刚开始字节,是会直接放到缓存数组中,只有当缓存数组放不下的时候,才会刷新缓存来真正的写字节。那么我们可以手动刷新缓存
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
或者关闭 BufferedOuputStream
来达到刷新的目的
public void close() throws IOException {
try (OutputStream ostream = out) {
flush();
}
}
例子
public class Test {
public static void main(String[] args) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream("Kalimba.mp3"));
bos = new BufferedOutputStream(new FileOutputStream("KalimpaCopy.mp3"));
byte[] buff = new byte[4 * 1024];
int length;
while ((length = bis.read(buff, 0, buff.length)) != -1) {
bos.write(buff, 0, length);
// 可以手动刷新缓存
// bos.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
// 关闭输出流,可以刷新缓存
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
打印流
PrintStream
PrintStream
可以很方便以各种格式写入数据,例如可以写完后换行的 println()
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
public void print(int i) {
write(String.valueOf(i));
}
PrintStream
还有一个特色,可以在构造参数中设置自动刷新
public PrintStream(OutputStream out) {
this(out, false);
}
public PrintStream(OutputStream out, boolean autoFlush) {
this(autoFlush, requireNonNull(out, "Null output stream"));
}
字符流
基本字符输入输出流
Reader & Writer
Reader
和 Writer
是字符流的基类,它们定义了读写单个字符的方法
// Reader.java
public int read() throws IOException {}
// Writer.java
public void write(int c) throws IOException {}
字符
char
以int
形式表示
当然也定义了读写多个字符的方法
// Reader.java
public int read(char cbuf[]) throws IOException {}
abstract public int read(char cbuf[], int off, int len) throws IOException;
// Writer.java
public void write(char cbuf[]) throws IOException {}
abstract public void write(char cbuf[], int off, int len) throws IOException;
几乎所有的字符输入输出流都是线程安全的,因为它们都有一个用于同步的对象
protected Object lock;
protected Reader() {
this.lock = this;
}
protected Reader(Object lock) {
if (lock == null) {
throw new NullPointerException();
}
this.lock = lock;
}
字符数组输入输出流
CharArrayReader & CharArrayWriter
类似于 ByteArrayInputStream
和 ByteArrayOuputStream
,CharArrayReader
和 CharArrayWriter
内部包含一个字符数组
/** The character buffer. */
protected char buf[];
读的时候,就从这个字符数组中读
// CharArrayReader.java
protected char buf[];
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
if (pos >= count)
return -1;
else
return buf[pos++];
}
}
写的时候呢,就往这个字节数组中写
// CharArrayWriter.java
private char[] writeBuffer;
public void write(int c) {
synchronized (lock) {
int newcount = count + 1;
if (newcount > buf.length) {
// 如果数组长度不够,就扩充数组
buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount));
}
buf[count] = (char)c;
count = newcount;
}
}
从上面代码中的
synchronized
关键字可知,CharArrayReader
和CharArrayWriter
的方法都是线程安全的。
例子
public class JavaIO {
public static void main(String[] args) {
String s = "hello中国";
CharArrayReader reader = new CharArrayReader(s.toCharArray());
CharArrayWriter writer = new CharArrayWriter();
int c;
try {
while ((c = reader.read()) != -1) {
writer.write(c);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
reader.close();
writer.close();
}
System.out.println(writer.toString());
}
}
字符串输入输出流
StringReader
表示输入源是字符串,StringReader
表示输出源是字符串。
StingReader
既然 StringReader
代表的输入源是字符串,那么构造函数就需要一个字符串作为参数
private String str;
public StringReader(String s) {
this.str = s;
this.length = s.length();
}
既然内部用一个 String
实现的,那么读取字符也是从这个 String
对象中读的
public int read() throws IOException {
synchronized (lock) {
// ...
return str.charAt(next++);
}
}
public int read(char cbuf[], int off, int len) throws IOException {
synchronized (lock) {
// ...
int n = Math.min(length - next, len);
str.getChars(next, next + n, cbuf, off);
next += n;
return n;
}
}
从实现看,read()
方法是线程安全的。
StringWriter
StringWriter
代表输出源是字符串,内部却是用 StringBuffer
实现的
private StringBuffer buf;
public StringWriter() {
buf = new StringBuffer();
lock = buf;
}
public StringWriter(int initialSize) {
if (initialSize < 0) {
throw new IllegalArgumentException("Negative buffer size");
}
buf = new StringBuffer(initialSize);
lock = buf;
}
提起 StringBuffer
自然想到的就是线程安全,所以它的写操作不会像 StingReader
那样需要加锁
public void write(int c) {
buf.append((char) c);
}
那有人可能会问,为何不用 String
?因为 String
的不可变性,它的操作每次都会创建新的 String
,而 StringBuffer
比起 StringBuilder
,既是线程安全,比起 String
,又不需要每次操作都去创建新的 String
。
举例
public class JavaIO {
public static void main(String[] args) {
String s = "hello中国";
StringReader reader = new StringReader(s);
StringWriter writer = new StringWriter();
int c;
try {
while ((c = reader.read()) != -1) {
writer.write(c);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
reader.close();
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(writer.toString());
}
}
文件输入输出流
FileReader
和 FileWriter
分别代表输入和输出源为文件。
InputStreamReader & InputStreamWriter
文件作为输入或输出流,只能以字节流的形式存在,如果要从文件中读取字符或者向文件中写入字符,就需要字符流和字节流相互转换,这就是FileReader
和 FileWriter
的父类 InputStreamReader
和 OuputStreamWriter
所做的。
InputStreamReader
转换代码如下
private final StreamDecoder sd;
public InputStreamReader(InputStream in) {
super(in);
try {
sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
}
然后读取字符就是从这个 StreamDecoder
中读取的
public int read() throws IOException {
return sd.read();
}
FileReader & FileWriter
FileReader
和 FileWriter
分别继承自 InputStreamReader
和 InputStreamWriter
,但是它们只是提供一个文件字节输入和输出流,其它什么也没做,转换啊,读取和写入字符这些功能都是由父类完成的。
例子
public class JavaIO {
public static void main(String[] args) {
FileReader reader = null;
FileWriter writer = null;
char[] buff = new char[256];
int length;
try {
reader = new FileReader("./src/test/JavaIO.java");
writer = new FileWriter("JavaIOCopy.java");
while ((length = reader.read(buff, 0, buff.length)) != -1) {
writer.write(buff, 0, length);
}
// 立即刷新写入到文件中,否则只能在 writer.close() 刷新缓存
writer.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
if (writer != null) {
writer.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
管道输入输出流
PipedReader
和 PipedWriter
代表字符的管道输入和输出流。
PipedReader & PipedWriter
它们的原理与 PipedInputStream
和 PipedOuputStream
一样。只不过它们之间的临界资源是一个字符数组
char buffer[];
同样,PipedReader
和 PipedWriter
是用于不同线程之间通信,如果用于同一线程中通信,可能会造成死锁。
例子
// Sender.java
public class Sender implements Runnable {
private PipedWriter writer = new PipedWriter();
@Override
public void run() {
System.out.println("Sender thread: " + Thread.currentThread());
try {
writer.write("Hello 中国");
} catch (IOException e) {
e.printStackTrace();
}
}
public PipedWriter getWriter(){
return writer;
}
}
// Receiver.java
public class Receiver implements Runnable {
private PipedReader reader = new PipedReader();
@Override
public void run() {
System.out.println("Receiver thread: " + Thread.currentThread());
char[] cs = new char[200];
try {
reader.read(cs);
System.out.println("Read result: " + new String(cs));
} catch (IOException e) {
e.printStackTrace();
}
}
public PipedReader getReader(){
return reader;
}
}
// JavaIO.java
public class JavaIO {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
Sender sender = new Sender();
Receiver receiver = new Receiver();
try {
sender.getWriter().connect(receiver.getReader());
service.execute(sender);
service.execute(receiver);
} catch (IOException e) {
e.printStackTrace();
}
}
}
缓存输入输出流
BufferedReader
BufferedReader
包装了一个 Reader
,并在内部创建了一个字符缓冲数组,默认的缓存数组大小为 8*1024
.
private static int defaultCharBufferSize = 8192;
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
读字符的时候就是从这个缓存字符数组中读的
public int read() throws IOException {
synchronized (lock) {
ensureOpen();
for (;;) {
// 如果数组字符已经读完
if (nextChar >= nChars) {
// 填充缓存数组
fill();
// 如果读到文件末尾就返回 -1
if (nextChar >= nChars)
return -1;
}
if (skipLF) {
skipLF = false;
if (cb[nextChar] == '\n') {
nextChar++;
continue;
}
}
// 从数组中返回字符
return cb[nextChar++];
}
}
}
从实现可以看到是从缓存数组中读取的,如果缓存数组已经读完了就要先填充再读取,这样就不用每次都从输入源中读一个字符,提高了效率。
BufferedReader
还有一个特色,就是可以读取一行字符串
public String readLine() throws IOException {
return readLine(false);
}
String readLine(boolean ignoreLF) throws IOException {
StringBuffer s = null;
int startChar;
synchronized (lock) {
ensureOpen();
boolean omitLF = ignoreLF || skipLF;
bufferLoop:
for (;;) {
// 如果没有多的字节可读就先填充
if (nextChar >= nChars)
fill();
// 填充后,还是没有过多字节可读,代表读到文件结尾了
if (nextChar >= nChars) { /* EOF */
// 如果读到了字符不为空,就直接返回
if (s != null && s.length() > 0)
return s.toString();
else
// 否则返回 null
return null;
}
boolean eol = false;
char c = 0;
int i;
/* Skip a leftover '\n', if necessary */
if (omitLF && (cb[nextChar] == '\n'))
nextChar++;
skipLF = false;
omitLF = false;
charLoop:
// 查找缓存数组中是否有换行或回车符
for (i = nextChar; i < nChars; i++) {
c = cb[i];
if ((c == '\n') || (c == '\r')) {
eol = true;
break charLoop;
}
}
startChar = nextChar;
nextChar = i;
// 如果找到换行或回车符,就把读到的所有字符返回
if (eol) {
String str;
if (s == null) {
str = new String(cb, startChar, i - startChar);
} else {
s.append(cb, startChar, i - startChar);
str = s.toString();
}
nextChar++;
if (c == '\r') {
skipLF = true;
}
return str;
}
// 如果没有找到换行或回车符,就存储读到的字符,并继续往下读
if (s == null)
s = new StringBuffer(defaultExpectedLineLength);
s.append(cb, startChar, i - startChar);
}
}
}
读取一行字符的原理就是先填充再读取,直到遇到换行或者回车字符,就把读到的所有字符返回。但是这一行的签字,显然并不包括换行或者回车换行。
BufferedWriter
BufferedReader
和 BufferedWriter
一样,带了 8k
的缓存数组,当然在构造函数中也可以指定大小。
写字符当然也是往缓存数组中写,但是有点小的区别
public void write(int c) throws IOException {
synchronized (lock) {
ensureOpen();
// 超过缓存长度,先刷新缓存
if (nextChar >= nChars)
flushBuffer();
// 写入到缓存数组中
cb[nextChar++] = (char) c;
}
}
void flushBuffer() throws IOException {
synchronized (lock) {
ensureOpen();
if (nextChar == 0)
return;
out.write(cb, 0, nextChar);
nextChar = 0;
}
}
可以看出,如果缓存数组没有满,就直接往缓存数组中写字符,否则是先刷新缓存,再向缓存数组中写字符。因此,如果我们调用了 BufferedWriter
的 writer()
方法后,如果你想快速看到效果,可以手动调用 flush()
来刷新缓存,也可以在在最后关闭输出流的时候刷新缓存
public void flush() throws IOException {
synchronized (lock) {
flushBuffer();
out.flush();
}
}
@SuppressWarnings("try")
public void close() throws IOException {
synchronized (lock) {
if (out == null) {
return;
}
try (Writer w = out) {
flushBuffer();
} finally {
out = null;
cb = null;
}
}
}
例子
我们经常会去复制一个较大的文件,这个时候使用缓存就显然尤为重要
public class FileUtils {
/**
* 每次从 BufferedReader 读入字符到缓存数组中,再向 BufferedWriter 写入
*/
public static void copyFileByCharBuffer(String src, String dst) {
BufferedReader reader = null;
BufferedWriter writer = null;
// 缓存的大小影响效率
char[] buff = new char[20 * 1024];
int length;
try {
reader = new BufferedReader(new FileReader(src));
writer = new BufferedWriter(new FileWriter(dst));
while ((length = reader.read(buff, 0, buff.length)) != -1) {
writer.write(buff, 0, length);
}
// 刷新缓存,让数据写入文件
writer.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 每次从 BufferedReader 读取一行字符串,再写入到 BufferedWrtier 中
*/
public static void copyFileByLine(String src, String dst) {
BufferedReader reader = null;
BufferedWriter writer = null;
String line;
try {
reader = new BufferedReader(new FileReader(src));
writer = new BufferedWriter(new FileWriter(dst));
while ((line = reader.readLine()) != null) {
// 写入一行数据,但不包括换行符
writer.write(line);
// 手动添加换行符
writer.newLine();
}
writer.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 每次从 BufferedInputStream 中读取字节到缓存数组中,再写入到 BufferedOuputStream
*/
public static void copyFileByByteBuffer(String src, String dst) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
// 缓存的大小影响读写的效率
byte[] buff = new byte[1024];
int length;
try {
bis = new BufferedInputStream(new FileInputStream(src));
bos = new BufferedOutputStream(new FileOutputStream(dst));
while ((length = bis.read(buff, 0, buff.length)) != -1) {
bos.write(buff, 0, length);
}
bos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这三个方法在复制文件的时候,例如一个Java
文件,效率都差不多。 但是在复制一个音频文件的时候,通常都是几十兆,以字节缓存的复制方式的效率明显是要高一些,而且复制出来的文件大小也精确些,这个在实际中需要注意。
<< Java编程思想 >> 一书中提到,“尽量尝试使用 Reader 和 Writer”,然而一旦发现有问题,应该使用字节流的类库。
带有行号的输入流
LineNumberReader
LineNumberReader
继承自 BufferedReader
,它拥有获取行号的能力,例如 readLine()
方法就可以轻松获取行号
public String readLine() throws IOException {
synchronized (lock) {
String l = super.readLine(skipLF);
skipLF = false;
if (l != null)
lineNumber++;
return l;
}
}
只要读取到一行字符串不为 null
,行号就加一。
然而它的设置行号的方法,并没有太大的意思
public void setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
}
setLineNumber()
并不影响读取的位置,它只是改变这个值(真不知道这个方法有个吊用)。
打印字符输出流
PrintWriter
PrintWriter
于 PrintStream
有着相同的方法,都是为了格式化写入各种数据。
它可以接受 Writer
的字符输出流作为构造参数
public PrintWriter (Writer out) {
this(out, false);
}
public PrintWriter(Writer out,
boolean autoFlush) {
super(out);
this.out = out;
this.autoFlush = autoFlush;
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}
也可以接受一个 OuputStream
的字节流作为参数
public PrintWriter(OutputStream out) {
this(out, false);
}
public PrintWriter(OutputStream out, boolean autoFlush) {
this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
// save print stream for error propagation
if (out instanceof java.io.PrintStream) {
psOut = (PrintStream) out;
}
}
其实代码中最后也把字符流转换为了字节流,这个字节流还用 BufferedWriter
包装了一层,为了效率。
如果面对输出源是文件,它还提供了一个更过分的方法,直接接受文件名作为参数
public PrintWriter(String fileName) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName))),
false);
}
public PrintWriter(String fileName, String csn)
throws FileNotFoundException, UnsupportedEncodingException
{
this(toCharset(csn), new File(fileName));
}
public PrintWriter(File file) throws FileNotFoundException {
this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))),
false);
}
public PrintWriter(File file, String csn)
throws FileNotFoundException, UnsupportedEncodingException
{
this(toCharset(csn), file);
}
与 PrintStream
相比较,PrintWriter
在构造函数中传入的自动刷新参数,只有在使用 println()
,printf()
,format()
方法时候才会调用
public void println(String x) {
synchronized (lock) {
print(x);
println();
}
}
public void print(String s) {
if (s == null) {
s = "null";
}
write(s);
}
public void println() {
newLine();
}
private void newLine() {
try {
synchronized (lock) {
ensureOpen();
out.write(lineSeparator);
if (autoFlush)
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
参考文档
编码
http://www.cnblogs.com/gdayq/p/5817367.html
Charset
https://blog.csdn.net/nicewuranran/article/details/52123516
https://www.cnblogs.com/lngrvr/p/java_AutoCharsetReader.html
网友评论