Files

Unless otherwise restricted, a Java application can read and write to the host filesystem with the same level of access as the user who runs the Java interpreter. Java applets and other kinds of untrusted applications can, of course, be restricted by the security policy and cut off from these services. We’ll discuss applet access at the end of this section. First, let’s take a look at the tools for basic file access.

Working with files in Java is still somewhat problematic. The host filesystem lies outside of Java’s virtual environment, in the real world, and can therefore still suffer from architecture and implementation differences. Java tries to mask some of these differences by providing information to help an application tailor itself to the local environment; we’ll mention these areas as they occur.

The java.io.File Class

The java.io.File class encapsulates access to information about a file or directory entry in the filesystem. It can be used to get attribute information about a file, list the entries in a directory, and perform basic filesystem operations like removing a file or making a directory. While the File object handles these tasks, it doesn’t provide direct access for reading and writing file data; there are specialized streams for that purpose.

File constructors

You can create an instance of File from a String pathname:

File fooFile = new File( "/tmp/foo.txt" );  
File barDir = new File( "/tmp/bar" );

You can also create a file with a relative path:

File f = new File( "foo" );

In this case, Java works relative to the current directory of the Java interpreter. You can determine the current directory by checking the user.dir property in the System Properties list:

System.getProperty("user.dir"));

An overloaded version of the File constructor lets you specify the directory path and filename as separate String objects:

File fooFile = new File( "/tmp", "foo.txt" );

With yet another variation, you can specify the directory with a File object and the filename with a String:

File tmpDir = new File( "/tmp" );  
File fooFile = new File ( tmpDir, "foo.txt" );

None of the File constructors throw any exceptions. This means the object is created whether or not the file or directory actually exists; it isn’t an error to create a File object for a nonexistent file. You can use the object’s exists( ) instance method to find out whether the file or directory exists. The File object simply exists as a handle for getting information about what is (potentially at least) a file or directory.

Path localization

One of the reasons that working with files in Java is problematic is that pathnames are expected to follow the conventions of the local filesystem. Java’s designers intend to provide an abstraction that deals with most system-dependent filename features, such as the file separator, path separator, device specifier, and root directory. Unfortunately, not all these features are implemented in the current version.

On some systems, Java can compensate for differences such as the direction of the file separator slashes in a pathname. For example, in the current implementation on Windows platforms, Java accepts paths with either forward slashes or backslashes. However, under Solaris, Java accepts only paths with forward slashes.

Your best bet is to make sure you follow the filename conventions of the host filesystem. If your application is just opening and saving files at the user’s request, you should be able to handle that functionality with the Swing JFileDialog class. This class encapsulates a graphical file-selection dialog box. The methods of the JFileDialog take care of system-dependent filename features for you.

If your application needs to deal with files on its own behalf, however, things get a little more complicated. The File class contains a few static variables to make this task possible. File.separator defines a String that specifies the file separator on the local host (e.g., / on Unix and Macintosh systems and \ on Windows systems); File.separatorChar provides the same information as a char. File.pathSeparator defines a String that separates items in a path (e.g., : on Unix systems and ; on Macintosh and Windows systems); File.pathSeparatorChar provides the information as a char.

You can use this system-dependent information in several ways. Probably the simplest way to localize pathnames is to pick a convention you use internally, for instance the forward slash, and do a String replace to substitute for the localized separator character:

// we'll use forward slash as our standard  
String path = "mail/1999/june/merle";  
path = path.replace('/', File.separatorChar);  
File mailbox = new File( path );

Alternately, you could work with the components of a pathname and build the local pathname when you need it:

String [] path = { "mail", "1999", "june", "merle" };  

   StringBuffer sb = new StringBuffer(path[0]);  
for (int i=1; i< path.length; i++)  
    sb.append( File.separator + path[i] );  
File mailbox = new File( sb.toString( ) );

One thing to remember is that Java interprets the backslash character (\) as an escape character when used in a String. To get a backslash in a String, you have to use \\.

Another issue to grapple with is that some operating systems use special identifiers for the "roots” of filesystems. For example, Windows uses C:\. Should you need it, the File class provides the static method listRoots( ) , which returns an array of File objects corresponding to the filesystem root directories.

File operations

Once we have a File object, we can use it to ask for information about the file or directory and to perform standard operations on it. A number of methods let us ask certain questions about the File. For example, isFile( ) returns true if the File represents a file, while isDirectory( ) returns true if it’s a directory. isAbsolute( ) indicates if the File has an absolute or relative path specification.

Components of the File pathname are available through the following methods: getName( ), getPath( ), getAbsolutePath( ), and getParent( ). getName( ) returns a String for the filename without any directory information; getPath( ) returns the directory information without the filename. If the File has an absolute path specification, getAbsolutePath( ) returns that path. Otherwise it returns the relative path appended to the current working directory. getParent( ) returns the parent directory of the File.

Interestingly, the string returned by getPath( ) or getAbsolutePath( ) may not follow the same case-conventions as the underlying filesystem. You can retrieve the filesystem’s own or “canonical” version of the file’s path using the method getCanonicalPath( ). In Windows, for example, you can create a File object whose getAbsolutePath( ) is C:\Autoexec.bat, but whose getCanonical-Path( ) is C:\AUTOEXEC.BAT. This is useful for comparing filenames that may have been supplied with different case conventions.

You can get or set the modification time of a file or directory with lastModified() and setLastModified( ) methods. The value is a long that is the number of milliseconds since the epoch ( Jan 1, 1970, 00:00:00 GMT). We can also get the size of the file in bytes with length( ) .

Here’s a fragment of code that prints some information about a file:

File fooFile = new File( "/tmp/boofa" );  
  
String type = fooFile.isFile( ) ? "File " : "Directory ";  
String name = fooFile.getName( );  
long len = fooFile.length( );  
System.out.println(type + name + ", " + len + " bytes " );

If the File object corresponds to a directory, we can list the files in the directory with the list( ) method or the listFiles( ) method:

String [] fileNames = fooFile.list( );
File [] files = fooFile.listFiles( );

list( ) returns an array of String objects that contains filenames. listFiles( ) returns an array of File objects. Note that in neither case are the files guaranteed to be in any kind of order (alphabetical, for example).

If the File refers to a nonexistent directory, we can create the directory with mkdir( ) or mkdirs( ). mkdir( ) creates a single directory; mkdirs( ) also creates all of the intervening directories in a File specification. Use renameTo( ) to rename a file or directory and delete( ) to delete a file or directory.

Note that using the File object itself isn’t generally the way to create a file; that’s normally done implicitly with a FileOutputStream or FileWriter, as we’ll discuss in a moment. The exception is the createNewFile( ) method, which can be used to attempt to create a new zero-length file at the location pointed to by the File object. The useful thing about this method is that the operation is guaranteed to be “atomic” with respect to all other file creation. createNewFile( ) returns a boolean value which tells you whether the file was created.

You can use this to implement file locking from Java. This is useful in combination with deleteOnExit( ) , which flags the file to be automatically removed when the Java Virtual Machine exits. Another file creation method related to the File class itself is the static method createTempFile( ) , which creates a file in a specified location using an automatically generated unique name. This, too, is useful in combination with deleteOnExit( ).

The toURL( ) method converts a file path to a file: URL object. We’ll talk about URLs in Chapter 12. They are an abstraction that allows you to point to any kind of object anywhere on the Net. Converting a File reference to a URL may be useful for consistency with more general routines that deal with URLs.

Table 10.1 summarizes the methods provided by the File class.

Table 10-1. File Methods

Method

Return Type

Description

canRead( )

boolean

Is the file (or directory) readable?

canWrite( )

boolean

Is the file (or directory) writable?

createNewFile()

boolean

Creates a new file

createTempFile (String pfx, String sfx)

static File

Creates a new file, with the specified prefix and suffix, in the default temp-file directory

delete( )

boolean

Deletes the file (or directory)

deleteOnExit()

void

When it exits, Java runtime system will delete the file

exists( )

boolean

Does the file (or directory) exist?

getAbsolutePath( )

String

Returns the absolute path of the file (or directory)

getCanonicalPath( )

String

Returns the absolute, case-correct path of the file (or directory)

getName( )

String

Returns the name of the file (or directory)

getParent( )

String

Returns the name of the parent directory of the file (or directory)

getPath( )

String

Returns the path of the file (or directory)

isAbsolute( )

boolean

Is the filename (or directory name) absolute?

isDirectory( )

boolean

Is the item a directory?

isFile( )

boolean

Is the item a file?

lastModified( )

long

Returns the last modification time of the file (or directory)

length( )

long

Returns the length of the file

list( )

String []

Returns a list of files in the directory

listfiles()

File[]

Returns the contents of the directory as an array of File objects

mkdir( )

boolean

Creates the directory

mkdirs( )

boolean

Creates all directories in the path

renameTo(File dest )

boolean

Renames the file (or directory)

setLastModified()

boolean

Sets the last-modified time of the file (or directory)

setReadOnly()

boolean

Sets the file to read-only status

toURL()

java.net.URL

Generates a URL object for the Thefile (or directory)

File Streams

Java provides two specialized streams for reading and writing files in the filesystem: FileInputStream and FileOutputStream. These streams provide the basic InputStream and OutputStream functionality applied to reading and writing the contents of files. They can be combined with the filter streams described earlier to work with files in the same way we do other stream communications.

Because FileInputStream is a subclass of InputStream, it inherits all standard InputStream functionality for reading the contents of a file. FileInputStream provides only a low-level interface to reading data, however, so you’ll typically wrap it with another stream, such as a DataInputStream.

You can create a FileInputStream from a String pathname or a File object:

FileInputStream foois = new FileInputStream( fooFile );  
FileInputStream passwdis = new FileInputStream( "/etc/passwd" );

When you create a FileInputStream, the Java runtime system attempts to open the specified file. Thus, the FileInputStream constructors can throw a FileNotFoundException if the specified file doesn’t exist, or an IOException if some other I/O error occurs. Be sure to catch and handle these exceptions in your code. When the stream is first created, its available( ) method and the File object’s length( ) method should return the same value. Be sure to call the close( ) method when you are done with the file.

To read characters from a file, you can wrap an InputStreamReader around a FileInputStream. If you want to use the default character-encoding scheme, you can use the FileReader class instead, which is provided as a convenience. FileReader works just like FileInputStream, except that it reads characters instead of bytes and wraps a Reader instead of an InputStream.

The following class, ListIt , is a small utility that sends the contents of a file or directory to standard output:

//file: ListIt.java
import java.io.*;  
  
class ListIt {  
    public static void main ( String args[] ) throws Exception {  
        File file =  new File( args[0] );  
  
        if ( !file.exists() || !file.canRead( ) ) {  
            System.out.println( "Can't read " + file );  
            return;  
        }  
  
        if ( file.isDirectory( ) ) {  
            String [] files = file.list( );  
            for (int i=0; i< files.length; i++)  
                System.out.println( files[i] );  
        }  
        else  
            try {  
                FileReader fr = new FileReader ( file ); 
                BufferedReader in = new BufferedReader( fr ); 
                String line; 
                while ((line = in.readLine( )) != null) 
                System.out.println(line); 
            }  
            catch ( FileNotFoundException e ) { 
                System.out.println( "File Disappeared" );  
            }  
    }  
}

ListIt constructs a File object from its first command-line argument and tests the File to see whether it exists and is readable. If the File is a directory, ListIt outputs the names of the files in the directory. Otherwise, ListIt reads and outputs the file.

FileOutputStream is a subclass of OutputStream, so it inherits all the standard OutputStream functionality for writing to a file. Just like FileInputStream though, FileOutputStream provides only a low-level interface to writing data. You’ll typically wrap another stream, like a DataOutputStream or a PrintWriter, around the FileOutputStream to provide higher-level functionality.

You can create a FileOutputStream from a String pathname or a File object. Unlike FileInputStream, however, the FileOutputStream constructors don’t throw a FileNotFoundException. If the specified file doesn’t exist, the FileOutputStream creates the file. The FileOutputStream constructors can throw an IOException if some other I/O error occurs, so you still need to handle this exception.

If the specified file does exist, the FileOutputStream opens it for writing. When you subsequently call the write( ) method, the new data overwrites the current contents of the file. If you need to append data to an existing file, you should use a different constructor that accepts an append flag:

FileInputStream foois = new FileOutputStream( fooFile, true);  
FileInputStream psis = new FileOutputStream("/etc/passwd", true);

Another way to append data to files is with a RandomAccessFile, as we’ll discuss shortly.

To write characters (instead of bytes) to a file, you can wrap an OutputStreamWriter around a FileOutputStream. If you want to use the default character-encoding scheme, you can use instead the FileWriter class, which is provided as a convenience. FileWriter works just like FileOutputStream, except that it writes characters instead of bytes and wraps a Writer instead of an OutputStream.

The following example reads a line of data from standard input and writes it to the file /tmp/foo.txt:

String s = new BufferedReader( 
    new InputStreamReader( System.in ) ).readLine( );  
File out = new File( "/tmp/foo.txt" );  
FileWriter fw = new FileWriter ( out );  
PrintWriter pw = new PrintWriter( fw )  
pw.println( s ); 
fw.close( );

Notice how we have wrapped a PrintWriter around the FileWriter to facilitate writing the data. Also, to be a good filesystem citizen, we’ve called the close( ) method when we’re done with the FileWriter.

The java.io.RandomAccessFile Class

The java.io.RandomAccessFile class provides the ability to read and write data from or to any specified location in a file. RandomAccessFile implements both the DataInput and DataOutput interfaces, so you can use it to read and write strings and primitive types. In other words, RandomAccessFile defines the same methods for reading and writing data as DataInputStream and DataOutputStream. However, because the class provides random, rather than sequential, access to file data, it’s not a subclass of either InputStream or OutputStream.

You can create a RandomAccessFile from a String pathname or a File object. The constructor also takes a second String argument that specifies the mode of the file. Use "r" for a read-only file or "rw" for a read-write file. Here’s how we would start to create a simple database to keep track of user information:

try {  
    RandomAccessFile users =
        new RandomAccessFile( "Users", "rw" );
    ...  
}  
catch (IOException e) { ... }

When you create a RandomAccessFile in read-only mode, Java tries to open the specified file. If the file doesn’t exist, RandomAccessFile throws an IOException. If, however, you are creating a RandomAccessFile in read-write mode, the object creates the file if it doesn’t exist. The constructor can still throw an IOException if some other I/O error occurs, so you still need to handle this exception.

After you have created a RandomAccessFile, call any of the normal reading and writing methods, just as you would with a DataInputStream or DataOutputStream. If you try to write to a read-only file, the write method throws an IOException.

What makes a RandomAccessFile special is the seek( ) method. This method takes a long value and uses it to set the location for reading and writing in the file. You can use the getFilePointer( ) method to get the current location. If you need to append data to the end of the file, use length( ) to determine that location, then seek( ) to it. You can write or seek beyond the end of a file, but you can’t read beyond the end of a file. The read( ) method throws an EOFException if you try to do this.

Here’s an example of writing some data to our user database:

users.seek( userNum * RECORDSIZE );  
users.writeUTF( userName );  
users.writeInt( userID );

One caveat to notice with this example is that we need to be sure that the String length for userName, along with any data that comes after it, fits within the specified record size.

Applets and Files

For security reasons, untrusted applets and applications are not permitted to read from and write to arbitrary places in the filesystem. The ability of untrusted code to read and write files, as with any kind of system resource, is under the control of the system security policy, through a SecurityManager object. A SecurityManager is installed by the application that is running the untrusted code, such as appletviewer or a Java-enabled web browser. All filesystem access must first pass the scrutiny of the SecurityManager.

For example, Sun’s HotJava web browser allows even untrusted applets to have access to specific files designated by the user in an access-control list. Netscape Navigator, on the other hand, currently doesn’t allow untrusted applets any access to the filesystem. In both cases, trusted applets can be given arbitrary access to the filesystem, just like a standalone Java application.

It isn’t unusual to want an applet to maintain some kind of state information on the system on which it’s running. But for a Java applet that is restricted from access to the local filesystem, the only option is to store data over the network on its server. Applets have at their disposal powerful general means for communicating data over networks. The only limitation is that, by convention, an applet’s network communication is restricted to the server that launched it. This limits the options for where the data will reside.

Currently, the only way for a Java program to send data to a server is through a network socket or tools like RMI, which run over sockets. In Chapter 11, we’ll take a detailed look at building networked applications with sockets. With the tools described in that chapter, it’s possible to build powerful client/server applications. Sun also has a Java extension called WebNFS, which allows applets and applications to work with files on an NFS server in much the same way as the ordinary File API.

Loading Application Resources

We often have data files and other objects that we want our programs to use. Java provides many ways to access these resources. In a standalone application, we can simply open files and read the bytes. In both standalone applications and applets, we can construct URLs to well-known locations. The problem with these methods is that we have to know where our application lives in order to find our data. This is not always as easy as it seems. What is needed is a universal way to access resources associated with our application and our application’s individual classes. The Class class’s getResource( ) method provides just this.

What does getResource( ) do for us? To construct a URL to a file, we normally have to figure out a home directory for our code and construct a path relative to that. In an applet, we could use getCodeBase( ) or getDocumentBase( ) to find the base URL, and use that base to create the URL for the resource we want. But these methods don’t help a standalone application—and there’s no reason that a standalone application and an applet shouldn’t be able to share classes anyway. To solve this problem, the getResource( ) method provides a standard way to get objects relative to a given class file or to the system classpath. getResource( ) returns a special URL that uses the class’s class loader. This means that no matter where the class came from—a web server, the local filesystem, or even a JAR file—we can simply ask for an object, get a URL for the object, and use the URL to access the object.

getResource( ) takes as an argument a slash-separated pathname for the resource and returns a URL. There are two kinds of paths: absolute and relative. An absolute path begins with a slash. For example: /foo/bar/blah.txt. In this case, the search for the object begins at the top of the class path. If there is a directory foo/bar in the class path, getResource( ) searches that directory for the blah.txt file. A relative URL does not begin with a slash. In this case, the search begins at the location of the class file, whether it is local, on a remote server, or in a JAR file (either local or remote). So if we were calling getResource( ) on a class loader that loaded a class in the foo.bar package, we could refer to the file as blah.txt. In this case, the class itself would be loaded from the directory foo/bar somewhere on the class path, and we’d expect to find the file in the same directory.

For example, here’s an application that looks up some resources:

//file: FindResources.java
package mypackage; 
import java.net.URL;
import java.io.IOException;
 
public class FindResources { 
  public static void main( String [] args ) throws IOException { 
    // absolute from the classpath 
    URL url = FindResources.class.getResource("/mypackage/foo.txt");
    // relative to the class location 
    url = FindResources.class.getResource("foo.txt"); 
    // another relative document 
    url = FindResources.class.getResource("docs/bar.txt"); 
  }
}

The FindResources class belongs to the mypackage package, so its class file will live in a mypackage directory somewhere on the class path. FindResources locates the document foo.txt using an absolute and then a relative URL. At the end, FindResources uses a relative path to reach a document in the mypackage/docs directory. In each case we refer to the FindResources’s Class object using the static .class notation. Alternatively, if we had an instance of the object, we could use its getClass( ) method to reach the Class object.

For an applet, the search is similar but occurs on the host from which the applet was loaded. getResource( ) first checks any JAR files loaded with the applet, and then searches the normal remote applet class path, constructed relative to the applet’s codebase URL.

getResource( ) returns a URL for whatever type of object you reference. This could be a text file or properties file that you want to read as a stream, or it might be an image or sound file or some other object. If you want the data as a stream, the Class class also provides a getResourceAsStream( ) method. In the case of an image, you’d probably hand the URL over to the getImage( ) method of the Applet class or one of the components of the Swing package for loading.

Get Learning Java 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.