- 网络程序的很大一部分是简单的输入输出,即从一个系统向另一个系统移动字节。字节就是字节,在很大程度上,读服务器发送的数据与读取文件没什么不同;向客户传送数据与写入一个文件也没有什么区别。
Java中输入和输出组织不同于大多数其他语言。它是建立在流(stream)上。不同的基本流类(如java.io.FileInputStream和sun.net.TelnetOutputStream)用于读写特定的数据资源。但是所有的基本输出流使用同一种基本方法读数据。
过滤器流可以连接到输入流或输出流。它可以修改已经读出或写人的数据(例如,加密或压缩数据),或者可以简单地提供附加方法将已经读出或写入的数据转化成其他格式。
最后Reader和Writer也可以链接到输入流和输出流,从而允许程序读出和写入文本(即字符)而不是字节。如果使用正确,Reader和Writer能够处理多种类型的字符编码,包括SJIS和UTF-8等多字节字符集。
一、输出流
java的基本输出流是 java.io.OutputStream.
public abstract class OutputStream
n public abstract void write(int b) throws IOException
n public void write(byte[] data) throws IOException
n public void write(byte[] data,int offset,int length) throws IOException
n public void flush() throws IOException
n public void close() throws IOException
OutputStream的子类使用这些方法向指定媒体写入数据。
我始终相信,我们理解了为什么它们存在,就会更好地记住它们,好,现在开始说一下OutputStream类的方法的由来
Ø public abstract void write(int b) throws IOException
OutputStream的基本方法是write(int b)。该方法将介于0到255之间的整数看作变量,并将相应的字节写到一个输出流。该方法声明是个抽象方法,因为子类需要改变它以处理特定媒体。例如,ByteArrayOutputStream可以使用拷贝字节到其数组的纯Java代码来实现方法。但是,FileOutputStream就需要使用代码,此代码应该理解如何在主机平台上将数据写入文件。注意:尽管该方法把整形值作为变量,但是它实际上写入的是一个无符号字节。Java没有无符号字节数据类型,因此这里使用整型来代替。无符号字节和有符号字节之间的真正区别是编译器对它们的解释。二者都是由8位组成,并且当使用write(int b)将一个int写入到网络连接流时,只有8位数据传送。如果将一个超出0-255范围的int传给write(int b),则写入该数字的低位字节,而忽略余下的三个字节(大家都知道java的int是4个字节的,这里本质就是将int转换为byte)。
Ø public void write(byte[] data) throws IOException和public void write(byte[] data,int offset,int length) throws IOException
每次写入一个字节通常效率不高。因此,大部分TCP/IP程序将数据存入一定长度的缓冲区,即在内存中累积字节,并仅当累积了一定数目字节或过了一定的时间段,才将它们发送到最终的目的地。因此write(byte[] data)和write(byte[] data,int offset,int length)就是这样产生了。
Ø public void flush() throws IOException
我们可以在软件中或直接在Java代码中对流实施缓冲操作,也可以在网络硬件中对流实施缓冲操作。就好像BufferedOutputStream或BufferedWriter链接到底层流来实现流缓冲。因此,如果正在写入数据,则刷新输出流是相当重要的。例如,假设已经写入了一个300字节的请求给一个HTTP Keep-Alive的HTTP服务器,通常希望在发送更多数据之间等待响应。但是,如果输出流有一个1024字节的缓冲区,则该流可能在将数据发送出缓冲区之前正在等待更多的数据到达,但是这些数据似乎不会到达的,因为它们还没有发送出去,但是缓冲流不会发送数据给服务器,除非它从底层流获得更多的数据,但是底层流不会发送更多的数据,除非它从服务器获得数据,而服务器不会发送数据,除非它获得保留在缓冲区中的数据(死锁了!),flush()方法就可以解决了这个僵局,因为即使缓冲区未满,他也会强制要求实行缓冲操作的流传送数据。注意:是否对流实行了缓冲操作,这决定于你如何获得指向流的引用(例如,不论是否希望对System.out执行缓冲操作,都会对其实施缓冲)。如果刷新流需要刷新时,就必须刷新,但是如果刷新失败了就会导致不可预料、不可重复的程序挂起(flush()返回值是void啊),如果事先不了解挂起问题所在,就很难解决这个问题了。因此,在关闭所有流之前,应当立即刷新它们。否则,关闭流前,缓冲区中的剩余数据可能会丢失。
Ø public void close() throws IOException
最后当利用完流之后,应当调用close()方法关闭流。它会释放所有与这个流相关的资源,如文件句柄或端口。一旦输出流关闭了,再向其写入数据就会触发IOException异常。但是,有些类型可能允许对对象进行一定操作。如一个已关闭的ByteArrayOutputStream仍然可以转化成一个实际的字节数组,而且一个已关闭的DigestOutputStream仍可以返回其摘要。
二、输入流
java的基本输入流是java.io.InputStream
public abstract class InputStream
n public abstract int read() throws IOException
n public int read(byte[] data) throws IOException
n public int read(byte[] data,int offset,int length) throws IOException
n public long skip(long n) throws IOException
n public int available() throws IOException
n public void close() throws IOException
InputStream的具体子类使用这些方法从指定媒体读取数据。但是不论读取何种资源,几乎只能使用这六种方法。有时你甚至可能不知道正在从哪种类型的流中读取数据。如隐藏在sun.net包中TelnetInputStream是一个文档没有说明的类。TelnetInputStream的实例由java.net包中的多种方法返回;如java.net.URL的openStram()方法。但是,这些方法仅声明了返回InputStream,而不是更加明确的子类TelnetInputStream,这又是多态性在起作用了。子类的实例可以作为超类的实例透明使用。
来了,又来说明方法的由来了。
Ø public abstract void read() throws IOException
InputStream类的基本方法是没有参量的read()方法(这个与OutputStream不同了)。该方法从输入流资源读取一个单个字节数据并将数据作为0到255之间的数返回,返回-1时表示流的结尾。因为Java没有无符号字节的数据类型,所以数据以整型类型返回。Read()方法等待和阻塞该方法后人和代码的执行,直到获得数据的一个字节并准备读取该字节。因此,输入和输出可能相当慢,这时用户如果需要完成其他比较重要的任务时,最好试图将I/O放到它们自己的线程中。Read()方法被声明为抽象方法,因为子类需要改变它来处理特定媒体。给个例子
byte[] input=new byte[10];
for(int i=0;i
int b=in.read();
if(b==-1) break;
input[i]=(byte)b;
}
上面尽管read()方法仅读取字节,但是它返回的是整型值。因此在将结果存储到字节数组之前,需要一个类型转换的过程。当然,这会产生一个介于-128到127的有符号字节,而不是read()方法返回的0到255之间的一个无符号字节。但是,只要用户清楚使用的是无符号还是有符号字节就不会有很大问题。因此,我们可以把一个有符号字节转化成无符号字节(转换的原因是只有范围在0-255的整数才可以被存储在java的一个byte类型的变量中)。
int i=b>=0?b:256+b;
这里费了大篇幅,说明了read()返回的与java的byte类型的处理问题,大家可要注意阿,如果对java的原始数据类型还有兴趣,可以看一下我的原始数据类型学习笔记(未完成)。
Ø public int read(byte[] data) throws IOException、public int read(byte[] data,int offset,int length) throws IOException
每次读取一个字节和每次写入一个字节效率都不高,因此read(byte[] data)和read(byte[] data,int offset,int length)也相应产生了。这两个方法将从流中读取的多个字节填充到一个指定的数组中。注意:这些填充到数组的操作不一定会成功的。一个很普遍的情况是一个读试图不会完全失败也不会完全成功,它可能读出请求数据的一部分字节,而不是全部字节。例如,当实际上只有512字节已经到达服务器时,用户可能会试图从一个网络流上读取1024字节,而其他字节仍然在传送中,这些字节最终会到达服务器,但到达时却已是不可以获得的。因此,多字节读取方法会返回实际读取的字节数目。给个例子
byte[] input=new byte[1024];
int bytesRead=in.read(input);
代码段试图从InputStream in读取1024字节到数组input中。但是,如果仅有512字节可以获得,则这些字节就是将要读取的全部字节,并且bytesRead值会设为512。但我们为了保证在实际上读取到所有的字节,怎么办?看
int bytesRead=0;
int byteToRead=1024;
byte[] input=new byte[byteToRead];
while(bytesRead
bytesRead+=in.read(input,bytesRead,byteToRead-bytesRead);
}
Ø public int available() throws IOException
如果由于某种原因用户不希望读取数据,除非用户想要的全部数据可以立即得到,这时候就可以用available()方法返回的字节数是能够读取的最小字节数,而在实际上可以读取更多的字节,但是能够读取的字节数据至少与available()返回的字节数一样多。
看例子
int bytesAvailable=in.available();
byte[] input=new byte[bytesAvailable];
int byteTead=in.read(input,0,bytesAvailable);
//其他代码
这里我们可以断言bytesRead正好等于bytesAvailable,但不能断言bytesRead>0,因为available()返回0是有可能的。
流结束时:
available()返回0;
read(byte[] data,int offset,int length)通常返回-1;
流没有结束,可读取字节数即available()得到的值为0时
read(byte[] data,int offset,int length)会忽略流的结束,返回0;
Ø public long skip(long n) throws IOException
在极少数情况下,用户可能希望跳过数据而不去读取它们。Skip()方法就是实现这个功能的。这个方法在从文件读取数据时较为有用,而在网络连接流上则用处较小。因为网络连接流是有序的而且通常很慢,因此读取数据的耗时不会太多的超过跳过数据的耗时。文件可以随机访问,因此我们通过重定位文件指针就能简单的实现数据的跳转,而不是跳过每一个字节。
Ø public void close() throws IOException
和输出流一样,程序利用完输入流之后,就应该调用close()方法关闭该输入流了(要记住啊)。该方法会释放与输入流有关的所有资源,如文件句柄和端口。一旦输入流关闭,再从它读取数据时会触发IOException。但是,有些类型的流可能仍允许对对象进行一定的操作。例如,用户通常不希望从java.security.DigestInputStream中获取报文摘要,除非已经读取了所有数据并且关闭了输入流。
看到这里或许你还会问怎么还有三个方法没有呢,对,还有三个不常用的方法
public void mark(int readAheadLimit)
public void reset() throws IOException
public boolean markSupported();
这些方法允许程序备份和重新读取已经读取过的数据。要实现这个功能,需要用mark()方法在输入流中的当前位置作个标记,在以后的某点可以使用reset()方法重新将流定位到标记处,随后的读取将返回从标记初开始的数据。但是,从标记处到重新将流定位点不能任意长。重新定位到标记处之前允许读取的字节数就是由mark()的变量readAheadLimit决定。多长就会触发IOException,而且任何指定时刻,输入流中只可以有一个标记,如果标记了第二个标记,就会覆盖第一个标记了。其实标记和重新设置位置都是通过存储从内部缓冲区中的标记位置读出每一字节来实现。最麻烦的状况是,并非所有输入流都支持标记和重新设置位置的。所以在设置之前要用markSupported()方法检测一下。
实际上不支持标记和重新设置位置的流多于支持它们的流。Elliotte Rusty Harold大师觉得这几个方法设计的标准不高,将功能性与一个许多甚至可能是大部分子类都不可用的抽象超类结合是一个相当拙劣的想法。最好是将这三个方法放在不同的接口中。提供类似于markSupported()方法在运行时进行功能性检测是较为传统,非面向对象的解决方法。面向对象的方法将通过接口和类把该方法嵌入到面向对象系统中,从而在编译时检测所有的流。
Java.io中总是支持标记的输入流:BufferedInputStream和ByteArrayInputStream。