Chapter 4. File Streams

Until now, most of the examples in this book have used the streams System.in and System.out. These are convenient for examples, but in real life, you’ll more commonly attach streams to data sources like files and network connections. The java.io.FileInputStream and java.io.FileOutputStream classes, which are concrete subclasses of java.io.InputStream and java.io.OutputStream, provide methods for reading and writing data in files. What they don’t provide is file management, like finding out whether a file is readable or writable or moving a file from one directory to another. For that, you may want to flip forward to Chapter 17, which talks about the File class itself and the way Java works with files.

Reading Files

java.io.FileInputStream is a concrete subclass of java.io.InputStream. It provides an input stream connected to a particular file. FileInputStream has all the usual methods of input streams, such as read( ), available( ), skip( ), and close( ), which are used exactly as they are for any other input stream. FileInputStream( ) has three constructors, which differ only in how the file to be read is specified:

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

The first constructor uses a string containing the name of the file. The second constructor uses a java.io.File object. The third constructor uses a java.io.FileDescriptor object.

To read a file, just pass the name of the file into the FileInputStream( ) constructor. Then use the read( ) method as normal. For example, the following code fragment reads the file README.TXT, then prints it on 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 looks for files in the current working directory . Generally, this is the directory you were in when you typed javaprogram_name to start running the program. You can open a file in a different directory by passing a full or relative path to the file from the current working directory. For example, to read the file /etc/hosts no matter which directory is current, you can do this:

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

Filenames are platform-dependent, so hardcoded filenames should be avoided wherever possible. This example depends on a Unix-style pathname. It is not guaranteed to work on other platforms such as Windows or Mac OS 9, though it might. Using a filename to create a FileInputStream violates Sun’s rules for “100% Pure Java.” Some runtime environments such as Apple’s Macintosh Runtime for Java include extra code to translate from Unix-style filenames to the native style. However, for maximum cross-platform awareness, you should use File objects instead. These can be created directly from filenames as described in Chapter 17, supplied by the user through a GUI such as a Swing JFileChooser, or returned by various methods scattered throughout the API and class libraries. Much of the time, code that uses a File object adapts more easily to unexpected filesystem conventions. One particularly important trick is to create multisegment paths by successively appending new File objects for each directory like so:

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

However, this still assumes that the root of the filesystem is named “/”, which isn’t likely to be a true on a non-Unix system. It’s better to use the File.listRoots( ) method:

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

However, although this code is more platform independent, it still assumes a particular file layout structure. This can vary not just from platform to platform, but from one PC to the next, even those running the same operating system. For more robustness, you’ll want to get at least a directory, if not a complete file, by invoking a method that adapts to the local system. Possibilities include:

  • Ask the user to choose a file with a Swing JFileChooser.

  • Ask the user to choose a file with an AWT FileDialog.

  • Ask a third-party library such as MRJ Adapter’s SpecialFolder for a known location such as the preferences folder or the desktop folder.

  • Create a temporary file with the File.createTempFile( ) method.

  • Find the user’s home directory with System.getProperty("user.home").

  • Find the current working directory with System.getProperty("user.dir").

This list is not exhaustive; there are other approaches. Which one is appropriate depends on the use case. Details of these approaches are addressed in future chapters.

If the file you’re trying to read does not exist when the FileInputStream object is constructed, the constructor throws a FileNotFoundException (a subclass of java.io.IOException). If for some other reason a file cannot be read—for example, the current process does not have read permission for the file—some other kind of IOException is thrown.

Example 4-1reads a filename from the command line, then copies the named file to System.out. The StreamCopier.copy( ) method from Example 3-3 in the previous chapter does the actual reading and writing. Notice that that method does not care whether the input is coming from a file or going to the console. It works regardless of the type of the input and output streams it’s copying. It will work equally well for other streams still to be introduced, including ones that did not even exist when StreamCopier was created.

Example 4-1. The FileDumper program
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( );
    }
  }
}

Untrusted code is not usually allowed to read or write files. If an applet tries to create a FileInputStream, the constructor will throw a SecurityException.

The FileInputStream class has one method that’s not declared in the InputStream superclass: getFD( ).

public final FileDescriptor getFD( ) throws IOException

This method returns the java.io.FileDescriptor object associated with this stream. FileDescriptor objects are discussed inChapter 17. For now, all you can do with this object is use it to create another file stream.

It is possible to open multiple input streams to the same file at the same time, though it’s rarely necessary to do so. Each stream maintains a separate pointer that points to the current position in the file. Reading from the file does not change the file in any way. Writing to the file is a different story, as you’ll see in the next section.

Writing Files

The java.io.FileOutputStream class is a concrete subclass of java.io.OutputStream that provides output streams connected to files. This class has all the usual methods of output streams, such as write( ), flush( ), and close( ), which are used exactly as they are for any other output stream.

FileOutputStream( ) has three main constructors, differing primarily in how the file is specified:

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

The first constructor uses a string containing the name of the file; the second constructor uses a java.io.File object; the third constructor uses a java.io.FileDescriptor object. To write data to a file, just pass the name of the file to the FileOutputStream( ) constructor, then use the write( ) methods as usual. If the file does not exist, all three constructors will create it. If the file does exist, any data inside it will be overwritten.

A fourth constructor also lets you specify whether the file’s contents should be erased before data is written into it (append == false) or whether data is to be tacked onto the end of the file (append == true):

public FileOutputStream(String name, boolean append) throws IOException

Output streams created by the other three constructors simply overwrite the file; they do not provide an option to append data to the file.

Java looks for files in the current working directory. You can write to a file in a different directory by passing a full or relative path to the file from the current working directory. For example, to append data to the \Windows\java\javalog.txt file no matter which directory is current, you would do this:

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

Although Windows uses a backslash as the directory separator, Java still expects you to use a forward slash as in Unix. Hardcoded pathnames are dangerously platform-dependent. Using this constructor automatically classifies your program as impure Java. As with input streams, a slightly less dangerous alternative builds a File object a piece at a time like so:

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);

Untrusted code is normally not allowed to write files either. If an applet tries to create a FileOutputStream, the constructor throws a SecurityException.

The FileOutputStream class has one method that’s not declared in java.io.OutputStream: getFD( ).

public final FileDescriptor getFD( ) throws IOException

This method returns the java.io.FileDescriptor object associated with this stream.

Example 4-2 reads two filenames from the command line, then copies the first file into the second file. The StreamCopier class from Example 3-3 in the previous chapter does the actual reading and writing.

Example 4-2. The FileDumper program
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) { }
    }
  }
}

Since we’re no longer writing to System.out and reading from System.in, it’s important to make sure the streams are closed when we’re done. This is a good use for a finally clause, as we need to make sure the files are closed whether the reads and writes succeed or not.

Java is better about closing files than most languages. As long as the VM doesn’t terminate abnormally, the files will be closed when the program exits. Still, if this class is used inside a long-running program like a web server, waiting until the program exits isn’t a good idea; other threads and processes may need access to the files.

Example 4-2 has one bug: the program does not behave well if the input and output files are the same. While it would be straightforward to compare the two filenames before copying, this is not safe enough. Once aliases, shortcuts, symbolic links, and other factors are taken into account, a single file may have multiple names. The full solution to this problem will have to wait until Chapter 17, where I discuss canonical paths and temporary files.

File Viewer, Part 1

I often find it useful to be able to open an arbitrary file and interpret it in an arbitrary fashion. Most commonly, I want to view a file as text, but occasionally it’s useful to interpret it as hexadecimal integers, IEEE 754 floating-point data, or something else. In this book, I’m going to develop a program that lets you open any file and view its contents in a variety of different ways. In each chapter, I’ll add a piece to the program until it’s fully functional. Since this is only the beginning of the program, it’s important to keep the code as general and adaptable as possible.

Example 4-3 reads a series of filenames from the command line in the main( ) method. Each filename is passed to a method that opens the file. The file’s data is read and printed on System.out. Exactly how the data is printed on System.out is determined by a command-line switch. If the user selects text format (-a), the data will be assumed to be Latin-1 text and will be printed as chars. If the user selects decimal dump (-d), then each byte should be printed as unsigned decimal numbers between 0 and 255, 16 to a line. For example:

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

Leading zeros maintain a constant width for the printed byte values and for each line. For hex dump format (-h), each byte should be printed as two hexadecimal digits. For example:

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

Hexadecimal encoding is easier, because each byte is always exactly two hex digits. The static Integer.toHexString( ) method converts each byte read into two hexadecimal digits.

Text format is the default and is the simplest to implement. This conversion can be accomplished merely by copying the input data to the console.

Example 4-3. The FileDumper program
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( );
    }
  }
}

When FileDumper is used to dump its own .class file in hexadecimal format, it produces the following output:

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 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
...

In later chapters, I’ll add a graphical user interface and many more possible interpretations of the data in the file, including floating-point, big- and little-endian integer, and various text encodings.

Get Java I/O, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.