Chapter3 输入流
java.io.InputStream是所有输入流的抽象超类。 它声明了从流中读取字节数据所需的三种基本方法。 它还具有以下方法:关闭流,检查可读取多少字节数据,跳过输入,在流中定位并重置回该位置,以及确定是否支持标记和重置。
read( ) 方法
InputStream类的基本方法是read( )。 此方法读取数据的单个无符号字节,并返回无符号字节的整数值。 这是介于0到255之间的数字:
public abstract int read() throws IOException
InputStream是抽象的,read( )被声明为抽象方法; 因此,你永远不能直接实例化InputStream。 你始终使用其具体的子类之一。
以下代码从System.in输入流中读取10个字节,并将它们存储在int数组数据中:
int[] data = new int[10];
for(int i = 0;i<data.length;i++){
data[i] = System.in.read();
}
注意,read( )读取一个字节,但返回一个int。 如果要存储原始字节,则可以将int强制转换为一个字节。 例如:
byte[] b = new byte[10];
for (int i = 0; i < b.length; i++){
b[i] = (byte) System.in.read();
}
当然,这会产生一个有符号的字节,而不是read( )方法返回的无符号的字节(即,一个介于-128到127而不是0到255之间的字节)。 只要你清楚地知道自己的代码和使用签名或未签名的数据,就不会有任何麻烦。 可以将有符号字节转换回int,范围为0到255,如下所示:
int i = (b >= 0) ? b : 256+b;
调用read( )时,还必须捕获它可能抛出的IOException或声明你的方法将其抛出。 但是,如果read( )遇到输入流的末尾,则没有IOException。 在这种情况下,它返回-1。 你可以使用它作为标记来监视流的结束。 以下代码片段显示了如何捕获IOException并测试流的结尾:
try {
InputStream in = new FileInputStream("file.txt");
int[] data = new int[10];
for (int i = 0; i < data.length; i++){
int datum = in.read();
if (datum == -1) break;
data[i] = datum;
}
} catch (IOException ex){
System.err.println(ex.getMessage());
}
read( )方法通常会等待,以便获取数据字节。 大多数输入流不会超时。 (一些网络流是例外。)输入可能很慢,因此,如果程序正在做其他重要的事情,请尝试将I / O放在其自己的线程中。
示例3-1是一个程序,该程序从System.in中读取数据,并使用System.out.println( )打印在控制台上读取的每个字节的数值。
Example 3-1. StreamPrinter类
import java.io.*;
public class StreamPrinter{
public static void main(String[] args){
try {
while(true){
int datum = System.in.read();
if (datum == -1) break;
System.out.println(datum);
}
} catch(IOException ex){
System.err.println("Couldn't read from System.in!");
}
}
}
从流中读取数据块
输入和输出通常是程序中的性能瓶颈。从磁盘读取或写入到磁盘的速度可能比从存储器读取或写入到存储器的速度慢数百倍。 网络连接和用户输入甚至更慢。 尽管磁盘容量和速度随着时间的推移而增加,但它们从未与CPU速度保持同步。 因此,重要的是,尽量减少程序实际执行的读写次数。
所有输入流都具有重载的read( )方法,该方法将连续数据的块读入字节数组。 第一个变体尝试读取足够的数据以填充数组。 第二个变体尝试从数组的位置偏移处读取长度为字节的数据。 这些方法都不能保证读取所需的字节数。 两种方法都返回实际读取的字节数,或在流末尾返回-1。
public int read(byte[] data) throws IOException
public int read(byte[] data, int offset, int length) throws IOException
这些方法在java.io.InputStream类中的默认实现仅调用基本read( )方法足够多次以填充请求的数组或子数组。 因此,读取10个字节的数据所需的时间是读取1个字节的数据的10倍。 但是,大多数InputStream子类会使用更有效的方法(可能是本机方法)覆盖这些方法,这些方法可以从底层源中读取数据作为块。
例如,要尝试从System.in读取10个字节,可以编写以下代码:
try{
byte[] b = new byte[10];
System.in.read(b);
}catch(IOException ex){
System.err.println("Couldn't read from System.in!");
}
读取并不一定总能获得所需的字节数。 相反,没有什么可以阻止你尝试将更多数据读入数组。 如果这样做,则read( )抛出ArrayIndexOutOfBoundsException。 例如,以下代码重复循环,直到它填满数组或看到流的结尾:
try{
byte[] b = new byte[100];
int offset = 0;
while (offset < b.length){
int bytesRead = System.in.read(b, offset, b.length-offset);
if (bytesRead == -1) break;
offset += bytesRead;
}
}catch(IOException ex){
System.err.println("Couldn't read from System.in!");
}
计算可用字节
有时,在尝试读取字节之前先知道可以读取多少个字节是很方便的。 InputStream类的available( )方法告诉您可以读取多少字节而不会阻塞。 如果没有可供读取的数据,则返回0。
public int available() throws IOException
示例:
try{
byte[] b = new byte[100];
int offset = 0;
while (offset < b.length) {
int a = System.in.available( );
int bytesRead = System.in.read(b, offset, a);
if (bytesRead == -1) break; // end of stream
offset += bytesRead;
}catch(IOException ex){
System.err.println("Couldn't read from System.in!");
}
该代码中存在潜在的错误。 可用字节数可能超过了数组中容纳它们的空间。 一种常见的习惯用法是根据available( )返回的数量来调整数组的大小,如下所示:
try {
byte[] b = new byte[System.in.available( )];
System.in.read(b);
}catch (IOException ex) {
System.err.println("Couldn't read from System.in!");
}
如果你要执行一次读取,则此方法效果很好。 但是,对于多次读取,创建多个数组的开销过大。 仅当可用字节数多于数组容纳的字节数时,才应重新使用该数组并创建一个新的数组。
java.io.InputStream中的available( )方法始终返回0。子类应该覆盖它,但我见过一些没有。 你可能能够从基础流中读取更多字节,而不会受到超过available( )建议的阻塞; 你只是不能保证可以。 如果需要解决的话,请将输入放置在单独的线程中,以使被阻止的输入不会阻止程序的其余部分。
跳过(略过)字节
skip( )方法在输入中跳过一定数量的字节:
public long skip(long bytesToSkip) throws IOException
skip( )的参数是要跳过的字节数。 返回值是实际跳过的字节数,可能小于bytesToSkip。 如果遇到流的末尾,则返回−1。 参数和返回值均为long,因此skip( )可以处理非常长的输入流。 跳过通常比读取和丢弃不需要的数据要快。 例如,当输入流附加到文件时,跳过字节仅需要更改文件中的位置,而读取则涉及将字节从磁盘复制到内存中。 例如,要跳过以下输入流的80个字节:
try{
long bytesSkipped = 0;
long bytesToSkip = 80;
while(bytesSkipped < bytesToSkip){
long n = in.skip(bytesToSkip - bytesSkipped);
if ( n == -1) break;
bytesSkipped += n;
}
}catch(IOException ex){
System.err.println(ex);
}
关闭输入流
与输出流一样,关闭输入流以释放任何本机资源,例如文件句柄或流所保留的网络端口。 要关闭流,请调用其close( )方法:
public void close() throws IOException
一旦关闭了输入流,就不应再读取它。 大多数尝试这样做都会抛出IOException(尽管有一些例外)。
例如,并非所有流都需要关闭,System.in通常不需要关闭。 但是,与文件和网络连接关联的流在使用完后应关闭。 与输出流一样,最好在finally块中执行此操作,以确保关闭流,即使在打开流时抛出异常也是如此。 例如:
// Initialize this to null to keep the compiler from complaining
// about uninitialized variables
InputStream in = null;
try {
URL u = new URL("http://www.msf.org/");
in = u.openStream( );
// Read from the stream...
}catch (IOException ex) {
System.err.println(ex);
}finally {
if (in != null) {
try {
in.close( );
}catch (IOException ex){
System.err.println(ex);
}
}
}
如果你抛出异常而不进行捕获,则此策略可能会更短,更简单。 例如:
// Initialize this to null to keep the compiler from complaining
// about uninitialized variables
InputStream in = null;
try {
URL u = new URL("http://www.msf.org/");
in = u.openStream( );
// Read from the stream...
}finally {
if (in != null) in.close( );
}
标记和重置
能够读取一些字节然后备份并重新读取它们通常很有用。 例如,在Java编译器中,直到读取了太多字符,你才能确定是否要读取标记<,<<或<<< =。 知道已读取的令牌后,能够备份并重新读取该令牌将非常有用。 一些(但不是全部)输入流允许你标记流中的特定位置,然后返回到该位置。 java.io.InputStream类中的三个方法处理标记和重置:
public void mark(int readLimit)
public void reset() throws IOException
public boolean markSupported()
如果此流支持标记,则markSupported( )方法返回true,否则返回false。 如果不支持标记,则reset( )会引发IOException,而mark( )则不执行任何操作。 假设流确实支持标记,则mark( )方法将书签放置在流中的当前位置。 只要你读取的内容不超过readLimit字节,就可以稍后使用reset( )将流倒回到该位置。 在任何给定时间流中只能有一个标记。 标记第二个位置会删除第一个标记。
在java.io中仅有的两个输入流类始终支持标记,BufferedInputStream(其中System.in是实例)和ByteArrayInputStream。
但是,如果其他输入流(如DataInputStream)需要支持标记首先链接到缓冲的输入流中。
提示
这是一个真正的怪异设计。 将方法放入并非适用于所有子类的超类几乎总是一个坏主意。 解决此问题的正确方法是定义一个Resettable接口,该接口声明这三种方法,然后让子类根据自己的选择实现该接口。 然后,你可以通过一个简单的Resettable测试实例来判断是否支持标记和重置。 我在这里可以提供的解释是,这种设计是十年前在Java 1.0中发明的,当时并不是所有从事Java的人都完全熟练于面向对象的设计。
InputStream子类
InputStream的直接子类必须提供抽象read( )方法的实现。 它们也可能会覆盖某些非抽象方法。 例如,默认的markSupported( )方法返回false,mark( )不执行任何操作,reset( )抛出IOException。 任何允许标记和重置的类都必须重写这三种方法。 子类还应该重写available( )以返回0以外的值。此外,它们可以重写skip( )和其他两个read( )方法以提供更有效的实现。
示例3-2是一个名为RandomInputStream的简单类,它“读取”数据的随机字节。 这提供了有用的无限数据来源,你可以在测试中使用它们。 一个java.util.Random对象提供了数据。
Example 3-2. RandomInputStream类
package com.elharo.io;
import java.util.*;
import java.io.*;
public class RandomInputStream extends InputStream {
private Random generator = new Random( );
private boolean closed = false;
public int read( ) throws IOException {
checkOpen( );
int result = generator.nextInt( ) % 256;
if (result < 0) result = result;
return result;
}
public int read(byte[] data, int offset, int length) throws IOException {
checkOpen( );
byte[] temp = new byte[length];
generator.nextBytes(temp);
System.arraycopy(temp, 0, data, offset, length);
return length;
}
public int read(byte[] data) throws IOException {
checkOpen( );
generator.nextBytes(data);
return data.length;
}
public long skip(long bytesToSkip) throws IOException {
checkOpen( );
// It's all random so skipping has no effect.
return bytesToSkip;
}
public void close( ) {
this.closed = true;
}
private void checkOpen( ) throws IOException {
if (closed) throw new IOException("Input stream closed");
}
public int available( ) {
// Limited only by available memory and the size of an array.
return Integer.MAX_VALUE;
}
}
无参read( )方法返回一个无符号字节(0到255)范围内的随机整数。 另外两个read( )方法用随机字节填充数组的指定部分。 它们返回读取的字节数(在这种情况下为创建的字节数)。
高效的流式复印机
在示例3-3中,作为输入流和输出流的有用示例,我将展示一个StreamCopier类,该类将尽快在两个流之间复制数据。 (我将在后面的章节中重用该类。)此方法从输入流中读取并写入输出流,直到输入流用尽。 1K缓冲区用于尝试提高读取效率。 main( )方法通过从System.in中读取并复制到System.out中来为此类提供简单的测试。
Example 3-3. StreamCopier 类
package com.elharo.io;
import java.io.*;
public class StreamCopier {
public static void main(String[] args) {
try {
copy(System.in, System.out);
} catch (IOException ex) {
System.err.println(ex);
}
}
public static void copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024];
while (true) {
int bytesRead = in.read(buffer);
if (bytesRead == −1) break;
out.write(buffer, 0, bytesRead);
}
}
}
测试输出:
D:\JAVA\ioexamples\03> java com.elharo.io.StreamCopier this is a test
this is a test
0987654321
0987654321
^Z
直到每一行的末尾,输入才从控制台(DOS提示符)输入到StreamCopier程序。 由于我是在Windows上运行的,因此流结束字符为Ctrl-Z。 在Unix上,应该是Ctrl-D。