《Java I/O》Chapter4

Chapter4 文件流

到目前为止,本书中的大多数示例都使用了System.in和System.out流。这些示例很方便,但是在现实生活中,你通常会将流附加到数据源,例如文件和网络连接。 java.io.FileInputStream和java.io.FileOutputStream类是java.io.InputStream和java.io.OutputStream的具体子类,提供用于在文件中读取和写入数据的方法。他们不提供文件管理功能,例如确定文件是否可读或可写,或者将文件从一个目录移动到另一个目录。为此,你可能需要跳到第17章,它讨论File类本身以及Java处理文件的方式。

读取文件
java.io.FileInputStream是java.io.InputStream的具体子类。它提供了连接到特定文件的输入流。 FileInputStream具有所有常规的输入流方法,例如read( ),available( ),skip( )和close( ),它们与其他任何输入流完全一样。 FileInputStream( )具有三个构造函数,它们仅在指定要读取的文件的方式上有所不同:

public FileInputStream(String fileName) throws IOException
public FileInputStream(File file) throws FileNotFoundException
public FileInputStream(FileDescriptor fdObj) 

第一个构造函数使用包含文件名的字符串。第二个构造函数使用java.io.File对象。第三个构造函数使用java.io.FileDescriptor对象。要读取文件,只需将文件名传递给FileInputStream( )构造函数即可。然后像往常一样使用read( )方法。例如,以下代码片段读取文件README.TXT,然后将其打印在System.out上:

try {
   FileInputStream fis = new FileInputStream("README.TXT");
   for (int n = fis.read(); n !=1; n = fis.read( )) {
        System.out.write(n);
   }
} catch (IOException ex) {
   System.err.println(ex);
}
System.out.println();

Java在当前工作目录中查找文件。通常,就是你键入java program_name开始运行程序时所在的目录。你可以通过从当前工作目录向文件传递完整或相对路径来打开其他目录中的文件。例如,无论哪个目录是当前目录,都要读取文件/etc/hosts,你都可以这样做:

FileInputStream fis = new FileInputStream("/etc/hosts"); 

文件名取决于平台,因此应尽可能避免使用硬编码的文件名。此示例取决于Unix样式的路径名。尽管保证可以在其他平台(例如Windows或Mac OS 9)上运行。使用文件名创建FileInputStream违反了Sun的“ 100%纯Java”规则。某些运行时环境(例如Apple的Macintosh Java运行时)包括一些额外的代码,可以将Unix样式的文件名转换为本地样式。但是,为了最大程度地了解跨平台,应改为使用File对象。它们可以直接按照第17章中所述的文件名创建,由用户通过诸如Swing JFileChooser之类的GUI提供,或者由分散在API和类库中的各种方法返回。在很多时候,使用File对象的代码更容易适应意外的文件系统约定。一个特别重要的技巧是通过为每个目录依次附加新的File对象来创建多段路径,如下所示:

File root = new File("/");
File dir = new File(root, "etc");
File child = new File(dir, "hosts");
FileInputStream fis = new FileInputStream(child);

但是,这仍然假设文件系统的根名为“ /”,在非Unix系统上不太可能是真的。最好使用File.listRoots( )方法:

File[] roots = File.listRoots();
File dir = new File(roots[0], "etc");
File child = new File(dir, "hosts");
FileInputStream fis = new FileInputStream(child);

但是,尽管此代码更不受平台限制,但仍采用特定的文件布局结构。这不仅可能因平台而异,而且可能因一台PC到下一台PC而不同,即使是运行相同操作系统的PC也如此。为了提高鲁棒性(健壮性),你将希望通过调用适合于本地系统的方法至少获得一个目录(如果不是完整的文件)。可能性包括:

  • 要求用户使用Swing JFileChooser选择文件。
  • 要求用户使用AWT FileDialog选择文件。
  • 向第三方库(例如MRJ Adapter的SpecialFolder)询问已知位置,例如首选项文件夹或桌面文件夹。
  • 使用File.createTempFile( )方法创建一个临时文件。
  • 通过System.getProperty(“ user.home”)查找用户的主目录。
  • 使用System.getProperty(“ user.dir”)查找当前的工作目录。

此列表并不详尽。 还有其他方法。 哪种合适取决于用例。 这些方法的详细信息将在以后的章节中介绍。

如果构造FileInputStream对象时你要读取的文件不存在,则构造方法将抛出FileNotFoundException(java.io.IOException的子类)。 例如,如果由于某种其他原因而无法读取文件,则当前进程没有对该文件的读取许可权,并且引发了其他类型的IOException。

示例4-1从命令行读取文件名,然后将命名文件复制到System.out。 上一章中的示例3-3中的StreamCopier.copy( )方法进行了实际的读取和写入。 注意,该方法并不关心输入是来自文件还是进入控制台。 无论它复制的输入和输出流的类型如何,它都可以工作。 对于将要引入的其他流,包括创建StreamCopier时甚至不存在的流,它将同样有效。

Example 4-1. FileDumper 程序

import java.io.*;
import com.elharo.io.*;
public class FileTyper {
  public static void main(String[] args) throws IOException {
      if (args.length != 1) {
          System.err.println("Usage: java FileTyper filename");
          return;
      }
      typeFile(args[0]);
  }
  public static void typeFile(String filename) throws IOException {
      FileInputStream fin = new FileInputStream(filename);
      try {
         StreamCopier.copy(fin, System.out);
      }finally{
         fin.close();
      }
  }
}

通常不允许不信任的代码读取或写入文件。 如果小程序尝试创建FileInputStream,则构造方法将抛出SecurityException。 FileInputStream类具有InputStream超类中未声明的一种方法:getFD( )。 公共最终FileDescriptor getFD( )引发IOException此方法返回与此流关联的java.io.FileDescriptor对象。 FileDescriptor对象将在第17章中讨论。 现在,你可以使用此对象创建另一个文件流。 尽管很少有必要同时打开多个输入流到同一文件,但是这是可能的。 每个流都维护一个单独的指针,该指针指向文件中的当前位置。 从文件读取不会以任何方式更改文件。 写入文件是另一回事,你将在下一节中看到。

写文件
java.io.FileOutputStream类是java.io.OutputStream的具体子类,提供连接到文件的输出流。 此类具有所有通常使用的输出流方法,例如write( ),flush( )和close( ),它们与其他任何输出流完全一样地使用。

FileOutputStream( )具有三个主要的构造函数,主要区别在于文件的指定方式:

public FileOutputStream(String filename) throws IOException
public FileOutputStream(File file) throws IOException
public FileOutputStream(FileDescriptor fd) 

第一个构造函数使用包含文件名的字符串。 第二个构造函数使用java.io.File对象; 第三个构造函数使用java.io.FileDescriptor对象。 要将数据写入文件,只需将文件名传递给FileOutputStream( )构造函数,然后照常使用write( )方法。 如果文件不存在,则所有三个构造函数都将创建它。 如果该文件确实存在,则其中的任何数据都将被覆盖。

第四个构造函数还允许你指定在将数据写入文件之前应删除文件的内容(append == false)还是将数据添加到文件末尾(append == true):

public FileOutputStream(String name, boolean append) throws IOException 

其他三个构造函数创建的输出流将简单地覆盖该文件。 他们没有提供将数据追加到文件的选项。 Java在当前工作目录中查找文件。 您可以通过从当前工作目录向文件传递完整或相对路径来写入其他目录中的文件。 例如,要将数据附加到\Windows\java\javalog.txt文件,无论当前目录是哪个,你都可以这样做:

FileOutputStream fout = new FileOutputStream("/Windows/java/javalog.txt", true); 

尽管Windows使用反斜杠作为目录分隔符,但Java仍然希望你像在Unix中一样使用正斜杠。 硬编码的路径名危险地依赖于平台。 使用此构造函数可将你的程序自动分类为不纯Java。 与输入流一样,危险程度稍低的替代方法一次构建一个File对象,如下所示:

File[] roots = File.listRoots( )
File windows = new File(roots[0], "Windows");
File java = new File(windows, "java");
File javalog = new File(java, "javalog.txt");
FileInputStream fis = new FileInputStream(javalog);

通常也不允许不可信的代码写文件。 如果小程序尝试创建FileOutputStream,则构造方法将引发SecurityException。

FileOutputStream类具有一种未在java.io.OutputStream中声明的方法:getFD( )。

public final FileDescriptor getFD( ) throws IOException 

此方法返回与此流关联的java.io.FileDescriptor对象。

**示例4-2 **从命令行读取两个文件名,然后将第一个文件复制到第二个文件中。 上一章中的示例3-3中的StreamCopier类进行实际的读写。

Example 4-2. FileDumper 程序

import java.io.*;
import com.elharo.io.*;
public class FileCopier {
  public static void main(String[] args) {
    if (args.length != 2) {
       System.err.println("Usage: java FileCopier infile outfile");
    }
    try {
       copy(args[0], args[1]);
    }catch (IOException ex) {
       System.err.println(ex);
    }
  }
  public static void copy(String inFile, String outFile) throws IOException {
    FileInputStream fin = null;
    FileOutputStream fout = null;
    try {
       fin = new FileInputStream(inFile);
       fout = new FileOutputStream(outFile);
       StreamCopier.copy(fin, fout);
    }finally {
      try {
        if (fin != null) fin.close( );
      }catch (IOException ex) {
      
      }
      try {
        if (fout != null) fout.close( );
      }catch (IOException ex) { 

      }
    }
  }
} 

由于我们不再写System.out或从System.in读取数据,因此请务必在完成后关闭流。 这对于finally子句很有用,因为无论读写是否成功,我们都需要确保关闭文件。
Java在关闭文件方面比大多数语言都更好。 只要VM(虚拟机)不会异常终止,程序退出时文件就会关闭。 不过,如果此类类是在长期运行的程序(例如网络服务器)中使用的,那么等到程序退出并不是一个好主意; 其他线程和进程可能需要访问文件。

**示例4-2 **有一个错误:如果输入和输出文件相同,则该程序的运行状况不佳。虽然在复制之前比较两个文件名很简单,但这还不够安全。一旦考虑了别名,快捷方式,符号链接和其他因素,单个文件可能会具有多个名称。要完全解决此问题,必须等到第17章(在此讨论规范路径和临时文件)。

文件查看器,第1部分
我经常发现能够打开任意文件并以任意方式解释它很有用。最常见的是,我想以文本形式查看文件,但偶尔将其解释为十六进制整数,IEEE 754浮点数据或其他内容很有用。在本书中,我将开发一个程序,使你可以打开任何文件并以多种不同方式查看其内容。在每一章中,我都会在程序中添加一个部分,直到其完全起作用。由于这只是程序的开始,因此重要的是,使代码保持通用性和适应性。

**例4-3 **在main( )方法中从命令行读取一系列文件名。每个文件名都传递给一个打开文件的方法。读取文件的数据并将其打印在System.out上。数据在System.out上的确切打印方式由命令行开关确定。如果用户选择文本格式(a),则该数据将被假定为Latin-1文本,并将被打印为char。如果用户选择十进制转储(d),则每个字节应打印为0到255之间的无符号十进制数字,第16行。例如:

000 234 127 034 234 234 000 000 000 002 004 070 000 234 127 098

前导零对于打印的字节值和每一行保持恒定的宽度。 对于十六进制转储格式(h),每个字节应打印为两个十六进制数字。 例如:

CA FE BA BE 07 89 9A 65 45 65 43 6F F6 7F 8F EE E5 67 63 26 98 9E 9C

十六进制编码更容易,因为每个字节总是正好是两个十六进制数字。 静态Integer.toHexString( )方法将每个读取的字节转换为两个十六进制数字。

文本格式是默认格式,是最容易实现的格式。 仅通过将输入数据复制到控制台即可完成此转换。

Example 4-3. FileDumper 程序

import java.io.*;
import com.elharo.io.*;

public class FileDumper {
    public static final int ASC = 0;
    public static final int DEC = 1;
    public static final int HEX = 2;

    public static void main(String[] args) {
        if (args.length < 1) {
            System.err.println("Usage: java FileDumper [-ahd] file1 file2...");
            return;
        }
        int firstArg = 0;
        int mode = ASC;
        if (args[0].startsWith("-")) {
            firstArg = 1;
            if (args[0].equals("-h")) mode = HEX;
            else if (args[0].equals("-d")) mode = DEC;
        }
        for (int i = firstArg; i < args.length; i++) {
            try {
                if (mode == ASC) dumpAscii(args[i]);
                else if (mode == HEX) dumpHex(args[i]);
                else if (mode == DEC) dumpDecimal(args[i]);
            } catch (IOException ex) {
                System.err.println("Error reading from " + args[i] + ": " + ex.getMessage());
            }
            if (i < args.length - 1) { // more files to dump
                System.out.println("\r\n-\r\n");
            }
        }
    }

    public static void dumpAscii(String filename) throws IOException {
        FileInputStream fin = null;
        try {
            fin = new FileInputStream(filename);
            StreamCopier.copy(fin, System.out);
        } finally {
            if (fin != null) fin.close();
        }
    }

    public static void dumpDecimal(String filename) throws IOException {
        FileInputStream fin = null;
        byte[] buffer = new byte[16];
        boolean end = false;
        try {
            fin = new FileInputStream(filename);
            while (!end) {
                int bytesRead = 0;
                while (bytesRead < buffer.length) {
                    int r = fin.read(buffer, bytesRead, buffer.length bytesRead);
                    if (r == -1){
                        end = true;
                        break;
                    }
                    bytesRead += r;
                }
                for (int i = 0; i < bytesRead; i++) {
                    int dec = buffer[i];
                    if (dec < 0) dec = 256 + dec;
                    if (dec < 10) System.out.print("00" + dec + " ");
                    else if (dec < 100) System.out.print("0" + dec + " ");
                    else System.out.print(dec + " ");
                }
                System.out.println();
            }
        } finally {
            if (fin != null) fin.close();
        }
    }

    public static void dumpHex(String filename) throws IOException {
        FileInputStream fin = null;
        byte[] buffer = new byte[24];
        boolean end = false;
        try {
            fin = new FileInputStream(filename);
            while (!end) {
                int bytesRead = 0;
                while (bytesRead < buffer.length) {
                    int r = fin.read(buffer, bytesRead, buffer.length bytesRead);
                    if (r == -1){
                        end = true;
                        break;
                    }
                    bytesRead += r;
                }
                for (int i = 0; i < bytesRead; i++) {
                    int hex = buffer[i];
                    if (hex < 0) hex = 256 + hex;
                    if (hex >= 16) System.out.print(Integer.toHexString(hex) + " ");
                    else System.out.print("0" + Integer.toHexString(hex) + " ");
                }
                System.out.println();
            }
        } finally {
            if (fin != null) fin.close();
        }
    }
}

当FileDumper用于以十六进制格式转储其自己的.class文件时,它将产生以下输出:

D:\JAVA\ioexamples\04> java FileDumper h FileDumper.class ca fe ba be 00 00 00 2e 00 78 0a 00 22 00 37 09 00 38 00 39 08 00 3a 0a
00 3b 00 3c 08 00 3d 0a 00 3e 00 3f 08 00 40 0a 00 3e 00 41 08 
00 3b 00 3c 08 00 3d 0a 00 3e 00 3f 08 00 40 0a 00 3e 00 41 08 00 42 0a
00 21 00 43 0a 00 21 00 44 0a 00 21 00 45 09 00 38 00 46 08 00 47 07 00
48 0a 00 0f 00 49 0a 00 4a 00 4b 07 00 4c 0a 00 3b 00 4d 0a 00 0f 00 4e
... 

在后面的章节中,我将添加图形用户界面以及文件中数据的更多可能解释,包括浮点数,大尾数和小尾数整数以及各种文本编码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值