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

 

public abstract class InputStream

 

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

 

public abstract int read() throws IOException


pulic int read(byte[] input) throws IOException


pulic int read(byte[] input, int offset, int length) throws IOException


public long skip(long n) throws IOException


public int available()throws IOException


public int close()throws IOException

InputStream的具体子类使用这些方法从某种特定介质中读取数据。

例如:

FileInputStream从文件中读取数据。

TelnetInputStream从网络连接中读取数据。

ByteArrayInputStream从字节数组中读取数据。

但无论哪种数据源主要只使用以上这六个方法。

有时候,你不知道正在读取的流具体是什么类型。例如,TelnetInputStream类隐藏在sun.net包中没有提供相关文档。java.net包中有很多方法都会返回这个类的实例(例如:java.net.URL的openStream()方法)。不过,这些方法声明会只返回InputStream,而不是特定的子类TelnetInputStream。这又是多态在起作用。子类的实例可以透明地作为超类的实例来使用。并不需要子类特定的知识。

 

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 < input . length; i++){

    int b = in.read();

    if(b == -1){

        break;

    }

    input[i] = (byte) b;

}

 

虽然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 dffset, int length)。第一个方法尝试填充指定得数组input。第二个方法尝试填充指定的input数组中从offset开始连续length字节的子数组。

 

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

 

byte[] input = new byte[1024];

int byteRread = in.read(input);

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

int bytesRead = 0;

int bytesToRead = 1024;

byte[] input = new byte[bytesToPead];

while(byteRead < 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.avaliable();


byte[] input = new byte[bytesAvailable];

int byteRead = in.read(input, 0, bytesAvailable);

在这种情况下,可以认为bytresRead与bytesAvaliable相等。不过不能期望bytesRead大于0,有可能没有可用的字节。在流的最后,avaliable会返回0。一般来说,read(byte[] inpue, int offset, int length)在流结束之前返回-1;但是如果length是0,那么它不会注意流的结束,而是返回0。

 

在少数情况下,你可以希望跳过数据不进行读取。skip()方法会完成这项任务。与读取文件相比它在网络连接中用处不大。网络连接是顺序的,一般情况下都会很慢,所以与跳过数据相比,读取文件并不会耗费太久时间。文件访问是随机的,所以要跳过数据,可以简单实现为重新指定文件指针位置,而不再需要处理要跳过的各个字节。

 

与输出流一样,一旦结束对输入流的操作,应当调用它的close()方法进行关闭。

 

标记和重置

 

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

这些方法是:

 

public void mark(int readAheadLimmit)


public void reset() throws IOException


public bolean markSupported()

 

为了重新读取数据,要用mark()方法标记流的当前位置。在以后的某个时刻,可以用reset()方法把流重置到之前标记的位置上。接下来的读取操作会返回从标记位置开始的数据。

不过,不能随心所欲的向前标记任意远的位置。从标记处读取重置的字节数由mark的readAheadLimit参数确定。如果试图标记的太远,就会抛出IOException异常。

此外,一个流任何时刻都只能有一个标记,标记第二个时第一个会清除。

 

标记和重置通常通过将标志位置之后的所有字节存储到一个内部缓存区来实现。不过,不是所有的流都支持这一点。

在尝试使用标记重置的时候,要检查markSupported方法是否都返回true。如果返回truue,那么这个流确实支持标记和重置。

否则mark()什么也不会去做,而reset()会抛出IOException 异常。

 

java.io中仅有两个始终支持标的输入流是BufferedInputStream和ByteArrayInputStream。

而其他流如果先串连到缓冲的输入流才支持标记。

 

一个文件复制的例子:

package com.hello.java04;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

public class FileCopy {

      public static void main(String[] args) {

            

            byte[] buffer=new byte[1024];

            int numberRead = 0;

            FileInputStream input = null;

            FileOutputStream output = null;

            try {

                  input = new FileInputStream("E:/text/1.txt");

                  

                  output = new FileOutputStream("E:/text/2.txt");

                  try {

                        while((numberRead=input.read(buffer))!=-1 ){

                              

                              output.write(buffer, 0, numberRead);

                        }

                        

                  } catch (IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                  }

            } catch (FileNotFoundException e) {

                  // TODO Auto-generated catch block

                  e.printStackTrace();

            }

            finally{

                  try {

                        input.close();

                        output.close();

                  } catch (IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                  }

                  

            }

            

            

      }

}