Reading and Writing Files - Writing Internet Applications - Sams Teach Yourself Java in 24 Hours, 7th Edition (2014)

Sams Teach Yourself Java in 24 Hours, 7th Edition (2014)

Part VI: Writing Internet Applications

Hour 21. Reading and Writing Files

THIS HOUR’S TO-DO LIST:

Image Read bytes from a file into a program.

Image Create a new file on your computer.

Image Save an array of bytes to a file.

Image Make changes to the data stored in a file.

There are numerous ways to represent data on a computer. You already have worked with one by creating objects. An object includes data in the form of variables and references to objects. It also includes methods that use the data to accomplish tasks.

To work with other kinds of data, such as files on your hard drive and documents on a web server, you can use the classes of the java.io package. The “io” part of its name stands for “input/output” and the classes are used to access a source of data, such as a hard drive, DVD, or the computer’s memory.

You can bring data into a program and send data out by using a communications system called streams, which are objects that take information from one place to another.

Streams

To save data permanently within a Java program, or to retrieve that data later, you must use at least one stream.

A stream is an object that takes information from one source and sends it somewhere else, taking its name from water streams that take fish, boats, and industrial pollutants from one place to another.

Streams connect a diverse variety of sources, including computer programs, hard drives, Internet servers, computer memory, and thumb drives. After you learn how to work with one kind of data using streams, you are able to work with others in the same manner.

During this hour, you use streams to read and write data stored in files on your computer.

There are two kinds of streams:

Image Input streams, which read data from a source

Image Output streams, which write data to a source


Note

Java class files are stored as bytes in a form called bytecode. The Java Virtual Machine runs bytecode, which doesn’t actually have to be produced by the Java language. It can run compiled bytecode produced by other languages, including NetRexx and Jython. You also hear the JVM referred to as the bytecode interpreter.


All input and output streams are made up of bytes, individual integers with values ranging from 0 to 255. You can use this format to represent data, such as executable programs, word-processing documents, and MP3 music files, but those are only a small sampling of what bytes can represent. A byte stream is used to read and write this kind of data.

A more specialized way to work with data is in the form of characters—individual letters, numbers, punctuation, and the like. You can use a character stream when you are reading and writing a text source.

Whether you work with a stream of bytes, characters, or other kinds of information, the overall process is the same:

Image Create a stream object associated with the data.

Image Call methods of the stream to either put information in the stream or take information out of it.

Image Close the stream by calling the object’s close() method.

Files

In Java, files are represented by the File class, which also is part of the java.io package. Files can be read from hard drives, floppy drives, CD-ROMs, and other storage devices.


Note

This example works on a Windows system, which uses the backslash \\ character as a separator in path and filenames. Linux and other Unix-based systems use a forward slash / character instead. To write a Java program that refers to files in a way that works regardless of the operating system, use the class variable File.pathSeparator instead of a forward or backslash, as in this statement:

File bookName = new File("data" + File.pathSeparator
+ "address.dat");


A File object can represent files that already exist or files you want to create. To create a File object, use the name of the file as the constructor, as in this example:

File bookName = new File("address.dat");

This creates an object for a file named address.dat in the current folder. You also can include a path in the filename:

File bookName = new File("data\\address.dat");

When you have a File object, you can call several useful methods on that object:

Image exists()—true if the file exists, false otherwise

Image getName()—The name of the file, as a String

Image length()—The size of the file, as a long value

Image createNewFile()—Creates a file of the same name, if one does not exist already

Image delete()—Deletes the file, if it exists

Image renameTo(File)—Renames the file, using the name of the File object specified as an argument

You also can use a File object to represent a folder on your system rather than a file. Specify the folder name in the File constructor, which can be absolute (such as C:\\MyDocuments\\) or relative (such as java\\database).

After you have an object representing a folder, you can call its listFiles() method to see what’s inside the folder. This method returns an array of File objects representing every file and subfolder it contains.

Reading Data from a Stream

The first project of the hour is to read data from a file using an input stream. You can do this using the FileInputStream class, which represents input streams that are read as bytes from a file.

You can create a file input stream by specifying a filename or a File object as the argument to the FileInputStream() constructor.

Methods that read or write files can fail with an IOException if there’s an error accessing the file. Many of the methods associated with reading and writing files generate this exception, so a try-catch block is often used.

Streams are one of the resources in Java that must be closed when they’re no longer being used. Leaving a stream open is a significant drain on resources in the Java Virtual Machine as a program runs.

A special try statement called try-with-resources makes it easy to ensure that a resource, such as a file input stream, will be closed when it’s no longer needed. The try statement is followed by parentheses. Inside the parentheses are one or more Java statements that declare variables that read or write data through a resource.

Here’s an example that reads a text file called cookie.web using a file input stream named stream:

File cookie = new File("cookie.web");
try (FileInputStream stream = new FileInputStream(cookie)) {
System.out.println("Length of file: " + cookie.length());
} catch (IOException ioe) {
System.out.println("Could not read file.");
}

Because stream is in the try statement, the stream is closed automatically when the try-catch block completes (if it hasn’t been closed).

File input streams read data in bytes. You can read a single byte by calling the stream’s read() method without an argument. If no more bytes are available in the stream because you have reached the end of the file, a byte value of –1 is returned.

When you read an input stream, it begins with the first byte in the stream, such as the first byte in a file. You can skip some bytes in a stream by calling its skip() method with one argument: an int representing the number of bytes to skip. The following statement skips the next 1,024 bytes in a stream named scanData:

scanData.skip(1024);

If you want to read more than one byte at a time, do the following:

Image Create a byte array that is exactly the size of the number of bytes you want to read.

Image Call the stream’s read() method with that array as an argument. The array is filled with bytes read from the stream.

You create an application that reads ID3 data from an MP3 audio file. Because MP3 is such a popular format for music files, 128 bytes are often added to the end of an ID3 file to hold information about the song, such as the title, artist, and album.


Note

On the ASCII character set, which is included in the Unicode Standard character set supported by Java, those three numbers represent the capital letters “T,” “A,” and “G,” respectively.


The ID3Reader application reads an MP3 file using a file input stream, skipping everything but the last 128 bytes. The remaining bytes are examined to see if they contain ID3 data. If they do, the first three bytes are the numbers 84, 65, and 71.

Create a new empty Java file called ID3Reader and fill it with the text from Listing 21.1.

LISTING 21.1 The Full Text of ID3Reader.java


1: package com.java24hours;
2:
3: import java.io.*;
4:
5: public class ID3Reader {
6: public static void main(String[] arguments) {
7: File song = new File(arguments[0]);
8: try (FileInputStream file = new FileInputStream(song)) {
9: int size = (int) song.length();
10: file.skip(size - 128);
11: byte[] last128 = new byte[128];
12: file.read(last128);
13: String id3 = new String(last128);
14: String tag = id3.substring(0, 3);
15: if (tag.equals("TAG")) {
16: System.out.println("Title: " + id3.substring(3, 32));
17: System.out.println("Artist: " + id3.substring(33, 62));
18: System.out.println("Album: " + id3.substring(63, 91));
19: System.out.println("Year: " + id3.substring(93, 97));
20: } else {
21: System.out.println(arguments[0] + " does not contain"
22: + " ID3 info.");
23: }
24: file.close();
25: } catch (IOException ioe) {
26: System.out.println("Error -- " + ioe.toString());
27: }
28: }
29: }


Before running this class as an application, you must specify an MP3 file as a command-line argument (using Run, Set Project Configuration, Configure in NetBeans). The program can be run with any MP3, such as Come On and Gettit.mp3, the unjustly forgotten 1973 soul classic by Marion Black. If you have the song Come On and Gettit.mp3 on your system (and you really should), Figure 21.1 shows what the ID3Reader application displays.

Image

FIGURE 21.1 Running the ID3Reader application.

The application reads the last 128 bytes from the MP3 in Lines 11–12 of Listing 21.1, storing them in a byte array. This array is used in Line 13 to create a String object that contains the characters represented by those bytes.


Tip

If you don’t have Come On and Gettit.mp3 on your computer (a big mistake, in my opinion), you can look for MP3 songs to examine using the Creative Commons license at http://search.creativecommons.org.

Creative Commons is a set of copyright licenses that stipulate how a work such as a song or book can be distributed, edited, or republished. The website Rock Proper at www.rockproper.com also offers a collection of MP3 albums that are licensed for sharing under Creative Commons.


If the first three characters in the string are “TAG,” the MP3 file being examined contains ID3 information in a format the application understands.

In Lines 16–19, the string’s substring() method is called to display portions of the string. The characters to display are from the ID3 format, which always puts the artist, song, title, and year information in the same positions in the last 128 bytes of an MP3 file.

Some MP3 files either don’t contain ID3 information at all or contain ID3 information in a different format than the application can read.

The file Come On and Gettit.mp3 contains readable ID3 information if you created it from a copy of the Eccentric Soul CD that you purchased. Programs that create MP3 files from audio CDs read song information from a music industry database called CDDB.


Note

You might be tempted to find a copy of Come On and Gettit.mp3 on a service such as BitTorrent, one of the most popular file-sharing services. I can understand this temptation perfectly where “Come On and Gettit” is concerned. However, according to the Recording Industry Association of America, anyone who downloads MP3 files for music CDs you do not own will immediately burst into flames. Eccentric Soul is available from Amazon.com, eBay, Apple iTunes, and other leading retailers.


After everything related to the ID3 information has been read from the MP3’s file input stream, the stream is closed in Line 24. You should always close streams when you are finished with them to conserve resources in the Java interpreter.

Buffered Input Streams

One of the ways to improve the performance of a program that reads input streams is to buffer the input. Buffering is the process of saving data in memory for use later when a program needs it. When a Java program needs data from a buffered input stream, it looks in the buffer first, which is faster than reading from a source such as a file.

To use a buffered input stream, you create an input stream such as a FileInputStream object, and then use that object to create a buffered stream. Call the BufferedInputStream(InputStream) constructor with the input stream as the only argument. Data is buffered as it is read from the input stream.

To read from a buffered stream, call its read() method with no arguments. An integer from 0 to 255 is returned and represents the next byte of data in the stream. If no more bytes are available, –1 is returned instead.

As a demonstration of buffered streams, the next program you create adds a feature to Java that many programmers miss from other languages they have used: console input.

Console input is the ability to read characters from the console (also known as the command line) while running an application.

The System class, which contains the out variable used in the System.out.print() and System.out.println() statements, has a class variable called in that represents an InputStream object. This object receives input from the keyboard and makes it available as a stream.

You can work with this input stream like any other. The following statement creates a buffered input stream associated with the System.in input stream:

BufferedInputStream bin = new BufferedInputStream(System.in);

The next project, the Console class, contains a class method you can use to receive console input in any of your Java applications. Enter the text from Listing 21.2 in a new Java file named Console.

LISTING 21.2 The Full Text of Console.java


1: package com.java24hours;
2:
3: import java.io.*;
4:
5: public class Console {
6: public static String readLine() {
7: StringBuilder response = new StringBuilder();
8: try {
9: BufferedInputStream bin = new
10: BufferedInputStream(System.in);
11: int in = 0;
12: char inChar;
13: do {
14: in = bin.read();
15: inChar = (char) in;
16: if (in != -1) {
17: response.append(inChar);
18: }
19: } while ((in != -1) & (inChar != '\n'));
20: bin.close();
21: return response.toString();
22: } catch (IOException e) {
23: System.out.println("Exception: " + e.getMessage());
24: return null;
25: }
26: }
27:
28: public static void main(String[] arguments) {
29: System.out.print("You are standing at the end of the road ");
30: System.out.print("before a small brick building. Around you ");
31: System.out.print("is a forest. A small stream flows out of ");
32: System.out.println("the building and down a gully.\n");
33: System.out.print("> ");
34: String input = Console.readLine();
35: System.out.println("That's not a verb I recognize.");
36: }
37: }


The Console class includes a main() method that demonstrates how it can be used. When you run the application, the output should resemble Figure 21.2.

Image

FIGURE 21.2 Running the Console application.


Note

The Console class is also the world’s least satisfying text adventure game. You can’t enter the building, wade in the stream, or even wander off. For a more full-featured version of this game, which is called Adventure, visit Web-Adventures at www.web-adventures.org.


The Console class contains one class method, readLine(), which receives characters from the console. When the Enter key is hit, readLine() returns a String object that contains all the characters that are received.

Writing Data to a Stream

In the java.io package, the classes for working with streams come in matched sets. There are FileInputStream and FileOutputStream classes for working with byte streams, FileReader and FileWriter classes for working with character streams, and many other sets for working with other kinds of stream data.

To begin writing data, you first create a File object that is associated with an output stream. This file doesn’t have to exist on your system.

You can create a FileOutputStream in two ways. If you want to append bytes onto an existing file, call the FileOutputStream() constructor with two arguments: a File object representing the file and the boolean of true. The bytes you write to the stream are tacked onto the end of the file.

If you want to write bytes into a new file, call the FileOutputStream() constructor with a File object as its only object.

After you have an output stream, you can call different write() methods to write bytes to it:

Image Call write() with a byte as its only argument to write that byte to the stream.

Image Call write() with a byte array as its only argument to write all the array’s bytes to the stream.

Image Specify three arguments to the write(byte[], int, int) method: a byte array, an integer representing the first element of the array to write to the stream, and the number of bytes to write.

The following statement creates a byte array with 10 bytes and writes the last 5 to an output stream:

File dat = new File("data.dat");
FileOutputStream datStream = new FileOutputStream(dat);
byte[] data = new byte[] { 5, 12, 4, 13, 3, 15, 2, 17, 1, 18 };
datStream.write(data, 5, 5);

When writing bytes to a stream, you can convert text to an array of bytes by calling the String object’s getBytes() method, as in this example:

String name = "Puddin N. Tane";
byte[] nameBytes = name.getBytes();

After you have finished writing bytes to a stream, you close it by calling the stream’s close() method.

The next project you write is a simple application, ConfigWriter, that saves several lines of text to a file by writing bytes to a file output stream. Create a Java file of that name and enter the text from Listing 21.3 into the source editor.

LISTING 21.3 The Full Text of ConfigWriter.java


1: package com.java24hours;
2:
3: import java.io.*;
4:
5: public class ConfigWriter {
6: String newline = System.getProperty("line.separator");
7:
8: public ConfigWriter() {
9: try {
10: File file = new File("program.properties");
11: FileOutputStream fileStream = new FileOutputStream(file);
12: write(fileStream, "username=max");
13: write(fileStream, "score=12550");
14: write(fileStream, "level=5");
15: fileStream.close();
16: } catch (IOException ioe) {
17: System.out.println("Could not write file");
18: }
19: }
20:
21: void write(FileOutputStream stream, String output)
22: throws IOException {
23:
24: output = output + newline;
25: byte[] data = output.getBytes();
26: stream.write(data, 0, data.length);
27: }
28:
29: public static void main(String[] arguments) {
30: ConfigWriter cw = new ConfigWriter();
31: }
32: }


When this application is run, it creates a file called program.properties that contains the following three lines of text:

Output

username=max
score=12550
level=5

The file is created in Line 10 and associated with a file output stream in Line 11. The three properties are written to the stream in Lines 12–14.

An application run in NetBeans will save the file (or files) it creates in the project’s main folder if no other folder is specified. To see the program.properties file in NetBeans, in the Projects pane, click the Files tab to bring it to the front. The file is in the top Java24 folder, as shown in Figure 21.3.

Image

FIGURE 21.3 Finding the program.properties file.

Double-click the filename to open it in the NetBeans source code editor.

Reading and Writing Configuration Properties

Java programs are more versatile when they can be configured using command-line arguments, as you have demonstrated in several applications created in preceding hours. The java.util package includes a class, Properties, that enables configuration settings to be loaded from another source: a text file.

The file can be read like other file sources in Java:

Image Create a File object that represents the file.

Image Create a FileInputStream object from that File object.

Image Call load() to retrieve the properties from that input stream.

A properties file has a set of property names followed by an equal sign = and their values. Here’s an example:

username=lepton
lastCommand=open database
windowSize=32

Each property has its own line, so this sets up properties named username, lastCommand, and windowSize with the values “lepton”, “open database”, and “32”, respectively. (The same format was used by the ConfigWriter application.)

The following code loads a properties file called config.dat:

File configFile = new File("config.dat");
FileInputStream inStream = new FileInputStream(configFile);
Properties config = new Properties();
config.load(inStream);

Configuration settings, which are called properties, are stored as strings in the Properties object. Each property is identified by a key. The getProperty() method retrieves a property using its key, as in this statement:

String username = config.getProperty("username");

Because properties are stored as strings, you must convert them in some manner to use a numerical value, as in this code:

String windowProp = config.getProperty("windowSize");
int windowSize = 24;
try {
windowSize = Integer.parseInt(windowProp);
} catch (NumberFormatException exception) {
// do nothing
}

Properties can be stored by calling the setProperty() method with two arguments—the key and value:

config.setProperty("username", "max");

You can display all properties by calling the list(PrintStream) method of the Properties object. PrintStream is the class of the out variable of the System class, which you’ve been using throughout the book to display output in System.out.println() statements. The following code calls list() to display all properties:

config.list(System.out);

After you have made changes to the properties, you can store them back to the file:

Image Create a File object that represents the file.

Image Create a FileOutputStream object from that File object.

Image Call store(OutputStream, String) to save the properties to the designated output stream with a description of the properties file as the string.

For the next project, you build on the ConfigWriter application, which wrote several program settings to a file. The Configurator application reads those settings into a Java properties file, adds a new property named runtime with the current date and time, and saves the altered file.

Create a new Java file to hold the Configurator class and enter the text from Listing 21.4.

LISTING 21.4 The Full Text of Configurator.java


1: package com.java24hours;
2:
3: import java.io.*;
4: import java.util.*;
5:
6: public class Configurator {
7:
8: public Configurator() {
9: try {
10: // load the properties file
11: File configFile = new File("program.properties");
12: FileInputStream inStream = new FileInputStream(configFile);
13: Properties config = new Properties();
14: config.load(inStream);
15: // create a new property
16: Date current = new Date();
17: config.setProperty("runtime", current.toString());
18: // save the properties file
19: FileOutputStream outStream = new FileOutputStream(configFile);
20: config.store(outStream, "Properties settings");
21: inStream.close();
22: config.list(System.out);
23: } catch (IOException ioe) {
24: System.out.println("IO error " + ioe.getMessage());
25: }
26: }
27:
28: public static void main(String[] arguments) {
29: Configurator con = new Configurator();
30: }
31: }


In this application, the File object for program.properties is created and associated with a file input stream in Lines 11–12. The contents of this file are loaded into a Properties object from the stream in Lines 13–14.

A new property for the current date and time is created in Lines 16–17. The file is then associated with an output stream in Line 19, and the entire properties file is written to that stream in Line 20.

The output of the Configurator application is shown in Figure 21.4.

Image

FIGURE 21.4 Running the Configurator application.

The program.properties file now contains the following text:

Output

#Properties settings
#Sun Dec 22 20:31:07 EDT 2013
runtime=Sun Dec 22 20\:31\:07 EDT 2013
score=12550
level=5
username=max

The backslash character’s \ formatting, which differs from the output of the application, ensures the properties file is stored properly.

Summary

During this hour, you worked with input streams and output streams that wrote bytes, the simplest way to represent data over a stream.

There are many more classes in the java.io package to work with streams in other ways. There’s also a package of classes called java.net that enables you to read and write streams over an Internet connection.

Byte streams can be adapted to many uses because you can easily convert bytes into other data types, such as integers, characters, and strings.

The first project of this hour, the ID3Reader application, read bytes from a stream and converted them into a string to read the ID3 data in this format from a song such as “Come On and Gettit” by Marion Black off the album Eccentric Soul.

Have I mentioned yet that you should buy the song?

Workshop

Q&A

Q. Why do some of the byte stream methods in this hour use integers as arguments? Should they be using byte arguments?

A. There’s a difference between the bytes in a stream and the bytes represented by the byte class. A byte in Java has a value ranging from –128 to 127, while a byte in a stream has a value from 0 to 255. You often have to use int when working with bytes for this reason—it can hold the values 128 to 255, whereas byte cannot.

Q. What is Mumblety-Peg?

A. It’s a schoolyard game played by children with pocketknives.

In the simplest form, players stand and throw knives at their own feet. The one whose knife lands closest wins. Other versions involve throwing the knife at each other so the opponent has to stretch a foot to where it lands. The player who stretches too far and falls down loses.

The name comes from a rule that the winner could pound a peg into the ground with three blows of the knife. The loser had to “mumble the peg,” removing it solely with his teeth.

The game faded from popularity in the early 20th century when the world reached the collective realization that children throwing knives at each other might not be the greatest idea in the world.

Quiz

To see whether you took a big enough byte from the tree of knowledge during this hour, answer the following questions about streams in Java.

1. Which of the following techniques can be used to convert an array of bytes into a string?

A. Call the array’s toString() method.

B. Convert each byte to a character and then assign each one to an element in a String array.

C. Call the String() constructor with the array as an argument.

2. What kind of stream is used to read from a file in a Java program?

A. An input stream

B. An output stream

C. Either

3. What method of the File class can be used to determine the size of a file?

A. getSize()

B. read()

C. length()

Answers

1. C. You can deal with each byte individually, as suggested in answer B, but you can easily create strings from other data types.

2. A. An input stream is created from a File object or by providing a filename to the input stream’s constructor.

3. C. This method returns a long, representing the number of bytes in the stream.

Activities

To experience the refreshing feeling of wading through another stream, test the waters with the following activities:

Image Write an application that reads the ID3 tags of all MP3 files in a folder and renames the files using the artist, song, and album information (when it is provided).

Image Write a program that reads a Java source file and writes it back without any changes under a new name.

Image Buy a copy of the song “Come On and Gettit” by Marion Black.

To see Java programs that implement these activities, visit the book’s website at www.java24hours.com.