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 networked applications can, of course, be restricted by the SecurityManager 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; I'll mention these areas as they occur.
The java.io.File class encapsulates access to information about a file or directory entry in the filesystem. It gets attribute information about a file, lists the entries in a directory, and performs 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.
You can create an instance of File from a String pathname as follows:
File fooFile = new File( "/tmp/foo.txt" ); File barDir = new File( "/tmp/bar" );
You can also create a file with a relative path like:
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 an nonexistent file. You can use the exists() method to find out whether the file or directory exists.
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 of 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 the above string. 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 java.awt.FileDialog class. This class encapsulates a graphical file-selection dialog box. The methods of the FileDialog 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 easier. 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), while File.separatorChar provides the same information in character form. File.pathSeparator defines a String that separates items in a path (e.g., ":" on UNIX systems; ";" on Macintosh and Windows systems); File.pathSeparatorChar provides the information in character form.
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, say "/", and do a String replace to substitute for the localized separator character:
// We'll use forward slash as our standard String path = "mail/1995/june/merle"; path = path.replace('/', File.separatorChar); File mailbox = new File( path );
Alternately, you could work with the components of a pathname and built the local pathname when you need it:
String [] path = { "mail", "1995", "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 " \\".
Once we have a valid File object, we can use it to ask for information about the file itself and to perform standard operations on it. A number of methods lets 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 whether the File has an absolute or relative path specification.
The 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 be the same, case-wise, as the actual name of the file. You can retrieve the case-correct version of the file's path using getCanonicalPath(). In Windows 95, for example, you can create a File object whose getAbsolutePath() is C:\Autoexec.bat but whose getCanonicalPath() is C:\AUTOEXEC.BAT.
We can get the modification time of a file or directory with lastModified(). This time value is not useful as an absolute time; you should use it only to compare two modification times. 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:
String [] files = fooFile.list();
list() returns an array of String objects that contains filenames. (You might expect that list() would return an Enumeration instead of an array, but it doesn't.)
If the File refers to a nonexistent directory, we can create the directory with mkdir() or mkdirs(). mkdir() creates a single directory; mkdirs() creates all of the directories in a File specification. Use renameTo() to rename a file or directory and delete() to delete a file or directory. Note that File doesn't provide a method to create a file; creation is handled with a FileOutputStream as we'll discuss in a moment.
Table 8.1 summarizes the methods provided by the File class.
Method | Return type | Description |
---|---|---|
canRead() | boolean |
Is the file (or directory) readable? |
canWrite() | boolean |
Is the file (or directory) writable? |
delete() | boolean |
Deletes the file (or directory) |
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 |
mkdir() | boolean |
Creates the directory |
mkdirs() | boolean |
Creates all directories in the path |
renameTo(File dest) | boolean |
Renames the file (or directory) |
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 filtered 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 another stream like a DataInputStream around the FileInputStream.
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, Java 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. You should 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 displays the contents of a file or directory to standard output:
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 if it exists and is readable. If the File is a directory, ListIt prints the names of the files in the directory. Otherwise, ListIt reads and prints 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 PrintStream 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 actually call a 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, as shown here:
FileInputStream foois = new FileInputStream( fooFile ); FileInputStream passwdis = new FileInputStream( "/etc/passwd" );
Antoher alternative for appending files is to use a RandomAccessFile, as I'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 the FileWriter class instead, 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, true ) pw.println( s );
Notice how we have wrapped a PrintWriter around the FileWriter to facilitate writing the data. To be a good filesystem citizen, you need to call the close() method when you are done with the FileWriter.
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 Java 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 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 on the end of the file, use length() to determine that location. You can write or seek beyond the end of a file, but you can't read beyond the end of a file. The read methods throws a 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 boundaries of the record size.
For security reasons, applets are not permitted to read and write to arbitrary places in the filesystem. The ability of an applet to read and write files, as with any kind of system resource, is under the control of a SecurityManager object. A SecurityManager is installed by the application that is running the applet, such as an applet viewer or Java-enabled Web browser. All filesystem access must first pass the scrutiny of the SecurityManager. With that in mind, applet-viewer applications are free to implement their own schemes for what, if any, access an applet may have.
For example, Sun's HotJava Web browser allows 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 applets any access to the filesystem.
It isn't unusual to want an applet to maintain some kind of state information on the system where 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. Although, at the moment, the Web is a relatively static, read-only environment, applets have at their disposal powerful, general means for communicating data over networks, as you'll see in Chapter 9, Network Programming. 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.
The only means of writing data to a server in Java is through a network socket. In Chapter 9, Network Programming we'll look at building networked applications with sockets in detail. With the tools of that chapter it's possible to build powerful client/server applications.