引用:http://blog.sina.com.cn/s/blog_66e177dd0100h79m.html
无论二进制文件还是文本文件,都可以用文件输入流 java.io.FileInputStream 以字节的方式进行读取操作。如果想以字符的形式读取文本文件,则应该使用 java.io.FileReader 。
[注:从本质上说,文件都是以字节码(即所谓的二进制码)的形式存在的。如果这些字节码表示的是某种字符集的一种编码格式,如ASCII, UTF8, GB2312 等等,则习惯上把这个文件称为文本文件。 java.io.FileReader 除了提供文件读取功能外,还内置了解码功能,因此说,如果读取文本文件,java.io.FileReader 更方便些。(关于 FileReader 的使用,请参见本站相关文章。)]
java.io.FileInputStream 类继承和重写了抽象类 java.io.InputStream。他们两个都是从最早的JDK-1.0就已经存在的类。
[父类 InputStream ]:
抽象类 InputStream 是所有字节输入流的基类,它共定义了一个构造方法和九个方法。构造方法什么也没做。在九个方法中,有,
public abstract int read() throws IOException, 这个方法读取字节流中下一个字节。读取的字节值是作为 int 类型返回的。实际返回值的范围是 0 ~ 255 ,而不同于基本数据类型 byte 的范围,-128 ~ +127。如果所有数据都读完了(即所谓的到达流终点),再调用这个方法,则返回-1。这是判定流中所有数据读取完毕的唯一方式。对文件输入流来说,返回-1意味着整个文件已经读取完毕了。这个方法是阻塞的,直到输入端有数据可读、到达流终端、或者有异常抛出。调用这个方法时,需要捕获和处理 IOException。
这个方法是InputStream 类唯一的抽象方法,需要具体类实现。具体如何使用可参看下面示例程序。
public int read(byte[] b) throws IOException, 与上面方法不同的是,这个方法一次可以读取多个字节,并放在缓存数组 b 中。需要注意的是,数组 b 一定是(由用户)已经创建好的对象实例。否则,这个方法调用失败,并抛出NullPointerException。
这个方法的返回值是实际的读取字节个数或-1。-1表示到达流终点,这一点和上面的方法类似,这也是用于判定是否读完的唯一方式。不同的是在以前的每次读取中,返回值代表实际读取的长度,用户应该对这个长度进行判断或累加,来获取实际读取的总数据长度,否则可能出现把缓冲中原有的数据误认为这次读取数据的情况。一般来说,如果用长度为 m 的缓存来读取长度为 n 个字节的文件,则总读取次数为n/m + 2 次,最后一次返回-1,倒数第二次返回值是 n%m,而倒数第三次直至正数第一次返回值是m。这个方法是阻塞的,直到输入端有数据可读、到达流终端、或者以异常抛出。InputSteam 中这个方法的实现是调用下面的read方法:read(b, 0, b.length)。
public int read(byte[] b, int off, int len) throws IOException,这个方法与上面方法类似,但用户可以指定读取的长度和缓存的起始位置。最多读取不是数组的长度了,而是len。如果缓存的起始位置off 加上 读取的长度 len 超过了缓存的长度, 则调用不成功并抛出IndexOutOfBoundsException 异常。如果 off 或 len 小于0, 也抛出这个异常。同样,数组 b 一定是(由用户)已经创建好的对象实例。否则,这个方法调用失败,并抛出NullPointerException。
这个方法的返回值是实际的读取字节个数或-1。同上面方法一样。
InputStream 中这个方法的实现是调用int read()方法。具体子类应该重写这个方法,提供更有效率的实现。
FileInputStream 重写了这三个 read 方法。
下面用一个例程演示 InputStream.read() 和 InputStream.read(byte[] b) 的使用:
// TestFileInputStream.java
import java.io.*;
public class TestFileInputStream
{
public static void testRead()
{
try{
FileInputStream fi = new FileInputStream("TestFileInputStream.java");
int i = fi.read();
int allRead = 0;
while(i != -1) // 判断文件读完的条件
{
System.out.print((char)i); // 注意:这里简单地把读到的字节转为字符输出,不适用于所有情况。
allRead ++;
i = fi.read();
}
System.out.println();
fi.close(); // 注意:如果用户忘记关闭打开的文件,系统可以通过 finalyze 方法在垃圾回收的时候替用户关闭这个流。
// 虽然如此,用户应该显示的调用这个方法。
System.out.println(allRead + " read in all. ");
}catch(IOException e)
{
e.printStackTrace();
}
}
public static void testReadBytes()
{
try{
FileInputStream fi = new FileInputStream("TestFileInputStream.java");
byte[] buffer = new byte[256]; //必须用户自己创建一个buffer。
int read = fi.read(buffer);
int allRead = 0;
while(read != -1) // 判断文件读完的条件
{
allRead += read;
System.out.println(read + " read ... and available is " + fi.available());
read = fi.read(buffer);
}
fi.close();
System.out.println(allRead + " read in all.");
}catch(IOException e)
{
e.printStackTrace();
}
}
public static void main(String[]args)
{
testRead();
testReadBytes();
}
}
public void mark(int readlimit) 和 public void reset() throws IOException
在用户读取流数据的过程中,可以使用 mark 方法来做一个标记,标记之后的数据可以被重复读取。何时来重复读取取决于 reset 方法。mark 中的参数用于限定最大重复读取的字节数。
标记使用的一般过程:如果 markSupported
返回 true
, 输入流将通过某种方式“记住”mark 方法调用之后用户使用read方法读取的字节,并在reset 方法调用后,把“记住”的这些子节提供给用户(用户仍然继续使用read 方法)。如果“记忆”的字节超过了 readlimit
则超出的部分不会被记忆。
mark 没有定义异常,即使标记不成功用户也不会知道。但reset定义了IOException 异常,如果调用这个方法前没有做过标记或者标记已经无效了,则抛出这个异常。标记无效的具体情况由子类定义。
public boolean markSupported()
并不是所有的输入流都支持标记。用户应该首先调用这个方法来判断一下,只有支持的情况下,调用mark 或 reset 方法才有意义。
InputStream 的实现中,这个方法返回false,不支持标记!
java.io.FileInputStream 没有重写这三个方法,意味着文件输入流也不支持标记功能。因此,关于标记,这里暂不举例,可以参考本站其他相关文章。
public int available() throws IOException
这个流中还有多少子节没有读?可以用这个方法获取。需要注意的是,针对“阻塞流”,这个方法返回的是“直到下一次阻塞,流中可以读取的字节”。
InputStream 中这个方法的实现总是返回0。具体子类应该重写这个方法。java.io.FileInputStream 重写了这个方法。
上面的例子里演示了这个方法的使用。
public void close() throws IOException
这个方法用于关闭流。虽然,如果用户忘记调用这个方法,系统可以通过 finalyze 方法在该 FileInputStream 对象垃圾回收的时候替用户关闭这个流。但用户应该养成良好的习惯显式的调用这个方法!
InputStream 中这个方法的实现什么也没做。具体子类应该重写这个方法。java.io.FileInputStream 重写了这个方法。 用于真正关闭相应的文件资源。
public long skip(long n) throws IOException
之所以称为流,是因为数据是顺序读取的。如果用户想跳过一定数量的字节,则可以通过这个方法。一般情况下,返回值应该和输入值相同,即代表实际跳过的字节数,但如果达到文件尾,实际的跳过值可能小于输入值。
InputStream 通过内部使用一个数组来缓存跳过的数组来实现这个方法。具体子类应该重写这个方法,提供更高效率的实现。java.io.FileInputStream 重写了这个方法。
[FileInputStream 的其他方法]:
java.io.FileInputStream 除了重写上述 InputStream 9 个方法中 6 个方法之外,还提供了额外三个方法:
protected void finalize() throws IOException 。这个方法其实是重写了 Object 的这个方法(Object 是所有Java 类(包括数组类型)的“根级”父类)。这个方法是被 Java 虚拟机中的垃圾回收器调用的。用于 Java 对象已经不存在了但它占有的资源没有释放的情况,即最后的释放资源的机会。FileInputStream 实现中,这个方法主要是调用了 close() 方法。上面例子中,如果用户没有调用 close() 方法,系统就通过这个方法在垃圾回收的时候关闭文件。但写程序时应该养成显式关闭文件的习惯。
public final FileDescriptor getFD() throws IOException。 这个方法返回 FileDescriptor 对象,这个对象表示这个文件流对应的文件系统中的实际文件。
关于 FD 的使用,参见下面示例程序。
public FileChannel getChannel() 这是在 JDK1.4 中引入的方法。用于支持New IO 的特性。这个方法返回 FileChannel 对象,这个对象表示这个文件对应的文件系统中的通道。关于Channel 和 New IO,请参阅本站其它文章。
[关于 FileInputStream 的构造]:
FileInputStream 提供了三个构造方法:
FileInputStream(File file) throws FileNotFoundException 和 FileInputStream(String name) throws FileNotFoundException 都是通过实际文件路径(或其标识的File对象)来创建文件流。需要注意的是,这两个构造方法中有打开文件的操作,因此,如果文件不存在,则抛出FileNotFoundException 异常。如果文件由于安全保护而不允许读取,则抛出 SecurityException 异常。
public FileInputStream(FileDescriptor fdObj) 这是用已经打开的文件来创建一个新的文件流,因此这个构造方法里没有打开文件操作。输入的 FD 对应的一定是一个已经打开的文件。
下面的示例代码演示了用 FileDescriptor 构造文件流对象,以及它与上两种方式的不同。
// TestFileDescriptor
import java.io.*;
public class TestFileDescriptor
{
public static void main(String[]args)
{
test1();
test2();
test3();
}
public static void test1()
{
try{
FileInputStream fi1 = new FileInputStream("TestFileDescriptor.java");
FileInputStream fi2 = new FileInputStream("TestFileDescriptor.java");
FileDescriptor fd1 = fi1.getFD();
FileDescriptor fd2 = fi2.getFD();
System.out.println("got " + fd1); //结果证明:即使打开同一个文件创建文件流,他们的FileDescriptor 对象也是不同的。事实上,在文件输入流的构造方法中创建了该流关联的FD。
System.out.println("got " + fd2); //
if (fd1!=null)
System.out.println("valid " + fd1.valid());
if (fd2!=null)
System.out.println("valid " + fd2.valid()); // 都是有效的
fi1.close();
fi2.close();
}catch(Exception e)
{
e.printStackTrace();
}
}
public static void test2()
{
try{
FileInputStream fi1 = new FileInputStream("TestFileDescriptor.java");
FileInputStream fi2 = new FileInputStream("TestFileDescriptor.java");
int b1 = fi1.read();
int b2 = fi2.read();
System.out.println("b1 = " + (char)b1); // 这个例子演示对同一个文件创建的两个流是独立的,
System.out.println("b2 = " + (char)b2); //互相不影响
fi1.close();
int b3 = fi2.read(); //一个流关闭不影响另一个流的读取
System.out.println("b3 = " + (char)b3);
fi2.close();
}catch(Exception e)
{
e.printStackTrace();
}
}
public static void test3()
{
try{
FileInputStream fi1 = new FileInputStream("TestFileDescriptor.java");
FileInputStream fi2 = new FileInputStream(fi1.getFD()); // 用 FD 构造不同于其他两种构造方法,这种情况下,两个流共用一个 FD
int b1 = fi1.read();
int b2 = fi2.read();
System.out.println("b1 = " + (char)b1);
System.out.println("b2 = " + (char)b2); //结果说明:两个流实际是一个流(流对象不同,FD 相同)。
fi1.close();
int b3 = fi2.read(); // 这里会抛出异常,原因是 FD 标识的文件已关闭。
System.out.println("b3 = " + (char)b3);
fi2.close();
}catch(Exception e)
{
e.printStackTrace();
}
}
}
[关于 FileDescriptor]:
通过上面示例,我们初步理解了实体文件,文件描述(FileDescriptor), 和文件流之间的关系。文件描述其实是操作系统层面的一个概念。(而流更多的可以理解成 Java 层面的概念。)FD 是一个句柄 (handle) 用于指代与平台相关的文件结构。它可以看作是文件操作中 Java 代码和底层 C 代码之间指代文件的一个桥梁。一般来说这个对象由系统创建并维护,Java 程序员不该直接实例化这个类。
FD 不仅用于输入流的创建,也可用于输