《Java I/O》Chapter2

chapter2 输出流

java.io.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是一个抽象类。 子类提供抽象write(int b)方法的实现。 它们也可能会覆盖四种非抽象方法。 例如,FileOutputStream类使用调用本机代码写入文件的方法覆盖所有五个方法。 尽管OutputStream是抽象的,但通常你只需要知道你拥有的对象就是OutputStream; 对你来说,OutputStream的更具体的子类是隐藏的。 例如,java.net.URLConnection的getOutputStream()方法具有以下签名:

public OutputStream getOutputStream() throws IOException

根据与此URLConnection对象关联的URL的类型,返回的输出流的实际类可能是sun.net.TelnetOutputStream,sun.net.smtp.SmtpPrintStream,sun.net.www.http.KeepAliveStream, 或完全其他的东西。 作为程序员,你所需要知道的就是,返回的对象是某种OutputStream。

此外,即使使用已知类型的子类,你仍需要能够使用从OutputStream继承的方法。 而且由于继承的方法未包含在API文档中,因此务必记住它们在哪里。 例如,java.io.DataOutputStream类没有声明close()方法,但是你仍然可以调用它从其超类继承的方法。
字节写入输出流
OutputStream类的基本方法是write()方法:

public abstract void write(int b) throws IOException

此方法写入数据的单个无符号字节,其值应介于0和255之间。如果你传递一个大于255或小于0的数,在写之前会进行模256运算。

示例2-1,ASCII字符,是一个简单的程序,它在控制台上写入可打印的ASCII字符(32到126)。控制台将数值解释为ASCII字符,而不是数字。这是控制台的特性,而不是System.out,OutputStream类或其特定子类的功能。write()方法只是将特定的位模式发送到特定的输出流。该位模式的解释方式取决于流另一端连接的内容。

示例 2-1. ASCII字符程序

import java.io.*;

public class AsciiChart{
    public static void main(String[] args){
       for(int i = 32;i < 127;i++){
          System.out.write(i);
          // break line after every eight characters.
          if(i % 8 == 7)
            System.out.write('\n');
          else
            System.out.write('\t');
       }
    }
}

注意’\t’和’\n’的用法。编译器将它们分别转换为数字9和10。当这些数字写入控制台时,控制台将它们分别解释为制表符和换行符。同样的效果也可以通过if语句来实现:

if(i % 8 == 7)
  System.out.write('\n');
else
  System.out.write('\t');

输出如下:

% java AsciiChart
! " # $ % & '
( ) * + , - . /
0 1 2 3 4 5 6 7
8 9 : ; < = > ?
@ A B C D E F G
H I J K L M N O
P Q R S T U V W
X Y Z [ \ ] ^ _
` a b c d e f g
h i j k l m n o
p q r s t u v w
x y z { | } ∼ 

write()方法可以抛出IOException异常,因此该方法的调用最好放在try/catch块中,或者让调用的方法抛出IOException异常。举例:

try {
    for (int i = 32;i<=127;i++)
    out.write(i);    
} catch (IOException ex){
    System.err.println(ex);
}

细心的读者会注意到,示例2-1实际上并未捕获任何IOException。 System.out是实例的PrintStream类使用不会引发IOException的变体覆盖write()。 这是非常不寻常的,并且PrintStream几乎是唯一这样做的类。 我将在第7章中对PrintStream进行更多说明,包括这种非常不安全的行为。
写入字节数组
以大块的形式写入数据通常比逐字节写入数据要快。write()方法的方法重载:

public void write(byte[] data) throws IOException
public void write(byte[] data, int offset, int length) throws IOException

第一个变量写入整个字节数组数据。第二个只写入从偏移量开始到指定长度的数据。例如,下面的代码片段将字符串中的字节写入System.out:

String s = "How are streams treating you?";
byte[] data = s.getBytes();
System.out.write(data);

相反,如果试图一次写入太多数据,则可能会遇到性能问题。确切的转折点取决于数据的最终目的地。文件通常最好以磁盘块大小的小倍数写入,通常为1024、2048或4096字节。网络连接通常需要较小的缓冲区大小——128或256字节。最佳缓冲区大小取决于系统特定的细节,因此无法确定,但我通常使用128字节用于网络连接,1024字节用于文件。

Example 2-2是一个简单的程序,它构造了一个填充有ASCII字符的字节数组,然后通过调用write()方法将其打印到控制台上。

Example 2-2.ASCII数组程序

import java.io.*;
public class AsciiArray{
    public static void main(String[] args){
        byte[] b = new byte[(127-31)*2];
        int index = 0;
        for (int i = 32; i < 127; i++) {
            b[index++] = (byte) i;
            // Break line after every eight characters.
            if (i % 8 == 7) b[index++] = (byte) '\n';
            else b[index++] = (byte) '\t';
        }
        b[index++] = (byte) '\n';
        try {
            System.out.write(b);
        } catch (IOException  ex){
            System.err.println(ex);
        }
}

输出与Example 2-1相同。由于控制台的性质,此特定程序可能不会比Example 2-1快很多,但如果你将数据写入文件而不是控制台,则肯定会快很多。在对write()的一次调用中写入字节数组与通过对数组的每个数据依次调用write()来写入,同一数组之间的性能差异很容易达到百倍更多

关闭输出流
当你用完流时,应该把它关上。这允许操作系统释放与流相关的任何资源。

这些资源具体是什么取决于你的平台,并随流的类型而变化。然而,许多系统的资源是有限的。

例如,在某些个人计算机操作系统上,一次打开的文件不能超过几百个。多用户操作系统有更大的权限和资源,但仍然有限制。

要关闭流,请调用其close()方法:

public void close() throws IOException

例如,再次假设out是OutputStream,调用out.close()关闭流并释放任何底层资源,如与流关联的文件句柄或网络端口。

一旦你关闭了一个输出流,你可能就不能在这个流上写任何东西了。尝试这样做通常会导致IOException异常,尽管有一些类不会发生这种情况。

提示
同样,System.out是局部异常,因为作为PrintStream,它抛出的所有异常都被吃掉了。一旦关闭System.out,就无法对其进行写入。 这样做不会引发任何异常; 但是,你的输出将不会出现在控制台上。

并非所有流都需要关闭——例如字节数组输出流不需要关闭。但是,与文件和网络连接关联的流在处理完后应该始终关闭。例如,如果你打开一个文件进行写入,但在完成时忽略了关闭它,则可能会阻止其他进程读取或写入该文件。通常,文件是这样关闭的:

try {
   OutputStream out = new FileOutputStream("numbers.dat");
   // Write to the stream...
   out.close();
} catch (IOException ex){
   System.err.println(ex);  
}

然而,这个代码片段有潜在泄漏问题。如果写入时引发IOException,流将不会关闭。更可靠的做法是在finally块中关闭流,这样无论是否抛出异常,流都会被关闭。为此,需要在try块外部声明OutputStream变量。例如:

// Initialize this to null to keep the compiler from complaining
// about uninitialized variables
OutputStream out = null;
try {
   out = new FileOutputStream("numbers.dat");
   // Write to the stream...
}catch (IOException ex) {
   System.err.println(ex);
}finally {
  if (out != null) {
    try{
        out.close();
    }catch(IOException ex){
        System.out.println(ex);
    }
   }
}   

可变作用域和嵌套的try-catch-finally块使更安全,但不美观。你可以选择将任何IOException异常不捕获直接抛出,那么代码可以更干净一些;也就是说,如果包含此代码的方法声明抛出IOException异常。在这种情况下,对close()的典型调用如下所示:

// Initialize this to null to keep the compiler from complaining
// about uninitialized variables
OutputStream out == null;
try {
out = new FileOutputStream("numbers.dat");
// Write to the stream...
}finally {
if (out != null) out.close( );
}

Closeable接口
Java 5添加了Closeable接口由OutputStream类来实现:

package java.io;

public interface Closeable{
    void close() throws IOException;
}

InputStream、Channel、Formatter和其他各种可以close()的也实现了这个接口。就我个人而言,我从来没有弄清楚这个额外接口的理由是什么,但如果出于某种原因,你想编写一个只接受可以关闭的参数或类似参数的方法,那么它就在那里。

刷新输出流
许多输出流使用缓冲区写入以提高性能。不是在写入时将每个字节发送到目的地,而是将字节累积在一个大小从几个字节到几千字节的内存缓冲区中。当缓冲区填满时,所有的数据一次被发送。flush()方法强制写入数据,无论缓冲区是否已满:

public void flush() throws IOException

这与操作系统或硬件执行的任何缓冲不同。调用flush()不会清空这些缓冲区。(File Descriptor类中的sync()方法,会在第17章中讨论,有时会清空这些缓冲区。)

如果仅短时间使用流,则无需显式刷新它。 当关闭流时,它将自动刷新。 这应该在程序退出或调用close()方法时发生。 仅当您要确保在处理完数据流之前已发送数据时,才显式刷新输出数据流。 例如,周期性地通过网络发送数据突发的程序应在每个数据突发写入流后刷新。

当你试图调试崩溃的程序时,刷新通常很重要。当缓冲区填满时,所有流都会自动刷新,当程序正常终止时,所有流都应该刷新。但是,如果程序异常终止,缓冲区可能不会被刷新。在这种情况下,除非每次写入后都显式调用flush()方法,否则无法确定输出中显示的数据是否指示程序崩溃的点。事实上,程序在崩溃之前可能已经持续运行了一段时间。

System.out,System.err和其他一些(但不是全部),在每次调用println()以及每次写入的字符串中出现换行字符(’\n’)之后,打印流会自动刷新。可以在PrintStream构造函数中启用或禁用自动刷新。

Flushable接口
Java 5添加了Flushable接口由OutputStream类来实现:

package java.io;

public interface Flushable{
    void flush() throws IOException;
}

格式化程序和其他各种可以刷新的也实现了这个接口。我也从来没有弄清楚这个额外接口的理由是什么,但是如果出于某种原因,你想编写一个只接受可以作为参数刷新的对象或类似对象的方法,那么它就在那里。

OutputStream相关子类
OutputStream是一个抽象类,主要描述任何OutputStream对象的可用操作。特定的子类知道如何将字节写入特定的目的地。例如,FileOutputStream使用本机代码在文件中写入数据,ByteArrayOutputStream使用纯Java在扩展字节数组中写入其输出。

回想一下OutputStream中write()方法有三个重载变体,一个抽象,两个具体:

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

子类必须实现抽象write(int b)方法。他们通常还重写三参数write(byte[]data,int offset,int length)的方法,以提高性能。OutputStream中write()方法的三参数版本的实现只是重复调用write(int b)方法,也就是说:

public void write(byte[] data, int offset, int length) throws IOException{
    for (int i = offset;i < offset+length; i++){
          write(data[i]);
    }
}

大多数子类可以提供这种方法的更有效实现。write()的一个参数变体仅调用write(data,0,data.length); 如果三参数变体已被覆盖,则此方法将执行得很好。 但是,仍有一些子类可能会覆盖它。

**示例2-3 **是一个名为NullOutputStream的简单程序,该程序模仿Unix操作系统上/ dev / null的行为,写入空输出流的数据将丢失。

示例 2-3.NullOutputStream类

package com.elharo.io;
import java.io.*;

public class NullOutputStream extends OutputStream {
private boolean closed = false;
public void write(int b) throws IOException {
 if (closed) throw new IOException("Write to closed stream");
}
public void write(byte[] data, int offset, int length) throws IOException {
 if (data == null) throw new NullPointerException("data is null");
}
public void close(){
    closed = true;
}

从超类继承的no-op flush()方法在这里已经足够好了,因为该流确实不需要刷新。 但是,请注意,此类确实需要在写入任何内容之前检查流是否已关闭,并检查传递给write()的数组是否为null。 大多数子类将需要进行类似的检查。

通过将System.out和System.err重定向到程序的出厂版本中的空输出流,可以禁用任何可能通过质量保证而漏掉的调试消息。 例如

OutputStream out = new NullOutputStream();
PrintStream ps = new PrintStream(out);
System.setOut(ps);
System.setErr(ps);

输出流的图形用户界面
作为示例,我将展示javax.swing.JTextArea的子类,该子类可以连接到输出流。 当数据写入流中时,它会附加到默认字符集中的文本区域。(这不是理想的。由于文本区域包含文本,因此编写者将是此数据的更好来源。在以后的章节中,我将在该类上进行扩展,改为使用writer。就目前而言,这是一个整洁的示例。)此子类如示例2-4所示。

实际的输出流包含在JStreamedTextArea类内的内部类中。每个JStreamedTextArea组件在其theOutput字段中都包含一个TextAreaOutputStream对象。客户端程序员通过getOutputStream()方法访问此对象。

JStreamedTextArea类具有四个重载的构造函数,它们模仿javax.swing.JTextArea类中的四个构造函数,每个构造函数采用文本,行和列的不同组合。 前三个构造函数仅使用this()将其参数和适当的默认值传递给最通用的第四个构造函数。 第四个构造函数调用最通用的超类构造函数,然后调用setEditable(false)以确保用户在输出流向其中时不更改文本。

示例 2-4.JStreamedTextArea组件

package com.elharo.io.ui;
import javax.swing.*;
import java.io.*;
public class JStreamedTextArea extends JTextArea {

    private OutputStream theOutput = new TextAreaOutputStream( );

    public JStreamedTextArea( ) {
        this("", 0, 0);
    }
    public JStreamedTextArea(String text) {
        this(text, 0, 0);
    }
    public JStreamedTextArea(int rows, int columns) {
        this("", rows, columns);
    }
    public JStreamedTextArea(String text, int rows, int columns) {
        super(text, rows, columns);
        setEditable(false);
    }
    public OutputStream getOutputStream( ) {
        return theOutput;
    }
    private class TextAreaOutputStream extends OutputStream {
        private boolean closed = false;
        public void write(int b) throws IOException {
            checkOpen( );
            // recall that the int should really just be a byte
            b &= 0x000000FF;
            // must convert byte to a char in order to append it
            char c = (char) b;
            append(String.valueOf(c));
        }
        private void checkOpen( ) throws IOException {
            if (closed) throw new IOException("Write to closed stream");
        }
        public void write(byte[] data, int offset, int length) throws IOException {
            checkOpen( );
            append(new String(data, offset, length));
        }
        public void close( ) {
            this.closed = true;
        }
   }
} 

TextAreaOutputStream内部类很简单,它扩展了OutputStream,因此必须实现抽象方法write()。 它还重写了主数组write()方法以提供更有效的实现。 最后,它重写close()以确保在关闭流之后不进行任何写操作。

图 2-1显示了一个使用JStreamedTextArea显示从下载的数据的程序http://www.oreilly.com/。本图的应用程序将在第5章中开发。

图2-1

在以后的章节中,我将使用尚未讨论的技术来复习和改进本课程。 特别是,我将更加关注字符集和编码的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值