在java中我们使用输入流来向一个字节序列对象中写入,使用输出流来向输出其内容。C语言中只使用一个File包处理一切文件操作,而在java中却有着60多种流类型,构成了整个流家族。看似庞大的体系结构,其实只要使用适合的方法将其分门别类,就显得清晰明了了。而我准备将其按照处理文件类型的不同,分为字节流类型和字符流类型。
基类流
其实始终有人搞不清楚到底InputStream是读还是OutputStream是读。其实很简单就可以记住,你把你自己想象为是一个程序,InputStream对你来说是输入,也就是你要从某个地方读到自己这来,而OutputStream对你来说就是输出,也就是说你需要写到某个地方。这样就可以简单的区分输入输出流。InputStream是一个输入流,也就是用来读取文件的流,抽象方法read读取下一个字节,当读取到文件的末尾时候返回 -1。如果流中没有数据read就会阻塞直至数据到来或者异常出现或者流关闭。这是一个受查异常,具体的调用者必须处理异常。除了一次读取一个字节,InputStream中还提供了read(byte[]),读取多个字节。read(byte[])其实默认调用的还是read(byte b[], int off, int len)方法,表示每读取一个字节就放在b[off++]中,总共读取len个字节,但是往往会出现流中字节数小于len,所以返回的是实际读取到的字节数。
接下来是一些高级的用法,skip方法表示跳过指定的字节数,来读取。调用这种方法需要知道,一旦跳过就不能返回到原来的位置。当然,我们可以看到还有剩下的三种方法,他们一起合作实现了可重复读的操作。mark方法在指定的位置打上标记,reset方法可以重新回到之前的标记索引处。但是我们可以想到,它一定是在打下mark标记的地方,使用字节数组记录下接下来的路径上的所有字节数据,直到你使用了reset方法,取出字节数组中的数据供你读取(实际上也不是一种能够重复读,只是用字节数组记录下这一路上的数据而已,等到你想要回去的时候将字节数组给你重新读取)。
OutputStream是一种输出流,具体的方法和InputStream差不多,只是,一个读一个写。但是,他们都是抽象类,想要实现具体的功能还是需要依赖他们的子类来实现,例如:FileInputStream/FileOutputStream等。
文件字节流
FileInputStream继承与InputStream,主要有以下两个构造方法:
public FileInputStream(String name)
public FileInputStream(File file)
第一种构造方法传的是一个字符串,实际上是一个确定文件的路径,内部将此路径封装成File类型,调用第二种构造方法。第二中构造方法,直接绑定的是一个具体的文件。FileInputStream 的内部方法其实和父类InputStream中定义的方法差不多,我们通过一个读文件的实例来演示用法。
FileInputStream fin = new FileInputStream("hello.txt");
byte[] buffer = new byte[1024];
int x = fin.read(buffer,0,buffer.length);
String str = new String(buffer);
System.out.println(str);
System.out.println(x);
fin.close();
结果意料之中,调用了read方法将hello.txt中的内容读到字节数组buffer中,然后通过String类构造方法将字节数组转换成字符串。返回实际上读取到的字节数13。(10个字母+两个空格+一个字符串结束符)FileOutputStream继承父类OutputStream,主要方法代码如下:
private final boolean append;
public FileOutputStream(String name)
public FileOutputStream(String name, boolean append)
public FileOutputStream(File file)
public FileOutputStream(File file, boolean append)
private native void writeBytes(byte b[], int off, int len, boolean append)
public void write(byte b[]) throws IOException
FileOutputStream的一些基本的操作和FileInputStream类似,只是一个是读一个是写。我们主要要知道,append属性是指定对于文件的操作是覆盖方式(false),还是追加方式(true)。下面通过一个实例演示其用法:
FileOutputStream fou = new FileOutputStream("hello.txt");
String str = "Walker_YAM";
byte[] buffer = str.getBytes("UTF-8");
fou.write(buffer,0 ,buffer.length);
fou.close();
如我们所料,字符串"Walker_YAM"将会被写入hello.txt,由于没有指定append,所以将会覆盖hello.txt中的所有内容。
动态字节数组流
在我们上述的文件读取流中,我们定义 byte[] buffer = new byte[1024];,buffer数组为1024,如果我们将要读取的文件中的内容有1025个字节,buffer是不是装不下?当然我们也可以定义更大的数组容量,但是从内存的使用效率上,这是低效的。我们可以使用动态的字节数组流来提高效率。
ByteArrayInputStream的内部使用了类似于ArrayList的动态数组扩容的思想。
protected byte buf[];
protected int count;
public ByteArrayInputStream(byte buf[])
public ByteArrayInputStream(byte buf[], int offset, int length)
public synchronized int read()
public synchronized int read(byte b[], int off, int len)
ByteArrayInputStream内部定义了一个buf数组和记录数组中实际的字节数,read方法也很简单,读取下一个字节,read(byte b[], int off, int len) 将内置字节数组读入目标数组。实际上,整个ByteArrayInputStream也就是将一个字节数组封装在其内部。为什么这么做?主要还是为了方便参与整个InputStream的体系,复用代码。ByteArrayOutputStream的作用要比ByteArrayInputStream更加的实际一点:
protected byte buf[];
protected int count;
public ByteArrayOutputStream() { this(32); }
public ByteArrayOutputStream(int size)
private void ensureCapacity(int minCapacity)
public synchronized void write(byte b[], int off, int len)
public synchronized void writeTo(OutputStream out)
public synchronized byte toByteArray()[]
public synchronized String toString()
和ByteArrayInputStream一样,内部依然封装了字节数组buf和实际容量count,通过构造方法可以指定内置字节数组的长度。主要的是write方法,将外部传入的字节数组写到内置数组中,writeTo方法可以理解为将自己内置的数组交给OutputStream 的其他子类使用。toByteArray和toString则会将内置数组转换成指定类型返回。下面我们利用他们解决刚开始说的效率问题。
FileInputStream fin = new FileInputStream("hello.txt");
ByteArrayOutputStream bou = new ByteArrayOutputStream();
int x = 0;
while((x = fin.read()) !=-1){
bou.write(x);
}
System.out.println(bou.toString());
从hello文件中每读取一个字节写入ByteArrayOutputStream 中,我们不用担心hello文件太大而需要设置较大的数组,使用ByteArrayOutputStream 动态增加容量,如果添加字节即将超过容量上限,进行扩充(往往是指数级扩充)
装饰者字节流
上述的流都是直接通过操作字节数组来实现输入输出的,那如果我们想要输入一个字符串类型或者int型或者double类型,那还需要调用各自的转字节数组的方法,然后将字节数组输入到流中。我们可以使用装饰流,帮我们完成转换的操作。我们先看DataOutputStream。
public DataOutputStream(OutputStream out)
public synchronized void write(byte b[], int off, int len)
public final void writeBoolean(boolean v)
public final void writeByte(int v)
public final void writeShort(int v)
public final void writeInt(int v)
public final void writeDouble(double v)
简单的列举了一些方法,可以看到,DataOutputStream只有一个构造方法,必须传入一个OutputStream类型参数。(其实它的内部还是围绕着OutputStream,只是在它的基础上做了些封装)。我们看到,有writeBoolean、writeByte、writeShort、writeDouble等方法。他们内部都是将传入的 boolean,Byte,short,double类型变量转换为了字节数组,然后调用从构造方法中接入的OutputStream参数的write方法。
缓冲流
在这之前,我们读取一个字节就要将它写会磁盘,这样来回开销很大,我们可以使用缓冲区来提高效率,在缓冲区满的时候,或者流关闭时候,将缓冲区中所有的内容全部写会磁盘。BufferedInputStream和BufferedOutputStream也是一对装饰流,我们先看看BufferedInputStream:
private static int DEFAULT_BUFFER_SIZE = 8192;
protected volatile byte buf[];
protected int pos;
protected int count;
public BufferedInputStream(InputStream in)
public BufferedInputStream(InputStream in, int size)
public synchronized int read()
public synchronized void mark(int readlimit)
public synchronized void reset()
一样也是装饰类流,第一种构造方法要求必须传入InputStream类型参数,DEFAULT_BUFFER_SIZE 指定了默认的缓冲区的大小,当然还可以使用第二种构造方法指定缓冲区的大小(当然不能超过上界),read方法读取的时候会将数据读入内部的缓冲区中,当然缓冲区也是可以动态扩容的。
BufferedInputStream bi = new BufferedInputStream(new FileInputStream("hello.txt"));
bi.read();
bi.read();
bi.read();
bi.read();
System.out.println(bi.available());
BufferedOutputStream和它是逆操作,不在赘述。这种缓冲字节流可以很大程度上提高我们的程序执行的效率,所以一般在使用别的流的时候都会包装上这层缓冲流。