Java的基本输入类是 java.io.InputStream:
public abstract class InputStream
这个类提供了将数据读取为原始字节所需的基本方法。这些方法包括:
public abstract int read() throws IOExceptionpublic int read(byte[] input) throws IOExceptionpublic int read(byte[] input,int offset,int length) throws IOExceptionpublic long skip(long n) throws IOExceptionpublic int available() throws IOExceptionpublic void close() throws IOException
InputStream的具体子类使用这些方法从某种特定介质中读取数据。例如,FileInputStream从文件中读取数据。TelnetInputStream从网络连接中读取数据。ByteArrayInputStream从字节数组中读取数据。但无论读取哪种数据源,主要只使用以上这6个方法。
InputStream的基本方法是没有参数的read()方法。这个方法从输入流的源中读取1字节数据,作为一个0到255的int返回。流的结束通过返回 -1 来表示。read()方法会等待并阻塞其后任何代码的执行,直到有1字节的数据可供读取。输入和输出可能很慢,所以如果程序有做其他重要的工作,要尽量将 I/O 放在单独的线程中。
read()方法声明为抽象方法,因为各个子类需要修改这个方法来处理特定的介质。例如,ByteArrayInputStream会用纯java代码实现这个方法,从其数组复制字节。不过,TelnetInputStream需要使用一个原生库,它知道如何从主机平台的网络接口读取数据。
下面的代码段从InputStream in中读取10字节,存储在byte数组input中。不过,如果检测到流结束,循环就会提前终止:
byte[] input = new byte[10];for(int i=0;i
虽然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,这会断开你与其他地方数百个邻居的连接。这会导致一个IOException异常。但更常见的是,读尝试可能不会完全失败,但也不会完全成功。可能读取到一些请求的字节,但未能全部读取到。例如,你可能尝试从一个网络连接中读取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中。我们来看一个比较完整的例子,如下:
package test;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;public class InputStream2 { public static void main(String[] args) { try { readByte("F:/apk_decompile/1-page/1.java/java/a/a.java"); } catch (IOException e) { e.printStackTrace(); } } public static void readByte(String fileUrl) throws IOException{ int bytesToRead = 1024; byte[] input = new byte[bytesToRead]; try(InputStream in = new FileInputStream(new File(fileUrl))){ int bytesRead = 0; while(bytesRead < bytesToRead){ int result = in.read(input,bytesRead,bytesToRead - bytesRead); if(result == -1) break; //主要注意这里 bytesRead += result; } } //把读取结果打印输出 try(OutputStream out = System.out){ out.write(input); out.write('\r'); //回车 out.write('\n'); //换行 out.flush(); } }}
如果不想等待所需的全部字节都立即可用,可以使用 available() 方法来确定不阻塞的情况下有多少字节可以读取。它会返回可以读取的最少字节数。事实上还能读取更多字节,但至少可以读取available()建立的字节数。例如:
int tytesAvailable = in.available(); //网络IO可能用得多些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异常。
标记和重置
InputStream类还有3个不太常用的方法。允许程序标记和重新读取数据。这些方法:
public void mark(int readAheadLimit)public void reset() throws IOExceptionpublic boolean markSupported()
为了重新读取数据,要用mark()方法标记流的当前位置。在以后某个时刻,可以用reset()方法把流重置到之前标记的位置。接下来的读取操作会返回从标记位置开始的数据。不过,不能随心所欲地向前重置任意远的位置。从标记处读取和重置的字节数由mark()的readAheadLimit参数确定。此外,一个流在任何时候都只能有一个标记,标记第二个位置会清除第一个标记。我们来看看例子:
package test;import java.io.ByteArrayInputStream;import java.io.IOException;import java.io.InputStream;public class InputStream3 { public static void main(String[] args) { try { readByte(); } catch (IOException e) { e.printStackTrace(); } } public static void readByte() throws IOException{ int m = 0; byte[] bli = new byte[]{1,2,3,4,5,125,126,127,-1,-2,-3,-4,-5}; try(InputStream input = new ByteArrayInputStream(bli)){ for(int i=0;i5 就以i值作为标记位 if(i > 5 && input.markSupported()) { m = i; input.mark(m); break; } int k = input.read(); if(k != -1) System.out.println(k); } System.out.println("---------下面是重置到标记位重新执行----------"); input.reset(); //重置到标记位 int s = 0; while(s != -1){ s = input.read(); if(s != -1) System.out.println(s); } } }}
标记和重置通常通过将标记位置之后的所有字节存储在一个内部缓冲区中来实现。不过,不是所有输入流都支持这一点。在尝试使用标记和重置之前,要检查markSupported()方法是否返回true。如果返回true,那么这个流确实支持标记和重置。否则,mark()会什么都不做,而reset()将抛出一个IOException异常。
提示:在我看来,这是一个非常差的设计。实际上,不支持标记和重置的流比提供支持的更多。如果向抽象的超类附加一个功能,但这个功能对很多(甚至可能是大多数)子类都不可用,这就是一个很不好的想法。把这三个方法放在一个单独的接口中,由提供这个功能的类实现这个接口,这样做可能会更好。
注:java.io中仅有的两个始终支持标记的输入流类是 BufferedInputStream和 ByteArrayInputStream。而其他输入流(如:TelnetInputStream)如果先串链到缓冲的输入流时才支持标记。