Java的基本输出流类是:java.io.OutputStream:
public abstract class OutputStream
这个类提供了写入数据所需的基本方法。这些方法包括:
public abstract void write(int b) throws IOException
public void write(byte[] data) throws IOException
public void write(byte[] data,int offset,int length) throws IOException
public void flush() throws IOException
public void close() throws IOException
OutputStream的子类使用这些方法向某种特定介质写入数据。例如,FileOutputStream使用这些方法将数据写入文件。TelnetOutputStream使用这些方法将数据写入网络连接。ByteArrayOutputStream使用这些方法将数据写入扩展的字节数组。但不管写入哪种介质,大多都会使用同样的这5个方法。
OutputStream的基本方法是 write(int b)。这个方法接受一个 0 到 255 之间的整数作为参数,将对应的字节写入到输出流中。这个方法声明为抽象方法,因为各个子类需要修改这个方法来处理特定的介质。例如,ByteArrayOutputStream可以用纯java代码实现这个方法,将字节复制到数组中。与此不同,FileOutputStream则需要使用原生代码,这些代码了解如何将数据写入到主机平台的文件中。
注意,虽然这个方法接受一个int作为参数,但它实际上会写入一个无符号byte。Java没有无符号byte数据类型,所以这里要使用int来代替。无符号字节和有符号字节之间唯一的真正的区别:它们都由8个二进制位组成,当使用write(int b)将int写入一个网络连接时,线缆上只会放8个二进制位。如果将一个超出 0~255 的int传入write(int b),将写入这个数的最低字节,其他3个字节将被忽略(这正是将int强制转换为byte的结果)。提示:不过,在极少数情况下,你可能会看到一些有问题的第三方类,在写超出 0~255 的值时,它们的做法有所不同,比如会抛出IllegalArgumentException异常或者总是写入255,所以尽可能要避免写超出 0~255 的int。
我们来看看下面这个例子,这个例子是循环生成72个字符的文本行,其中包含可显示的ASCII字符(可显示的ASCII字符是33到126之间的字符,不包含各种空白符和控制字符)。第一行按顺序包含字符33到字符104,第二行包含字符34到字符105,一直这样到字符回绕:
package test;
import java.io.IOException;
import java.io.OutputStream;
public class ASCIICharPrint {
public static void main(String[] args) {
OutputStream out = null;
try {
out = System.out;
generateCharacters(out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(out != null) {
try {
out.close();
} catch (IOException e2) {}
}
}
}
public static void generateCharacters(OutputStream out) throws IOException {
int firstPrintableCharacter = 33;
int numberOfPrintableCharacters = 94;
int numberOfCharactersPerLine = 72;
int start = firstPrintableCharacter;
for(int k=0;k<1000;k++){
for(int i = start; i < start + numberOfCharactersPerLine; i++){
out.write(((i - firstPrintableCharacter) % numberOfPrintableCharacters) +
firstPrintableCharacter);
}
out.write('\r');
out.write('\n');
out.flush();
start = ((start + 1) - firstPrintableCharacter) %
numberOfPrintableCharacters + firstPrintableCharacter;
}
}
}
这里将一个OutputStream通过out参数传入generateCharacters()方法。一次向out写入1个字节。这些字节作为33到126之间循环序列中的整数给出。这里的大部分运算都是让循环在这个范围内回绕。在写入每个72字符块之后,就向输出流写入一个回车和一个换行。然后计算下一个起始字符,重复这个循环。
一次写入1字节通常效率不高。例如,流出以太网卡的每个TCP分片包含至少40字节的开销用于路由和纠错。如果每字节都单独发送,那么与你预想的数据量相比。实际填入到网络中的数据可能会高出41倍以上!如果增加主机网络层协议的开销,情况可能更糟糕。因此,大多数TCP/IP实现都会在某种程度上缓存数据。也就是说,它们在内存中积累数据字节,只有积累到一定量的数据后,或者经过了一定的时间后,才将所积累的数据发送到最终目的地。不过,如果有多字节要发送,则一次全部发送不失为一个好主意。使用:write(byte[] data) 或 write(byte[] data, int offset, int length)通常比一次写入1个字节要快得多。
例如,下面是generateCharacters()方法的另一个实现(使用write(byte[] data)),它将整行打包在一个字节数组中,一次发送一 行:
package test;
import java.io.IOException;
import java.io.OutputStream;
public class ASCIICharPrint2 {
public static void main(String[] args) {
try (OutputStream out = System.out) {
generateCharacters(out);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void generateCharacters(OutputStream out) throws IOException {
int firstPrintableCharacter = 33;
int numberOfPrintableCharacters = 94;
int numberOfCharactersPerLine = 72;
int start = firstPrintableCharacter;
byte[] line = new byte[numberOfCharactersPerLine + 2]; //加2对应回车与换行
for(int k=0;k<1000;k++){
for(int i = start; i < start + numberOfCharactersPerLine; i++){
line[i - start] = (byte)((i - firstPrintableCharacter) % numberOfPrintableCharacters + firstPrintableCharacter);
}
line[numberOfCharactersPerLine] = (byte)'\r';
line[numberOfCharactersPerLine + 1] = (byte)'\n';
out.write(line);
out.flush();
start = ((start + 1) - firstPrintableCharacter) %
numberOfPrintableCharacters + firstPrintableCharacter;
}
}
}
我们再来看看使用write(byte[] data, int offset, int length)重写的generateCharacters()方法,参数data表示要打印的字节数组,参数offset是在字节数组中的偏移量,比如从数组下标为5开始,参数length表示要从字节数组中打印的字节长度,如10,示例如下,不过该示例是是从字节数组中下标为5开始,打印该数组的10个字节:
package test;
import java.io.IOException;
import java.io.OutputStream;
public class ASCIICharPrint3 {
public static void main(String[] args) {
try (OutputStream out = System.out) {
generateCharacters(out);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void generateCharacters(OutputStream out) throws IOException {
int firstPrintableCharacter = 33;
int numberOfPrintableCharacters = 94;
int numberOfCharactersPerLine = 72;
int start = firstPrintableCharacter;
byte[] line = new byte[numberOfCharactersPerLine];
for(int k=0;k<1000;k++){
for(int i = start; i < start + numberOfCharactersPerLine; i++){
line[i - start] = (byte)((i - firstPrintableCharacter) % numberOfPrintableCharacters + firstPrintableCharacter);
}
out.write(line,5,10); //5为下标,也就是偏移量,10为打印的字节长度
out.write('\r');
out.write('\n');
out.flush();
start = ((start + 1) - firstPrintableCharacter) %
numberOfPrintableCharacters + firstPrintableCharacter;
}
}
}
第二,三个示例与第一个示例的区别在于这些字节在写入网络之前先打包到一个字节数组中。
与在网络硬件中缓存一样,流还可以在软件中得到缓冲,即直接用Java代码缓存。一般来说,这可以通过把BufferedOutputStream 或 BuferedWriter串链到底层流上来实现,后面我们会探讨这种技术。因此,在写入数据完成后,刷新(flush)输出流非常重要。例如,假设已经向使用HTTP Keep-Alive的HTTP1.1服务器写入300字节的请求,通常你会等待响应,然后再发送更多的数据。不过,如果输出流有一个1024字节的缓冲区,那么这个流在发送缓冲区中的数据之前会等待更多的数据到达。在服务器响应到达之前不会向流写入更多数据,但是响应永远也不会到达,因为请求还没有发送!下图显示了这种两难境地。flush()方法可以强迫缓冲的流发送数据,即使缓冲区还没有满,以此来打破这种死锁状态:
不管你是否认为有必要,刷新输出流都很重要。应当在关闭流之前立即刷新输出所有流。否则,关闭流时留在缓冲区中的数据可能会丢失。
最后,当结果一个流的操作时,要通过调用它的close()方法将其关闭。这会释放与这个流关联的所有资源,如文件句柄或端口。如果流来自一个网络连接,那么关闭这个流也会终止这个连接。一旦输出流关闭,继续写入时就会抛出IOException异常。
在一个长时间运行的程序中,如果未能关闭一个流,则可能会泄漏文件句柄、网络端口和其他资源。因此,在Java6和更早版本中,明智的做法是在一个finally块中关闭流。为了得到正确的变量作用域,必须在try块之外声明流变量,但必须在try块内完成初始化。另外,为了避免NullPointerException异常,在关闭流之前需要检查流变量是否为null。最后通常都希望忽略关闭流时出现的异常,或者最多只是把这些异常记入日志。例如:
OutputStream out = null;
try {
out = new FileOutputStream("/tmp/data.txt");
//处理输出流...
} catch (IOException ex) {
System.err.println(ex.getMessage());
} finally {
if(out != null) {
try {
out.close();
} catch (IOException ex) {
//忽略
}
}
}
这个技术有时称为:释放模式(dispose pattern),这对于需要在垃圾回收前先进行清理的对象是很常见的。你会看到,这个技术不仅用于流,还可以用于socket、通道、JDBC连接等。注意:我们的第一个示例就是这么做的。
java7引入了“带资源的try”构造(try with resources),可以更简洁地完成这个清理。不需要在try块之外声明流变量,完全可以在try块的一个参数表中声明。我们对上一个例子进行一下重构,例如:
try (OutputStream out = new FileOutputStream("/tmp/data.txt")) {
//处理输出流...
} catch (IOException ex) {
System.err.println(ex.getMessage());
}
现在不再需要finally子句。Java会对try块参数表中声明的所有AutoCloseable对象自动调用close()。注意:我们第二,三个示例就是这么做的。
提示:只要对象实现了Closeable接口,都可以使用“带资源的try”构造,这包括几乎所有需要释放的对象。到目前为止,JavaMail Transport对象是我见过的唯一的例外。这些对象还需要显式地释放。