Java网络编程(二)流

news2024/12/28 19:51:41

        网络程序所做的很大一部分工作都是简单的输入和输出:将数据字节从一个系统移动到另一个系统。字节就是字节。在很大程度上讲,读取服务器发送给你的数据与读取文件并没什么不同。向客户端发送文本与写文件也没有什么不同。但是,Java中输入和输出(I/O)的组织与其他大多数语言(如Fortran、 C和C++)都不一样。因此,这里要用几页来总结一下Java独特的I/O方法。

        Java的I/O建立于流(stream) 之上。输人流读取数据;输出流写入数据。不同的流类,如java. io. FileInputStream和sun.net.TelnetoutputStream会读/写某个特定的数据源。但是,所有输出流都有相同的基本方法来写入数据,所有输入流也使用相同的基本方法来读取数据。创建一个流之后,读/写时通常可以忽略读/写的具体细节。

        过滤器(filter) 流可以串链到输入流或输出流上。读/写数据时,过滤器可以修改数据(例如,通过加密或压缩),或者只是提供额外的方法,将读/写的数据转换为其他格式。例如,java. io.DataOutputStream类就提供了一个方法,可以将int转换为4字节,并把这些字节写入底层的输出流。

        阅读器(reader) 和书写器(writer) 可以串链到输入流和输出流上,允许程序读/写文本(即字符)而不是字节。只要正确地使用,阅读器和书写器可以处理很多字符编码,包括多字节字符集,如SJIS和UTF-8。

        流是同步的。也就是说,当程序(确切地讲是线程)请求一个流读/写一段数据时,在做任何其他操作前,它要等待所读/写的数据。Java还支持使用通道和缓冲区的非阻塞I/O。非阻塞I/O稍有些复杂,但在某些高吞吐量的应用程序中( 如Web服务器),非阻塞I/O要快得多。通常情况下,基本流模型就是实现客户端所需要和应当使用的全部内容。由于通道和缓冲区依赖于流,下面将首先介绍流和客户端,后面还会讨论服务器使用的非阻塞I/O。

一、输出流

        Java的基本输出流类是java. io.OutputStream:

public abstract class 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 {
    }

    public void close() throws IOException {
    }

        OutputStream的子类使用这些方法向某种特定介质写入数据。例如,File0utputStream使用这些方法将数据写入文件。TelnetOutputStream使用这些方法将数据写入网络连接。ByteArrayOutputStream使 用这些方法将数据写入可扩展的字节数组。但不管写入哪种介质,大多都会使用同样的这5个方法。有时甚至可能不知道所写入的流具体是何种类型。例如,在Java类库文档中找不到TelnetOutputStream。它被有意地隐藏在sun包中。java. net中很多类的很多方法都会返回TelnetOutputStream,如java .net. Socket的getoutputStream()方法。但是,这些方法声明为只返回0utputStream,而不是更特定的子类TelnetoutputStream.这正是多态的威力。如果你知道如何使用这些超类,也就知道如何使用所有这些子类。

        OutputStream的基本方法是write(int b)。 这个方法接受一个0到255之间的整数作为参数,将对应的字节写人到输出流中。这个方法声明为抽象方法,因为各个子类需要修改这个方法来处理特定的介质。例如,ByteArray0utputStream 可以用纯Java代码实现这个方法,将字节复制到数组中。与此不同,File0utputStream则需要使用原生代码,这些代码了解如何将数据写入到主机平台的文件中。

        注意,虽然这个方法接受-一个int作为参数,但它实际上会写入一个无符号字节。Java没有无符号字节数据类型,所以这里要使用int来代替。无符号字节和有符号字节之间唯一的真正区别在于解释。它们都由8个二进制位组成,当使用write(int b)将int写入一个网络连接时,线缆上只会放8个二进制位。如果将一个超出0 ~ 255的int传入write(intb),将写入这个数的最低字节,其他3字节将被忽略(这正是将in t强制转换为byt e的结果)。

提示:不过,在极少数情况下,你可能会看到一些有问题的第三方类,在写超出0 ~ 255的值时,它们的做法有所不同,比如会抛出IllegalArgumentException异常或者总是写入255,所以尽可能要避免写超出0~255的int。

        例如,字符生成器协议定义了一个发出ASCII文本的服务器。这个协议最流行的变体是发送72个字符的文本行,其中包含可显示的ASCII字符。( 可显示的ASCII字符是33到126之间的字符,不包含各种空白符和控制字符)。第一行按顺序包含字符33到字符104。第二行包含字符34到字符105。第三行包含字符35到字符106。一直到第29行包含字符55到字符126。至此,字符将回绕,这样第30行包含字符56到字符126,加上字符33。各行用回车(ASCII 13)和换行(ASCII 10)结束。输出如下:

        由于ASCII是一个7位字符集,所以每个字符都作为单字节发送。因此,这个协议可以直接使用基本write()方法实现,如以下代码段所示:

        这里将一个OutputStream通过out参数传入generateCharacters()方法。一次向out写入1字节。这些字节作为33到126之间循环序列中的整数给出。这里的大部分运算都是让循环在这个范围内回绕。在写人每个72字符块之后,就向输出流写入一个回车和一个换行。然后计算下一个起始字符,重复这个循环。整个方法声明为抛出I0Exception异常。这一点很重要,因为字符生成器服务器只在客户端关闭连接时才会终止。Java代码会把它看作是一个I0Exception异常。

        一次写入1字节通常效率不高。例如,流出以太网卡的每个TCP分片包含至少40字节的开销用于路由和纠错。如果每字节都单独发送,那么与你预想的数据量相比,实际填入到网络中的数据可能会高出41倍以上!如果增加主机网络层协议的开销,情况可能更糟糕。因此,大多数TCP/IP实现都会在某种程度上缓存数据。也就是说,它们在内存中积累数据字节,只有积累到一定量的数据后,或者经过了一定的时间后,才将所积累的数据发送到最终目的地。不过,如果有多字节要发送,则一次全部发送不失为一个好主意。使用write(byte[] data)或write(byte[] data, int offset, int length)通 常比一次写入data数组中的1字节要快得多。例如,下面是generateCha racters()方法的一个实现,它将整行打包在1字节数组中,一次发送一行:

        计算何时写哪些字节的算法与前面的实现中是一样的。重要的区别在于这些字节在写入网络之前先打包到1字节数组中。还要注意计算的int结果在存储到数组前要转换为字节。这在前面的实现中是不必要的,因为单字节write()方法就声明为接受一个int作为参数。

        与在网络硬件中缓存一样,流还可以在软件中得到缓冲,即直接用Java代码缓存。一般说来,这可以通过把Buffered0utputStream或BufferedWriter串链到底层流上来实现,稍后将探讨这种技术。因此,在写入数据完成后,刷新(flush) 输出流非常重要。例如,假设已经向使用HTTP Keep-Alive的HTTP 1.1服务器写入了300字节的请求,通常你会等待响应,然后再发送更多的数据。不过,如果输出流有-一个1024字节的缓冲区,那么这个流在发送缓冲区中的数据之前会等待更多的数据到达。在服务器响应到达之前不会向流写入更多数据,但是响应永远也不会到来,因为请求还没有发送!图2-1 显示了这种两难境地。flush()方 法可以强迫缓冲的流发送数据,即使缓冲区还没有满,以此来打破这种死锁状态。

        不管你是否认为有必要,刷新输出流都很重要。取决于你如何控制流的引用,你可能知道流是否缓冲,也可能不知道(例如,不论你是否希望如此,System.out都会缓冲) 。如果刷新输出对于某个特定的流来说没有必要,那么它也只是个低成本的操作。不过,如果有必要刷新输出,就必须完成这个操作。需要刷新输出时如果未能做到,那么会导致不可预知、不可重现的程序挂起,如果你未能首先清楚地知道问题出在哪里,那么诊断起来将会非常困难。相应地,应当在关闭流之前立即刷新输出所有流。否则,关闭流时留在缓冲区中的数据可能会丢失。

        最后,当结束一个流的操作时,要通过调用它的close()方法将其关闭。这会释放与这个流关联的所有资源,如文件句柄或端口。如果流来自一个网络连接,那么关闭这个流也会终止这个连接。一旦输出流关闭,继续写入时就会抛出I0Exception异常。不过,有些流仍允许对这个对象做一些处理。 例如,关闭的ByteArray0utputStream仍然 可以转换为实际的字节数组,关闭的DigestOutputStream仍然 可以返回其摘要。

        在一个长时间运行的程序中,如果未能关闭一个流,则可能会泄漏文件句柄、网络端口和其他资源。因此,在Java 6和更早版本中,明智的做法是在一个finally块中 关闭流。为了得到正确的变量作用域,必须在try块之外声明流变量,但必须在try块内完成初始化。另外,为了避免NullPointerException异常,在关闭流之前需要检查流变量是否为null。最后,通常都希望忽略关闭流时出现的异常,或者最多只是把这些异常记录日志。例如:

        这个技术有时称为释放模式(dispose pattern),这对于需要在垃圾回收前先进行清理的对象是很常见的。你会看到,这个技术不仅用于流,还可以用于socket、通道、JDBC连接和语句等。

        Java 7引入了“带资 源的try”构造(try with resources),可以更简洁地完成这个清理。不需要在try块之外声明流变量,完全可以在try块的一个参数表中声明。例如,前面的代码段现在就变得简单多了:

        现在不再需要Finally子句。Java会 对try块参数表中声明的所有AutoCloseable对象自动调用close()。

提示:只要对象实现了Closeable接口,都可以使用“带资源的try"”构造,这包括几乎所有需要释放的对象。到目前为止,JavaMail Transport对象是我见过的唯一的例外。 这些对象还需要显式地释放。

二、输入流

Java的基本输人类是java.io.InputStream:

public abstract class InputStream

 这个类提供了将数据读取为原始字节所需的基本方法。这些方法包括:


    public abstract int read() throws IOException;

    public int read(byte b[]) throws IOException 

    public int read(byte b[], int off, int len) throws IOException 

    public long skip(long n) throws IOException 

    public int available() throws IOException 

    public void close() throws IOException {}

        InputStream的具体子类使用这些方法从某种特定介质中读取数据。例如,FileInputStream从文件中读取数据。TeInetInputStream从网络连接中读取数据。ByteArrayInputStream从字节数组中读取数据。但无论读取哪种数据源,主要只使用以上这6个方法。有时你不知道正在读取的流具体是何种类型。例如,TelnetInputStream类隐藏在sun.net包中,没有提供相关文档。java.net包中的很多方法都会返回这个类的实例(例如j ava.net.UR L的openStream()方法)。不过,这些方法声明为只返回InputStream,而不是更特定的子类TelnetInputStream。这又是多态在起作用。子类的实例可以透明地作为其超类的实例来使用。并不需要子类更特定的知识。

        InputStream的基本方法是没有参数的read()方法。这个方法从输入流的源中读取1字节数据,作为一个0到255的int返回。流的结束通过返回-1来表示。read()方法 会等待并阻塞其后任何代码的执行,直到有1字节的数据可供读取。输入和输出可能很慢,所以如果程序在做其他重要的工作,要尽量将I/O放在单独的线程中。

        read()方法声明为抽象方法,因为各个子类需要修改这个方法来处理特定的介质。例如,ByteArrayInputStream会 用纯Java代码实现这个方法,从其数组复制字节。不过,TelnetInputStream需要使用一一个原生库,它知道如何从主机平台的网络接口读取数据。

        下面的代码段从InputStream in中读取10字节,存储在byte数组input中。不过,如果检测到流结束,循环就会提前终止:

        虽然read()只读取1字节,但它会返回一个int。这样在把结果存储到字节数组之前就必须进行类型转换。当然,这会产生一个-128到127之间的有符号字节,而不是read()方法返回的0到255之间的一个无符号字节。不过,只要你清楚在做什么,这就不是大问题。你可以如下将一个有符号字节转换为无符号字节:

int i = b >= 0 ? b : 256 + b;

        与一次写入1字节的数据一样,一次读取1字节的效率也不高。因此,有两个重载的read()方法,可以用从流中读取的多字节的数据填充一个 指定的数组: read(byte[] input)和read(byte[] input, int offset, int length)。 第一个方法尝试填充指定的数组input。第二个方法尝试填充指定的input中从offset开始连续length字节的子数组。

        注意我说这些方法是在尝试填充数组,但不是一定会成功。尝试可能会以很多不同的方式失败。例如,你可能听说过,当你的程序正在通过DSL从远程Web服务器读取数据时,由于电话公司中心办公室的交换机存在bug,这会断开你与其他地方数百个邻居的连接。这会导致一个I0Exception异常。但更常见的是,读尝试可能不会完全失败,但也不会完全成功。可能读取到一些请求的字节,但未能全部读取到。例如,你可能尝试从一个网络连接中读取1024字节,现在实际上只有512字节到达,其他的仍在传输中。尽管它们最终会到达,但此时却不可用。考虑到这一点,读取多字节的方法会返回实际读取的字节数。例如,考虑下面的代码段:

byte[] input = new byte[1024];
int bytesRead = in.read(input);

        它尝试从InputStream in向数组input中读入 1024字节。不过,如果只有512字节可用,就只会读取这么多,bytesRead将会设置为512。为保证你希望的所有数据都真正读取到,要把读取方法放在循环中,这样会重复读取,直到数组填满为止。例如:

int bytesRead = 0;
int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead) {
    bytesRead += in.read(input, bytesRead, bytesToRead - bytesRead);
}

        这项技术对于网络流尤为重要。一般来讲如果一个文件完全可用,那么文件的所有字节也都可用。不过,由于网络要比CPU慢得多,所以程序很容易在所有数据到达前清空网络缓冲区。事实上,如果这两个方法尝试读取暂时为空但打开的网络缓冲区,它通常会返回0,表示没有数据可用,但流还没有关闭。这往往比单字节的read()方法要好,在这种情况下单字节方法会阻塞正在运行的线程。

        所有3个read()方法都用返回-1表示流的结束。如果流已结束,而又没有读取的数据,多字节read()方法会返回这些数据,直到缓冲区清空。其后任何一个read()方法调用会返回-1。-1永远不会放进数组中。数组中只包含实际的数据。前面的代码段中存在一个bug,因为它没有考虑所有1024字节可能永远不会到达的情况(这与前面所说的情况不同,那只是当时不可用,但以后所有字节总会到达)。要修复这个bug,需要先测试read()的返回值,然后再增加到bytesRead中。例如:

int bytesRead = 0;
int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead) {
    int result = in.read(input, bytesRead, bytesToRead - bytesRead);
    if (result == -1) break; //流结束
    bytesRead += result;
}

        如果不想等待所需的全部字节都立即可用,可以使用available()方法来确定不阻塞的情况下有多少字节可以读取。它会返回可以读取的最少字节数。事实上还能读取更多字节,但至少可以读取available()建议的字节数。例如:

int bytesAvailable = in.available();
byte[] input = new byte[bytesAvailable];
int bytesRead = in.read(input, 0, bytesAvailable);
//立即继续执行程序的其他部分...

        在这种情况下,可以认为bytesRead与bytesAvailable相等。不过,不能期望bytesRead大于0,有可能没有可用的字节。在流的最后,available()会返回0。一般来说,read(byte[] input, int offset, int length) 在流结束时返回-1;但如果length是0,那么它不会注意流的结束,而是返回0。在少数情况下,你可能希望跳过数据不进行读取。skip()方法会 完成这项任务。

        与读取文件相比,在网络连接中它的用处不大。网络连接是顺序的,一般情况下很慢,所以与跳过数据(不读取)相比,读取数据并不会多耗费太长时间。文件是随机访问的,所以要跳过数据,可以简单地实现为重新指定文件指针位置,而不需要处理要跳过的各字节。

        与输出流一样,一旦结束对输入流的操作,应当调用它的close()方法将其关闭。这会释放与这个流关联的所有资源,如句柄或端口。一旦输入流已关闭,进一步读取这个流会抛出IOException异常。不过,有些流可能仍然允许处理这个对象。例如,你通常会在读取了数据并关闭流之后才会从java. security . DigestInputStream获取消息摘要。

三、标记和重置

        InputStream类还有3个不太常用的方法,允许程序备份和重新读取已经读取的数据。这些方法是:

public void mark(int readAheadLimit)
public void reset() throws IoException
public boolean markSupported()

        为了重新读取数据,要用mark()方法标记流的当前位置。在以后某个时刻,可以用reset()方法把流重置到之前标记的位置。接下来的读取操作会返回从标记位置开始的数据。不过,不能随心所欲地向前重置任意远的位置。从标记处读取和重置的字节数由mark()的readAheadLimit参数确定。如果试图重置得太远,就会抛出IOException异常。此外,一个流在任何时刻都只能有一个标记。标记第二个位置会清除第一个标记。

        标记和重置通常通过将标记位置之后的所有字节存储在一个内部缓冲区中来实现。不过,不是所有输入流都支持这一点。在尝试使用标记和重置之前,要检查markSupported()方法是否返回true。如果返回true,那么这个流确实支持标记和重置。否则,mark()会什么都不做,而reset()将抛出一个IOException异常。

提示:在我看来,这是一个非常差的设计。实际上,不支持标记和重置的流比提供支持的更多。如果向抽象的超类附加一个功能,但这个功能对很多(甚至可能是大多数)子类都不可用,这就是一个很不好的想法。把这三个方法放在一个单独的接口中,由提供这个功能的类实现这个接口,这样做可能会更好。这个方法的缺点是不能在未知类型的任意输入流上调用这些方法,但实际上也不会这样做,因为并不是所有流都支持标记和重置。可以提供一个方法(如markSupported())在运行时进行检查,这是针对该问题的-一个更传统的非面向对象的解决方案。面向对象的方法是通过接口和类将其嵌入在类型系统中,这样就可以在编译时进行检查。

        java.io中仅有的两个始终支持标记的输入流类是BufferedInputStream和ByteArrayInputStream。而其他输入流( 如TelnetInputStream)如果先串链到缓冲的输入流时才支持标记。

四、过滤器流

        InputStream和0utputStream是相当原始的类。它们可以单个或成组地读/写字节,但仅此而已。要确定这些字节的含义(比如,它们是整数还是IEEE 754浮点数或是Unicode文本),这完全由程序员和代码来完成。不过,有一些极为常见的数据格式,如果在类库中提供这些数据格式的固定实现,会很有好处。例如,许多作为网络协议一部分传递的整数是32位big- endian整数。许多通过Web发送的文本是7位ASCII、8位Latin-1或多 字节UTF-8。许多由FTP传输的文件存储为zip格式。Java提供了很多过滤器类,可以附加到原始流中,在原始字节和各种格式之间来回转换。

        过滤器有两个版本:过滤器流以及阅读器和书写器。过滤器流仍然主要将原始数据作为字节处理,例如通过压缩数据或解释为二进制数字。阅读器和书写器处理多种编码文本的特殊情况,如UTF-8和ISO 8859-1。

        过滤器以链的形式进行组织,如图2-2所示。链中的每个环节都接收前一个过滤器或流的数据,并把数据传递给链中的下一个环节。在这个示例中,从本地网络接口接收到一个压缩的加密文本文件,在这里本地代码将这个文件表示为TelnetInputStream(TelnetInputStream没有相关文档提供说明)。通过一个BufferedInputStream缓冲这个数据来加速整个过程。由一个CipherInputStream将数据解密。再由一个GZIPInputStream解压解密后的数据。一个InputStreamReader将解压后的数据转换为Unicode文本。最后,文本由应用程序读取并处理。

        每个过滤器输出流都有与java.io.OutputStream相同的write()、close()和flush() 方法。每个过滤器输入流都有与java.io.InputStream相同的read()、close()和available()方法。有些情况下,如BufferedInputStream和Buffered0utputStream, 过滤器可能只有这些方法。过滤纯粹是内部操作,不提供任何新的公共接口。不过,在大多数情况下,过滤器流会增加-些公共方法提供额外的作用。有时除了平常的read()和write()方法之外,还需要使用这些方法,如PushbackInputStream的unread()方法。另外一些情况下,它们几乎完全代替了最初的接口。例如,PrintStream的write()方法就很少使用,而会使用它的print()和println()方法。

五、将过滤器串链在一起

        过滤器通过其构造函数与流连接。例如,下面的代码段将缓冲文件data.txt的输入。首先,创建一个FileInputStream对象fin,为此将文件名作为参数传递给FileInputStream构造函数。然后,通过将fin作为参数传递给BufferedInputStream构造函数来创建一个BufferedInputStream对象bin:

FileInputStream fin = new FileInputStream("data.txt");
BufferedInputStream bin = new BufferedInputStream(fin) ;

        在此之后,从文件data .txt中读取文件可能会同时使用fin和bin的read()方法。不过,如果混合调用连接到同一个源的不同流,这可能会违反过滤器流的一些隐含的约定。大多数情况下,应当只使用链中最后一个过滤器进行实际的读/写。要想在编写代码时尽量不带入这种bug,可以有意地重写底层输人流的引用。例如:

InputStream in = new FileInputStream("data.txt");
in = new BufferedInputStream(in);

        执行这两行代码后,再没有任何方法能访问底层的文件输入流了,所以也就不会不小心读取这个流而破坏缓冲区。这个示例之所以可行,原因是既然BufferedInputStream可以多态地用作InputStream的实例,所以没有必要区分InputStream和BufferedInputStream的方法。如果必须使用超类中没有声明的过滤器流的其他方法,可以直接在一个流中构建另一个流。例如:

Data0utputStream dout = new DataOutputStream(new Buffered0utputStream(
                                new FileOutputStream("data.txt")));

        虽然这些语句有些长,不过很容易将这条语句分成多行,像这样:

Data0utputStream dout = new Data0utputStream(
                            new BufferedOutputStream(
                            new FileOutputStream("data.txt")
                            ));

        这种连接是永久的。过滤器无法与流断开连接。

        有时可能会需要使用链中多个过滤器的方法。例如,在读取Unicode文本文件时,可能希望读取前3字节中的字节顺序标记,来判断文件是用big-endian UCS-2、little-endianUCS-2,还是用UTF-8编码的,然后选择与编码匹配的Reader (阅读器)过滤器。或者当连接Web服务器时,可能希望读取服务器发送的首部,找到Content-encoding(内容编码),然后用这个内容编码方式选取正确的Reader(阅读器)过滤器来读取响应主体。或者可能希望通过网络连接使用DataOutputStream发送浮点数,然后从DataOutputStream所链接的Diges toutputStream中获取一个MessageDigest。 在所有这些情况下,都需要保存和使用各个底层流的引用。不过,除了链中最后一个过滤器之外,无论如何你都不应该从其他的过滤器读取数据,或向其写入任何内容。

六、缓冲流

        Buffered0utputStream类将写入的数据存储在缓冲区中(一个名为buf的保护字节数组字段),直到缓冲区满或刷新输出流。然后它将数据一次全部写入底层输出流。如果一次写入多字节,这与多次写入少量字节(但字节加起来是一样的)相比,前者往往要快得.多。对于网络连接尤其是这样,因为每个TCP片或UDP包都有一定数量的开销,一般大约为40字节。这意味着,如果一次发送1字节,那么发送1K数据实际上需要通过线缆发送40K,而一次全部发送只需要发送1K多一点点数据。大多数网卡和TCP实现自身都提;供了一定程度的缓冲,所以实际的数量不会那么夸张。尽管如此,缓冲网络输出通常会带来巨大的性能提升。

        BufferedInputStream类也有一个作为缓冲区的保护字节数组,名为buf。当调用某个流的read()方法时,它首先尝试从缓冲区获得请求的数据。只有当缓冲区没有数据时,流才从底层的源中读取数据。这时,它会从源中读取尽可能多的数据存入缓冲区,而不管是否马上需要所有这些数据。不会立即用到的数据可以在以后调用read()时读取。当从本地磁盘中读取文件时,从底层流中读取几百字节的数据与读取1字节数据几乎一样快。因此,缓冲可以显著提升性能。对于网络连接,这种效果则不甚明显,在这里瓶颈往往是网络传送数据的速度,而不是网络接口向程序传送数据的速度或程序运行的速度。尽管如此,缓冲输人没有什么坏处,随着网络的速度加快会变得更为重要。
BufferedInputStream有两个构造函数,Buffered0utputStream也一样:

public BufferedInputStream( InputStream in)
public BufferedInputStream(InputStream in, int bufferSize)
public BufferedOutputStream(OutputStream out)
public BufferedOutputStream(OutputStream out, int bufferSize)

        第一个参数是底层流,可以从中读取未缓冲的数据,或者向其写入缓冲的数据。如果给出第二个参数,它会指定缓冲区中的字节数。否则,输入流的缓冲区大小设置为2048字节,输出流的缓冲区大小设置为512字节。缓冲区的理想大小取决于所缓冲的流是何种类型。对于网络连接,你会希望比一般的包大小更大-一些。不过,这很难预测,根据本地网络连接和协议的不同也有所区别。更快、更大带宽的网络倾向于使用更大的包,不过TCP片通常不会大于1K字节。

        BufferedInputStream没有声明自己的任何新方法。它只覆盖了InputStream的方法。它支持标记和重置。两个多字节read()方法尝试根据需要多次从底层输人流中读取数据,从而完全填充指定的数组或子数组。只有当数组或子数组完全填满、到达流的末尾或底层流阻塞而无法进一步读取时,这两个read()方法才返回。大多数输入流都不这样做。它们在返回前只从底层流或数据源中读取一次。

        Buffered0utputStream也没有声明自己的任何新方法。调用它的方法与调用任何输出流的方法是一样的。区别在于,每次写入会把数据放在缓冲区中,而不是直接放入底层的输出流。因此,需要发送数据时应当刷新输出流,这一点非常重要。

七、PrintStream

        PrintStream类是大多数程序员都会遇到的第一个过滤器输出流,因为System. out就是一个PrintStream.不过,还可以使用下面两个构造函数将其他输出流串链到打印流:

public PrintStream(OutputStream out)
public PrintStream(OutputStream out, boolean autoFlush)

        默认情况下,打印流应当显式刷新输出。不过,如果autoFlush参 数为true,那么每次写入1字节数组或换行,或者调用println()方法时,都会刷新输出流。

        除了平常的write()、fush()和close()方法, PrintStream还 有9个重载的print()方法和10个重载的println()方法:

public void print(boolean b)
public void print(char c)
public void print(int i)
public void print(long 1)
public void print(float f
public void print(double d)
public void print(char[] text)
public void print(String s)
public void print(Object o)
public void println()
public void println(boolean b)
public void println(char c)
public void println(int i)
public void println(long l)
public void println(float f)
public void println(double d)
public void println(char[] text)
public void println(String s)
public void println(object 0) 

        每个print()方法都将其参数以可预见的方式转换为-一个字符串,再用默认的编码方式把字符串写入底层输出流。println()方法也完成相同的操作,但会在所写的行末尾追加一个与平台有关的行分隔符。在UNIX (包括Mac OS X)下是换行符(\n) ,在MacOS9下是回车符(\r) ,在Windows下是回车/换行对(\r\n)。

警告: PrintStream是有害的,网络程序员应当像躲避瘟一样避开它!

        第一个问题是println()的输出是与平台有关的。取决于运行代码的机器,各行有时用换行符分隔,有时则用回车符或者回车/换行对来分隔。写人控制台时这不会产生问题,但对于编写必须遵循明确协议的网络客户端和服务器而言,这却是个灾难。大多数网络协议( 如HTTP和Gnutela)明确指定行应当以回车/换行对结束。使用println()写出的程序很有可能可以在Windows.上正常工作,但在UNIX和Mac.上无法工作。虽然许多服务器和客户端能够“宽容”地接受而且能处理不正确的行结束符,但偶尔也有例外。

        第二个问题是PrintStream假定使用所在平台的默认编码方式。不过,这种编码方式可能不是服务器或客户端所期望的。例如,一个接收XML文件的Web浏览器希望文件以UTF-8或UTF16方式编码,除非服务器另行要求。不过,一个使用PrintStream的Web服务器可能会从一个美国本地化环境的Windows系统发送CP1252编码的文件,或者从日本本地化环境的系统发送SJIS编码的文件,而不管客户端是否期望或理解这些编码方式。PrintStream不提供任何改变默认编码的机制。这个问题可以通过使用相关的PrintWriter类来修补。但是其他问题依旧。

        第三个问题是PrintStream吞掉了所有异常。这使得Pr intStream很适合作为教科书程序,如HelloWorld,因为要讲授简单的控制台输出,不用让学生先去学习异常处理和所有相关的知识。不过,网络连接不如控制台那么可靠。连接经常会由于网络拥塞、电话公司的错误、远程系统崩溃,以及很多其他原因而断开。网络程序必须准备处理数据流中意料之外的中断。要做到这一.点,就需要使用异常处理。不过,PrintStream捕获 了底层输出流抛出的所有异常。注意PrintStream中5个标准0utputStream方法的声明没有平常的throws IOException声明:

public abstract void write(int b)
public void write(byte[] data)
public void write(byte[] data, int offset, int length)
public void flush()
public void close()

        实际上,PrintStream要依靠一个过时的不充分的错误标志。如果底层流抛出一一个异常,就会设置这个内部错误标志。要由程序员使用checkError()方法来检查这个标志的值:

public boolean checkError()

        要对Pr intStream完成任何错误检查,代码必须显式地检查每一一个调用。此外,一旦出现错误,就没有办法重置这个标志再进行进一步 的错误检测。也没有关于这个错误的更多信息。简而言之,PrintStream提供的错误通知对于不可靠的网络连接来说还远远不够。

八、数据流

DataInputStream和Data0utputStream类提供了一些方法,可以用二进制格式读/写Java的基本数据类型和字符串。所用的二进制格式主要用于在两个不同的Java程序之间交换数据(可能通过网络连接、数据文件、管道或者其他中间介质)。输出流写人什么数据,输入流就能读取什么数据。不过,这碰巧与大多数交换二进制数的Internet协议所用的格式相同。例如,时间协议使用32位big endian整数,类似于Java的int数据类型。负载受控的网络元素服务使用32位IEEE 754浮点数,类似于Java的float数据类型 (这是有关联的,而不只是巧合。Java和大多数网络协议都是由UNIX程序员设计的,因此都会倾向于使用大多数UNIX系统中的常见格式)。然而,这并不适用于所有网络协议,所以请检查你使用的协议的具体细节。例如,网络时间协议(NTP) 会把时间表示为64位无符号定点数,前32位是整数部分,后32位是小数部分。这与所有常见编程语言中的基本数据类型都不相同,不过处理起来相当简单,至少对于NTP必须使用这种格式。

DataOutputStream类提供了下面11种方法,可以写入特定的Java数据类型:

public final void writeBoolean(boolean b) throws IOException
public final void writeByte(int b) throws I0Exception
public final void writeShort(int s) throws IoException
public final void writeChar(int c) throws IOException
public final void writeInt(int i) throws IoException
public final void writeLong(long l) throws IOException
public final void writeFloat(float f) throws IOExcept ion
public final void writeDouble(double d) throws I0Exception
public final void writeChars(String s) throws IoException
public final void writeBytes(String s) throws I0Exception
public final void writeUTF(String s) throws IOException

        所有数据都以big-endian格式写人。整数用尽可能少的字节写为2的补码。因此,byte会写为1字节,short写为2字节, int写为4字节, long写为8字节。浮点数和双精度数分别写为4字节和8字节的IEEE 754格式。布尔数写为1字节,0表示false,1表示true。 字符写为两个无符号字节。

        最后三个方法有些棘手。writeChars()方法 只是对String参数迭代(循环)处理,将各个字符按顺序写为一个2字节的big-endian Unicode字符(确切地讲是UTF-16码点)。writeBytes()方法迭代处理String参数,但只写入每个字符的低字节。因此,如果字符串中包含有Latin-1字符集以外的字符,其中的信息将会丢失。对于一些指定了ASCII编码的网络协议来说,这个方法或许有用,但多数情况下都应当避免使用。

        writeChars和writeBytes都不会对输出流的字符串的长度编码。因此,你无法真正区分原始字符和作为字符串-部分的字符。writeUTF()方法则包括了字符串的长度。它将字符串本身用Unicode UTF-8编码的一个变体进行编码。由于这个变体编码方式与大多数非Java软件有点不兼容,所以应当只用于与其他使用DataInputStream读取字符串的Java程序进行数据交换。为了与所有其他软件交换UTF-8文本,应当使用有适当编码的InputStreamReader (如果Sun当初把这个方法及相应的读取方法命名为writeString()和readString(),而不是writeUTF()和readUTF(),那就不会产生任何混淆了)。

        除了这些写入二进制数字和字符串的方法,DataoutputStream当然还有所有OutputStream类都有的平常的write()、flush( )和close()方法。DataInputStream与Data0utputStream是互补的。Data0utputStream写 入的每一种格式,DataInputStream都可以读取。此外,DataInputStream还 有通常的read()、available()、skip和close()方法, 以及读取整个字节数组和文本行的方法。有9个读取进制数据的方法,这些方法对应于DataoutputStream的11个方法(writeBytes ()或writeChars()没有相应的读取方法,这要通过一次读取1字节和字符来处理) :

public final boolean readBoolean() throws I0Exception
public final byte readByte() throws IOException
public final char readChar() throws IOExcept ion
public final short readShort() throws IOException
public final int readInt() throws IoException
public final long readLong() throws IoException
public final float readFloat() throws IoException
public final double readDouble() throws IOException
public final String readUTF() throws IOException

        此外,DataInputStream提供了两个方法,可以读取无符号字节和无符号短整数,并返回等价的int。Java没有这些数据类型,但在读取C程序写入的二进制数据时会遇到:

public final int readUnsignedByte() throws I0Exception
public final int readUnsignedShort() throws IoException

        DataInputStream有两个通常的多字节read()方法,可把数据读入一个数组或子数组,并返回读取的字节数。它还有两个readFully()方法,会重复地从底层输入流向一个数组读取数据,直到读取了所请求的字节数为止。如果不能读取到足够的数据,就会抛出IOException异常。如果你能提前知道要读取多少字节,这些方法尤其有用。例如,如果你已经从HTTP首部读取了Content-length (内容长度)字段,就能知道有多少字节的数据,这种情况下就可以很好地利用这些方法:

public final int read(byte[] input) throws IOException
public final int read(byte[] input, int offset, int length) throws I0Exception
public final void readFully(byte[] input) throws IOException
public final void readFully(byte[] input, int offset, int length) throws IOException

        最后,DataInputStream还提供了流行的readLine()方法,它读取用行结束符分隔的一行文本,并返回一个字符串:

public final String readLine() throws IOException

        不过,任何情况下都不要使用这个方法,不仅是因为它已被废弃,而且它还有bug。之所以将这个方法废弃,是因为在大多数情况下它不能正确地将非ASCII字符转换为字节。这个任务现在由BufferedReader类的readLine()方法来处理。不过,这两个方法都存在同一个隐含的bug:它们并不总能把-一个回车识别为行结束。实际上,readLine()只能识别换行或回车/换行对。在流中检测到回车时,readLine()在继续之前会等待, 查看下一个字符是否为换行。如果是换行,就抛掉这个回车和换行,把这一行作为String返回。如果不是换行,就抛掉这个回车,把这一行作 为String返回,刚读取的这个额外的字符会成为下一行的一部分。不过,如果回车是流的最后一个字符,那么readLine()会挂起,等待最后一个字符的出现,但这个字符永远也不会出现。

        这个问题在读取文件时不太明显,因为几乎可以肯定会有下一个字符:如果没有别的字符,那么会由-1表示流结束。不过,在持久的网络连接中(如用于FTP和新型HTTP的连接),服务器或客户端可能只是在最后一个字符之后停止发送数据,并等待响应,而不会真正关闭连接。如果幸运,最终可能某一端的连接超时,你将得到一个I0Exception异常,不过这可能至少要花费几分钟,而且会使你丢失流的最后一行数据。如果不够幸运,程序将永远挂起。

九、阅读器和书写器

        许多程序员在编码时有一个坏习惯,好像所有文本都是ASCII,或者至少是该平台的内置编码方式。虽然有些较老的、较简单的网络协议(如daytime、quote of the day和chargen)确实指定文本采用ASCII编码方式,但对于HTTP和其他很多更新的协议却不是这样,它们允许多种本地化编码,如K0I8- R西里尔文、Big-5中文和土耳其语使用的ISO8859-9。Java的内置字符集是Unicode的UTF- 16编码。当编码不再是ASCII时,如果假定字节和字符实质上是一样的,这也会出问题。因此,对应于输人和输出流类层次体系,Java提供了一一个基本上完整的镜像,用来处理字符而不是字节。

        这个镜像体系中,两个抽象超类定义了读/写字符的基本API。java. io. Reader类指定读取字符的API。java. io. Writer指定写字符的API。对应输入和输出流使用字节的地方,阅读器和书写器会使用Unicode字符。Reader 和Writer的具体子类允许读取特定的源和写人特定的目标。过滤器阅读器和书写器可以附加到其他阅读器或书写器上,以提供额外的服务或接口。

        Reader和Writer最重要的具体子类是InputStreamReader和0utputStreamWriter类。InputStreamReader类包含一个底层输入流,可以从中读取原始字节。它根据指定的编码方式,将这些字节转换为Unicode字符。OutputStreamWriter从运 行的程序中接收Unicode字符,然后使用指定的编码方式将这些字符转换为字节,再将这些字节写入底层输出流中。

        除了这两个类,java.io包还提供了几个原始阅读器和书写器类,它们可以读取字符而不需要一个底层输入流,这些类包括:

  • FileReader
  • FileWriter
  • StringReader
  • StringWriter
  • CharArrayReader
  • CharArrayWriter

        以上所列的前两个类可以处理文件,后四个由Java内部使用,所以在网络编程中不太常用。不过,除了构造函数不同,这些类与所有其他阅读器和书写器类一样,都有相同的公共接口。

十、书写器

        Writer类是java. io.0utputStream类的映射。它是一一个抽象类, 有两个保护类型的构造函数。与OutputStream类似, Writer类从不直接使用;相反,会通过它的某个子类以多态方式使用。它有5个write()方法,另外还有flush()和close()方法:

protected Writer()
protected Writer(Object lock)
public abstract void write(char[] text, int offset, int length) throws IOException
public void write(int c) throws I0Exception
public void write(char[] text) throws I0Exception
public void write(String s) throws IOException
public void write(String s, int offset, int length) throws IOException
public abstract void flush() throws IOException
public abstract void close() throws IOException

write(char[] text, int offset, int length)方 法是基础方法,其他四个write()都是根据它实现的。子类至少要覆盖这个方法以及flush()和close(),但是为了提供更高效的实现方法,大多数子类还覆盖了其他一些write()方法。例如,给定- -个Writer对象w,可以这样写,入字符串“Network ”

char[] network = {'N', 'e','t','w', 'o', 'r', 'k'};
w.write(network, 0,network . length);

也可以用其他write()方法完成同样的任务:

w. write(network);
for (int i = 0; i < network.length; i++) w.write(network[i]);
w.write("Network") ;
w.write("Network", 0, 7);

所有这些例子表述都是同样的事情,只不过方式有所不同。在任何给定情况下,选择使用哪个方法主要考虑是否方便,以及你有什么偏好。不过,这些代码写入多少字节以及写入哪些字节,则取决于w使用的编码方式。如果使用big -endian UTF- 16编码,那么它将依次写入下面14字节(以十六进制显示) :

00 4E 00 65 00 74 00 77 00 6F 00 72 00 6B

        另一方面,如果w使用little-endian UTF-16,则将写入下面14字节的序列: 

4E 00 65 00 74 00 77 00 6F 00 72 00 6B 00

如果w使用Latin-1、UTF-8或MacRoman, 则写入下面7字节的序列:

4E 65 74 77 6F 72 6B

        其他编码方式可能写入不同的字节序列。具体的输出取决于编码方式。书写器可以缓冲,有可能直接串链到BufferedWriter,也有可能间接链入(因为其底层输出流是缓冲的)。为了强制将-一个写 人提交给输出介质,要调用flush()方法:

w.flush();

        close()方法的行为与OutputStream的close()方法类似。close()刷新输出书写器,然后关闭底层输出流,并释放与之关联的所有资源:

public abstract void close() throws IOException

在书写器关闭后,进一步的写入会抛出IOException异常。

十一、OutputStreamWriter

OutputStreamWriter是Writer的最重要的具体子类。OutputStreamWriter会 从Java程序接收字符。它根据指定的编码方式将这些字符转换为字节,并写入底层输出流。它的构造函数指定了要写入的输出流和使用的编码方式:

public OutputStreamWriter(OutputStream out, String encoding)
        throws Unsuppor tedEncodingException

        JDK中包括一个Sun的native2ascii工具,其相关文档中列出了所有合法的编码方式。如果没有指定编码方式,就使用平台的默认编码方式。2013年,Mac上的默认编码方式是UTF-8,Linux. 上也大多如此。不过,如果本地操作系统配置为默认使用另外某个字符集,Linux上的默认编码方式可能有变化。在Windows上,默认编码方式会根据国家和配置而改变,但是在美国,Windows.上默认的编码方式往往是Windows- 1252,又叫做CP1252。默认字符集可能会在出乎意料的时候导致意外的问题。如果能明确地指定字符集,这往往比让Java为你选择一个字符集要好。例如,下面的代码段会用CP1253Windows Greek编码方式写入荷马史诗奧德赛的前几个词:

OutputStreamWriter W = new OutputStreamWriter(new FileOutputStream( "OdysseyB.txt"), "Cp1253");
w.write("Rμoζ δ’ηpuγEvELa 中avη po6oδdKtuλoC 'H6C");

        除了构造函数,OutputStreamWriter 只有通常的Writer方法(这些方法与所有Writer类中的用法相同),还有一个返回对象编码方式的方法:

public String getEncoding()

十二、阅读器

        Reader类是java. io.InputStream类的镜像。它是一个抽象类,有两个保护的构造函数。与InputStream和Writer类似,Reader类从不直接使用,只通过其子类来使用。它有三个read()方法,另外还有skip()、close()、 ready()、 mark()、reset()和markSupported()方法:

protected Reader()
protected Reader(0bject lock)
public abstract int read(char[] text, int offset, int length) throws I0Exception
public int read() throws IOException
public int read(char[] text) throws IOException
public long skip(long n) throws IOException
public boolean ready()
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws I0Exception
public abstract void close() throws IOException

        read(char[] text, int offset, int length) 方法是基础方法,其他两个read()方法都是根据它实现的。子类必须至少覆盖这个方法及close(),但是为了提供更高效的实现,大多数子类还会覆盖其他一些read()方法。

        由于与对应的InputStream类似,大多数方法都很容易理解。read()方法将一个Unicode字符作为一个int返回,可以是0到65 535之间的一个值,或者在流结束时返回-1 (理论上讲,它会返回一个UTF-16码点,不过这几乎等同于Unicode字符)。read(char[]text)方法尝试使用字符填充数组text,并返回实际读取的字符数,或者在流结束时返回-1。read(char[] text, int offset, int length)方 法尝试将length个字符读入text的子数组中(从从offset开始持续length个字符)。它也会返回实际读取的字符数,或者在流结束时返回-1。skip(long n)方法跳过n个字符。 mark()和reset()方法允许一些阅读器重置到字符序列中做标记的位置。markSupported()方法会告知阅读器是否支持标记和重置。close()方法会关闭阅读器和所有底层输入流,如果试图进一步 读取则会抛出IOException异常。

        尽管与InputStream非常相似,但也有所例外: Reader类有一个ready()方法,它与InputStream的available()的用途相同,但语义却不尽相同,尽管都涉及字节到字符转换。available()返回一个int,指定可以无阻塞地最少读取多少字节,但ready()只返回一个boolean,指示阅读器是否可以无阻塞地读取。问题在于,有些字符编码方式(如UTF-8)对于不同的字符会使用不同数量的字节。因此在实际从缓冲器区读取之前,很难说有多少个字符正在网络或文件系统的缓冲区中等待。

        InputStreamReader是Reader的最重要的具体子类。InputStreamReader从 其底层输入流(如FileInputStream或TelnetInputStream)中读取字节。它根据指定的编码方式将这些字节转换为字符,并返回这些字符。构造函数指定要读取的输入流和所用的编码方式:

public InputStreamReader (InputStream in)
public InputStreamReader(InputStream in, Str ing encoding)
throws UnsupportedEncodingException

        如果没有指定编码方式,就使用平台的默认编码方式。如果指定了一个未知的编码方式,会抛出UnsupportedEncodingException异常。

        例如,下面的方法将读取一个输入流,使用MacCyrillic编码方式将其全部转换为一个Unicode字符串:

public static String getMacCyrillicString(InputStream in)
                    throws IOException {
    InputStreamReader r = new InputStreamReader(in, "MacCyrillic");
    StringBuilder sb = new StringBuilder();
    int c
    while ((c = r.read()) != -1) sb.append((char) c);
    return sb. toString();
}

十三、过滤器阅读器和书写器

        InputStreamReader和0utputStreamWriter类就相当于输入和输出流之上的装饰器,把面向字节的接口改为面向字符的接口。完成之后,就可以将其他面向字符的过滤器放在使用java.io.FilterReader和java.io.FilterWriter类的阅读器或书写器上面。与过滤器流一样,有很多子类可以完成特定的过滤工作,包括:

  • BufferedReader
  • BufferedWriter
  • LineNumberReader
  • PushbackReader
  • PrintWriter

        BufferedReader和BufferedWriter类是基于字符的,对应于面向字节的BufferedInputStream和BufferedOutputStream类。BufferedInputStream和BufferedOutputStream中使用一一个内部字节数组作为缓冲区,相应地,BufferedReader和BufferedWriter使用一个内部字符数组作为缓冲区。

        当程序从BufferedReader读取时,文本会从缓冲区得到,而不是直接从底层输入流或其他文本源读取。当缓冲区清空时,将用尽可能多的文本再次填充,尽管这些文本不是全部都立即需要,这样可以使以后的读取速度更快。当程序写入一个BufferedWriter时,文本被放置在缓冲区中。只有当缓冲区填满或者当书写器显式刷新输出时,文本才会被移到底层输出流或其他目标,这使得写人也要快得多。

        BufferedReader和BufferedWriter也有与阅读器和书写器关联的常用方法,如read()、ready()、write()和close()。 这两个类都有两个构造函数,可以将BufferedReader或BufferedWriter串链到一个底层阅读器或书写器,并设置缓冲区的大小。如果没有设置大小,则使用默认的大小8192字符:

public BufferedReader(Reader in, int bufferSize)
public BufferedReader(Reader in)
public BufferedWriter(Writer out)
public BufferedWriter(Writer out, int bufferSize)

        例如,前面的getMacCyrillicString()示例效率不太高,因为它每次只读取一一个字符。由于MacCyrillic是一一个单字节的字符集,所以也是每次读取1字节。不过,通过将一个BufferedReader串链到InputStreamReader,会使它运行得更快,如下所示:

public static String getMacCyrillicString(InputStream in)
                        throws IOException
    Reader r = new InputStreamReader(in, "MacCyrillic");
    r = new BufferedReader(r, 1024);
    StringBuilder sb = new StringBuilder();
    int C;
    while ((C = r.read()) != -1) sb.append((char) c);
    return sb. toString();
}

        要让这个方法进行缓冲,只需要增加另外一行代码。算法的其他部分都不用改变,因为要用到的InputStreamReader方法只是Reader超类中声明的read()和close()方法,所有Reader子类都有这两个方法,BufferedReader 也不例外。BufferedReader类还有一个readLine()方法,它读取一行文本,并作为一个字符串返回:

public String readLine() throws IOException

这个方法可以替代DataInputStream中已经废弃的readLine()方法,它与该方法的行为基本相同。主要的区别在于,通过将BufferedReader串链到InputStreamReader,你可以采用正确的字符集读取行,而不是采用平台的默认编码方式。

这个BufferedWriter()类增加了一个其超类所没有的新方法,名为newline(),也用于写入一行:

public void newLine() throws IOException

        这个方法向输出插入一个与平台有关的行分隔符字符串。line. separator系统属性会确定这个字符串是什么:在UNIX和Mac OS X下可能是换行,在Mac OS 9下是回车,在Windows下是回车/换行对。由于网络协议一般会指定所需的行结束符,所以网络编程中不要使用这个方法,而应当显式地写入协议所需的行结束符。大多数情况下,所需的结東符都是回车/换行对。

十四、PrintWriter

        PrintWriter类用于取代Java 1 .0的PrintStream类,它能正确地处理多字节字符集和国际化文本。Sun最初计划废弃PrintStream而支持PrintWriter,但当它意识到这样做会使太多现有的代码失效(尤其是依赖于System. out的代码),就放弃了这种想法。尽管如此,新编写的代码还是应当使用PrintWriter而不是PrintStream。

        除了构造函数,PrintWriter类 也有与PrintStream儿乎相同的方法集。包括:

public PrintWriter(Writer out)
public PrintWriter(Writer out, boolean autoFlush)
public PrintWriter(0utputStream out )
public PrintWriter(OutputStream out, boolean autoFlush)
public void flush()
public void close()
public boolean checkError()
public void write(int c)
public void write(char[] text, int offset, int length)
public void write(char[] text)
public void write(String S, int offset, int length)
public void write(String s)
public void print(boolean b)
public void print(char c)
public void print(int i)
public void print(long l)
public void print(float f)
public void print(double d)
public void print(char[] text)
public void print(String s)
public void print(Object o)
public void println()
public void println(boolean b)
public void println(char c)
public void println(int i)
public void println(long l)
public void println(float f)
public void println(double d)
public void println(char[] text)
public void println(String s)
public void println(Object o)

        这些方法的行为大多与PrintStream中相同。只有4个write()方法有所例外,它们写入字符而不是字节。此外,如果底层的书写器能正确地处理字符集转换,那么PrintWriter的所有方法也能处理这种转换。这是对非国际化的PrintStream类的改进,但对于网络编程来说,仍然不太适合。很遗憾,PrintWriter 也存在困扰Pr intStream类的平台依赖性和错误报告信息量小等问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/786568.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【node.js】04-模块化

目录 一、什么是模块化 二、node.js中的模块化 1. node.js中模块的分类 2. 加载模块 3. node.js 中的模块作用域 4. 向外共享模块作用域中的成员 4.1 module对象 4.2 module.exports 对象 4.3 exports对象 5. node.js 中的模块化规范 一、什么是模块化 模块化是指解…

Grafana - TDEngine搭建数据监测报警系统

TDengine 与开源数据可视化系统 Grafana 快速集成搭建数据监测报警系统 一、介绍二、前置条件三、Grafana 安装及配置3.1 下载3.2 安装3.2.1 windows安装 - 图形界面3.2.2 linux安装 - 安装脚本 四、Grafana的TDEngine配置及使用4.1 登录4.2 安装 Grafana Plugin 并配置数据源4…

安卓版本的发展4-13

Android 4.4 KitKat 1、通过主机卡模拟实现新的 NFC 功能。 2、低功耗传感器&#xff0c;传感器批处理&#xff0c;步测器和计步器。 3、全屏沉浸模式&#xff0c;隐藏所有系统 UI&#xff0c;例如状态栏和导航栏。它适用于鲜艳的视觉内容&#xff0c;例如照片、视频、地图、…

结构型设计模式之装饰器模式【设计模式系列】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 设计模式系列 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everythi…

上海科技大学智能生活组齐聚合合信息,“沉浸式”体验人工智能产品

近期&#xff0c;上海科技大学组织本科生产业实践-校企联合人才培养活动&#xff0c;30余名学生组成的“智能生活组”实地参访人工智能及大数据科技企业上海合合信息科技股份有限公司&#xff08;简称“合合信息”&#xff09;。本次活动旨在通过项目体验、主题交流&#xff0c…

2023-7-24-第二十二式备忘录模式

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

VS Code 设置大小写转换快捷键

VS Code 设置大小写转换快捷键 前言&#xff1a;VS Code 没有默认的大小写转换快捷键&#xff0c;需要我们自己添加。 一 、打开快捷键设置面板 二、添加快捷键 在搜索框输入 “转换为大写”&#xff0c;如果您的VS Code没有汉化&#xff0c;此处输入“Transform to Uppercase…

vmware平台上虚拟机无法查看到WWID

需要在虚拟机中部署rac测试环境&#xff0c;创建虚拟机后无法查看到wwid [rootdb1 ~]# for i in cat /proc/partitions |awk {print $4} |grep sd; do echo "Device: $i WWID: /usr/lib/udev/scsi_id --page0x83 --whitelisted --device/dev/$i "; done |sort -k4 De…

Bootstrap每天必学之面板

Bootstrap每天必学之面板 1、面板 面板&#xff08;Panels&#xff09;是Bootstrap框http://架新增的一个组件&#xff0c;其主要作用就是用来处理一些其他组件无法完成的功能。同样在不同的版本中具有不同的源码&#xff1a; ☑ Less版本&#xff1a;对应的源码文件是 panel…

深度学习论文分享(五)DDFM: Denoising Diffusion Model for Multi-Modality Image Fusion

深度学习论文分享&#xff08;五&#xff09;DDFM: Denoising Diffusion Model for Multi-Modality Image Fusion 前言Abstract1. Introduction2. Background2.1. Score-based diffusion models2.2. Multi-modal image fusion2.3. Comparison with existing approaches 3. Meth…

解决ros-melodic-desktop-full(18.04)安装过程中未满足的依赖关系问题(注:也可以解决20.04noetic的)

自己安装火焰截图软件时使用sudo apt-get install flameshot时出现&#xff1a; 正在读取软件包列表... 完成 正在分析软件包的依赖关系树 正在读取状态信息... 完成 您也许需要运行“apt --fix-broken install”来修正上面的错误。 下列软件包有未满足的依赖关系&#xff1a;…

Vue组件通信原理及应用场景解析

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

SolidWorks打开step.格式文件提示“输入的文件名无效、无法发现、被锁住或为不兼容的类型”的解决办法

有时候用SolidWorks打开step.格式文件会提示“输入的文件名无效、无法发现、被锁住或为不兼容的类型”&#xff0c;从而无法正常打开此文件&#xff0c;如图&#xff1a; 目前小编找了两种解决这个问题的办法&#xff0c;供大家参考&#xff1a; 方法一&#xff1a; 打开Solid…

istio安装部署总结

istio安装部署总结 大纲 istio基础概念版本选择安装istio核心主件卸载istiokiali安装 istio基础概念 https://istio.io/latest/zh/docs/ 中文文档 istio是一个服务治理平台&#xff0c;治理服务间的访问&#xff0c;&#xff08;例如流量控制&#xff0c;安全策略&#xf…

第五讲:MySQL中DDL表的修改与删除

1、alter&#xff1a;改变 2、table&#xff1a;表 3、truncate&#xff1a;截断&#xff0c;删节 学习渠道&#xff1a;黑马程序员

如何创建高级 CSS 下拉菜单

效果展示 实现思路及部分代码 1、定义整体页面结构 从上述的效果展示图可以看出&#xff0c;页面的整体结构应该需要一个总菜单容器来装载父级菜单项&#xff0c;并且对应的父级菜单项应该有对应的菜单子项。子菜单是分类的话&#xff0c;我们还需要额外在扩展对应的容器来装…

mysql(由浅到深)

文章目录 1. 数据库分类与SQL分类2. SQL的数据类型3. DDL CURD3.1 库的操作3.2 表约束3.3 表的操作 4 DML CURD5. DQL &#xff08;数据查询语言&#xff09;5.1 单表查询5.2 聚合查询与分组查询5.3 多表查询与外键约束5.4 多表之间的连接查询5.4.1左链接查询5.4.2 右连接查询5…

FPGA图像处理仿真实验——均值滤波(FIFO)

之前的博客中用shift ram做的均值滤波&#xff0c;那篇文章里讲了原理&#xff0c;在这里不进行重复。考虑到shift ram的深度有限&#xff0c;在处理高分辨率图片时可能会收到限制&#xff0c;所以这次采用FIFO来进行均值滤波。FIFO可以看成是一个先进先出的堆栈&#xff0c;有…

TSINGSEE视频监控汇聚平台EasyCVR视频监控录像的3种方式

视频监控综合管理平台EasyCVR可以实现海量资源的接入、汇聚、计算、存储、处理等&#xff0c;平台具备轻量化接入能力&#xff0c;可支持多协议方式接入&#xff0c;包括主流标准协议GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Eho…