Java io中通过InputStream字节输入流用来将数据读取到内存中,同时也提供了字节输出流OutputStream用来从内存中读取数据。
和InputStream结构类似,我们也通过以下几个类来了解OutputStream。
-
OutPutStream
OutputStream抽象类中主要提供了三个方法:
输出单个字节
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]);
}
}
刷新缓冲流:
public void flush() throws IOException {
}
具体的实现 我们看OutputStream的具体实现类
-
FileOutputStream
将数据通过字节的方式输出到文件中:
先看下构造方法:
public FileOutputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null, false);
}
public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}
public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}
public FileOutputStream(File file, boolean append)
throws FileNotFoundException
{
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkWrite(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
this.fd = new FileDescriptor();
this.append = append;
this.path = name;
fd.incrementAndGetUseCount();
open(name, append);
}
可以看到虽然FileOutputStream提供了多个构造方法,但是最终调用的都是最后一个构造方法,两个参数一个参数时File对象,还有一个参数代表是否是添加操作,append=true会保留原有文件的内容,在后面增加新内容,append=false贼会覆盖原有内容。
Write(byte[])方法:
public void write(byte b[]) throws IOException {
Object traceContext = IoTrace.fileWriteBegin(path);
int bytesWritten = 0;
try {
writeBytes(b, 0, b.length, append);
bytesWritten = b.length;
} finally {
IoTrace.fileWriteEnd(traceContext, bytesWritten);
}
}
public void write(byte b[], int off, int len) throws IOException {
Object traceContext = IoTrace.fileWriteBegin(path);
int bytesWritten = 0;
try {
writeBytes(b, off, len, append);
bytesWritten = len;
} finally {
IoTrace.fileWriteEnd(traceContext, bytesWritten);
}
}
private native void writeBytes(byte b[], int off, int len, boolean append)
throws IOException;
两种方式都是将内容写入这个文件输出流。
FileOutputStream比较常见,也比较简单,我们直接写一个实例,复制一个java文件到txt文件中:
测试代码:
package io;
/**
* copy .java to .txt
* 从内存中 先读再写
*/
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author 18092106
* @create 2018-09-05 19:58
**/
public class TestFileOutputStream {
public static void main(String[] args) {
try (FileOutputStream fos = new FileOutputStream("Out.txt", false);
FileInputStream fis = new FileInputStream("E:\\code\\leetcode\\src\\io\\TestFileOutputStream.java")) {
byte[] bytes = new byte[1024];
int hasRead = 0;
while((hasRead = fis.read(bytes)) != -1){
fos.write(bytes,0,hasRead);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
将需要复制的文件先读取到内存中,在从内存中读取输出。
-
ByteArrayOutputStream
ByteArrayOutputStream自己声明了一个缓冲区,每次操作的都是自己缓冲区的字节数组,我们看下他是怎么工作的:
构造方法:
public ByteArrayOutputStream() {
this(32);
}
public ByteArrayOutputStream(int size) {
if (size < 0) {
throw new IllegalArgumentException("Negative initial size: "
+ size);
}
buf = new byte[size];
}
每当声明一个ByteArrayOutputStream的时候,都会默认构造一个大小为32的字节数组。
再看下write(int)方法:
public synchronized void write(int b) {
ensureCapacity(count + 1);
buf[count] = (byte) b;
count += 1;
}
private void ensureCapacity(int minCapacity) {
// overflow-conscious code
if (minCapacity - buf.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = buf.length;
int newCapacity = oldCapacity << 1;
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity < 0) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
buf = Arrays.copyOf(buf, newCapacity);
}
在输出单个字节时,首先会去验证一下默认缓冲区和当前已使用容量的大小,如果当前使用的容量即将超过缓冲区,那么执行扩容操作:
int newCapacity = oldCapacity << 1;
直接扩容两倍,作为新的缓冲区
Write(byte[])方法类似,空间不够就先扩容,然后再将字节数组复制到缓冲区:
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;
}
ByteArrayOutPutStream中还有两个方法介绍下:
将缓冲区中的字节数组输出
public synchronized byte toByteArray()[] {
return Arrays.copyOf(buf, count);
}
将字节数组通过另一个输出流输出,比如可以和FileOutputStream配合,输出文件
public synchronized void writeTo(OutputStream out) throws IOException {
out.write(buf, 0, count);
}
我们写个实例测试下上面的几个方法:
package io;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author 18092106
* @create 2018-09-05 20:10
**/
public class TestByteArrayOutputStream {
public static void main(String[] args) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(32);
bos.write(97);
byte[] bytes = {97,98,99,100};
try {
bos.write(bytes);
for (byte b:bos.toByteArray()) {
System.out.println((char) b);
}
bos.writeTo(new FileOutputStream("Out2.txt"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出结果:
-
FilterOutputStream
和FilterInputStream一样,使用装饰器设计模式,本身不提供功能,只用来包装其他输出流,在其他输出流的方法基础上,实现一些功能。
-
BufferedOutputStream
BufferedOutputStream就是继承了FilterOutputStream的一个实现类,用来实现缓冲功能,减少io的交互,看下源码:
构造方法:
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
可以看到,BufferedOutputStream在实例化时会默认构造一个大小为8192的字节数组。
再看下最主要的write(byte[])方法:
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buf.length) {
/* If the request length exceeds the size of the output buffer,
flush the output buffer and then write the data directly.
In this way buffered streams will cascade harmlessly. */
flushBuffer();
out.write(b, off, len);
return;
}
if (len > buf.length - count) {
flushBuffer();
}
System.arraycopy(b, off, buf, count, len);
count += len;
}
在输出字节时,首先会进行判断,如果输出的字节数组的大小不超过缓冲区的大小,将这部分内容复制到默认的缓冲区中,如果输出的字节数组的大小超过了缓冲区的大小,那么调用flushBuffer()方法,我们看下这个方法的作用:
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
可以看到,这个方法调用包装的输出流的write方法,输出字节,同时将计数器清零
简单理解Buffered OutputStream的原理就是,它会默认构造一个8192也就是4K大小的一个缓冲区,当输出的字节数组的大小不超过它时,不会直接输出,而是被放到这个缓冲区中,当缓冲区中的空间不够时,再调用被包装的输出流的write(byte[])方法输出字节数组。
写个实例来看下效果:
package io;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
/**
* @author 18092106
* @create 2018-09-03 19:44
**/
public class TestBufferedOutputStream {
public static void main(String[] args) {
try{
FileOutputStream fos = new FileOutputStream("out3.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write(97);
}catch (Exception e){
e.printStackTrace();
}
}
}
可以看到,这个代码很简单,就是利用BufferedOutputStream包装一个FileOutputStream输出一个字节到文件中,我们执行上面的代码,看下那个文件的内容:
可以看到文件的确被成功创建了,但是文件并没有出现我们输出的字节,我们加上一个方法再试一下:
bos.flush();
可以看到,再加上了flush()这个方法以后,文件中出现了我们想要的内容,为什么会有这种情况呢?我们看下flush()做了什么事:
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
可以看到它调用强制刷洗缓冲区的方法,我们已经看过源码,里面调用就是out的write方法,flush起到的作用其实正是如此,通过强制调用输出流的write方法,将缓冲区的内容输出,避免出现上面第一次代码的情况,有内容在缓冲区中,没有输出。
细心的小伙伴可能发现了,之前的输出流我们都没有调用flush()方法,但是好像并没有影响,其实是因为这些类大部分都实现了Closeable和Flushable接口,然后jdk7以后支持try(XX x = new XX)catch{}的写法,像这种需要关闭,刷新的对象可以直接在try中进行声明,这样会自动关闭和刷新。