Experiments in Java


Session X3: Input, Output, Files, and Exceptions

In this laboratory session, you will implement versions of the SimpleInput and SimpleOutput classes that we have used in previous experiments. Along the way, you will learn about exceptions, one of Java's key mechanisms for dealing with run-time errors. The goals of this laboratory are to

Your instructor will tell you which of the proposed experiments you are to perform.

Prerequisite skills:

Required files:


Discussion

You may have noted that many of our programs and classes depend on two key classes, SimpleInput and SimpleOutput. SimpleInput has been responsible for the inputs to our programs and SimpleOutput has been responsible for generating output from our programs.

Are these standard classes that come with all releases of Java? No, they were developed specifically for this lab manual. Then why are we using them? Because the input and output classes that come with Java do not provide many of the key features that beginning programmers need. Java's input and output facilities are designed primarily for experienced programmers.

However, you should now be ready to think about input and output from the perspective of a more experienced programmer. At the same time, you may find it easiest to continue using SimpleInput and SimpleOutput for most of your programming tasks, extending them when you need new facilities.

Output and System.out

How does the computer write to the screen? It turns out to involve a number of complex operations, operations which often differ significantly from computer to computer. In addition, the way you write to the screen differs from the way you write to a file, and the way you write information intended for human consumption may differ from the way you write information intended for computer consumption.

Hence, most programming languages provide a variety of methods and objects, in object-oriented languages, to simplify printing. In addition, most languages have an object or variable that corresponds to the standard output of a program: the output that typically goes to the screen.

In Java, System.out is the name of an object of class java.io.PrintStream that can be used to print values.

Unfortunately, the designers of Java realized that they had made some mistakes in their original design, and deprecated PrintStream in Java 1.1. By saying that PrintStream is deprecated, they mean that they are still maintaining the class, but recommend that you use a replacement class (in this case, java.io.PrintWriter). We will begin by using the deprecated class, and then consider how we might update code to use the replacement.

How do you print something with a PrintStream object? The same way you do with a SimpleOutput object. That is, you can call print and println methods. So, if we have a PrintStream called ps, we might print Hello World with

    ps.println("Hello World");

How do we print with System.out? Since it's an object in class PrintStream, we can simply write

    System.out.println("Whatever we want to print");

What if we want to follow the new Java recommendations, and use a PrintWriter instead of a PrintStream? First, we must convert the PrintStream to a PrintWriter. Fortunately, Java includes a constructor for PrintWriter that does just that. Hence, if we want to convert Java's standard output to a PrintWriter named out, we would write

    PrintWriter pw = new PrintWriter(System.out, true);

How do we print with a PrintWriter now that we have one? We call its print and println methods, as in

    pw.println("Even more text");

In Experiment X3.1 you will experiment with the different output mechanisms.

Starting to build SimpleOutput

Let us begin to consider how we might write a SimpleOutput class. Why might we want such a class, given that PrintWriter seems to be able to do most of the work for us? One reason is that it will permit us to more easily add functionality. For example, it is often useful to have a default constructor for an output object that will send output to standard output. PrintWriter does not include such a constructor. In addition, PrintWriter does not include a print method that works on arrays. Given our current state of knowledge, writing a new class is the only way to extend other classes.

What fields will SimpleOutput need? Since it is recommended that we print with PrintWriters, it will have a writer field that refers to the PrintWriter.

  /**
   * The base PrintWriter that does the real work.
   */
  protected PrintWriter writer;

Our default constructor will need to initialize this writer. What should we initialize it to? To standard output.

  /**
   * Build an object that can write to standard output.
   */
  public MyOutput() {
    writer = new PrintWriter(System.out, true);
  } // MyOutput()

Now we need to fill in the print and println methods. In general, they will be fairly simple, consisting of just a call to the corresponding function in PrintWriter. For example,

  /**
   * Print a string.
   */
  public void print(String str) {
    writer.print(str);
  } // print(String)

Unfortunately, as you will soon realize, you will need to create a print method for every primitive type. This means that you will do some busy work. At the same time, you will have the freedom to experiment with the methods. You may also learn that the base class doesn't work quite the way you expected. For example, PrintWriters typically buffer their output (often, just until you tell them to finish the current line with a println). This means that you must explicitly tell them ``okay, print it now!'' with the flush operation.

  /**
   * Print a string.
   */
  public void print(String str) {
    writer.print(str);
    writer.flush();
  } // print(String)

In Experiment X3.2 you will start to build your own output class.

Output files

We are now ready to consider one advantage of using our own class: we can extend the class to print to files instead of to the screen. How do we do this? We create a different kind of PrintWriter. This time, we'll create one that can write to files. That is, we will use a PrintWriter object whose print and println methods print to a specified file, rather than to the screen.

How do we build such an object and associate it with a file? It is a multiple step process. First, we must build a java.io.FileWriter that knows how to write to files. Next, we convert that FileWriter to a PrintWriter, again using one of the nifty constructors that PrintWriter provides. Our constructor might then look like

  /**
   * Create an object that can write to a file.
   */
  public MyOutput(String filename) {
    // Build a FileWriter to write to the given file
    FileWriter fw = new FileWriter(filename);
    // Convert it to a PrintWriter
    writer = new PrintWriter(fw, true);
  } // MyOutput

Unfortunately, this code has a significant problem. In particular, it is not at all clear what should happen if you call the FileWriter constructor with an invalid filename. How does FileWriter tell MyOutput that there was an error?

In Experiment X3.3 you will consider output to files.

Exceptions

In Java, when a method cannot complete its required tasks, it indicates an error by throwing an exception. An exception is simply an object that is used to indicate errors. In effect, exceptions permit methods to exit in two different ways: they can exit normally, returning a value when appropriate or they can exit abnormally, indicating an error.

Every language provides some facility for dealing with errors. Java's differs from most in that Java forces the programmer to decide, in advance, what to do if an error occurs. Therefore, if you call a method that can throw exceptions, Java requires you to indicate what to do if an exception is thrown. It may be that you know that the method will never throw exceptions (e.g., you always create FileWriter objects using a valid file name). Nonetheless, the Java compiler will require you to indicate how to handle the exception.

How do you indicate how to handle the exception? You put the call to the method in a try/catch clause. First, you indicate to Java that you know that an exception may occur by writing

try {
  code-that-may-throw-exceptions;
} // try

Next, you indicate what to do by catching the exception. A catch clause looks a little like a method definition, except that it has no name and the parameter is an exception. Later, you will learn that there are a variety of types of exceptions. For now, you can just catch generic Exception objects.

catch (Exception e) {
  what-to-do-if-the-error-occurs;
} // catch(Exception) 

Putting it all together, we might write

  /**
   * Create an object that can write to a file.
   */
  public MyOutput(String filename) {
    try {
      // Build a FileWriter to write to the given file
      FileWriter fw = new FileWriter(filename);
      // Convert it to a PrintWriter
      writer = new PrintWriter(fw, true);
    } // try
    catch (Exception e) {
      // What should we do?
    } // catch(Exception)
  } // MyOutput

You may note that we enclosed a number of lines in the try clause. This is because we only want the subsequent lines to be executed if the thing we are trying succeeds. In effect, control exits a try clause when the first exception occurs. No subsequent statements are then executed. In this particular case, we don't want to create a new PrintWriter unless we've successfully created the FileWriter.

But what should we do if we catch an exception? In some cases, it may be appropriate to print an error message to output, although such cases are rare. In this case, we're setting up output, so it makes no sense to try to print a message. Instead, we should use Java's specified mechanisms for indicating errors and throw our own exception. Note that we are basically passing the buck. If a method we use fails, then we will most likely fail. If we fail, the method that called us may fail. Eventually, we may reach a method that understands how to handle failure.

To throw an exception, you use the throw command. Typically, you throw a newly created exception. You can create exceptions using strings as parameters. The strings, in effect, describe what went wrong.

If you write a method that throws exceptions, you need to indicate this by writing throws Exception in the function header. For our constructor, we might write:

  /**
   * Create an object that can write to a file.
   */
  public MyOutput(String filename) 
    throws Exception {
    // Build a FileWriter to write to the given file
    try {
      FileWriter fw = new FileWriter(filename);
      // Convert it to a PrintWriter
      writer = new PrintWriter(fw, true);
    } // try
    catch (Exception e) {
      throw new Exception("Could not send output to '" + filename + "'");
    } // catch(Exception)
  } // MyOutput

Instead of creating a new exception, it would also be possible to just rethrow e (the exception thrown by the FileWriter constructor.

In Experiment X3.4 you will reconsider output to files.

Closing files

It is considered good programming practice to close every file that you open. By closing a file, you are indicating that you are done with the file. If you don't close files, Java will close them for you when you are done with the program. However, if you don't close files and then open them again, you may not get the results you expected.

How do you close files in Java? Every FileWriter (and, in fact, every other kind of Writer) provides a close method. All you need to do is call this method when you're done with the FileWriter. How do you know when you're done? You might add a close method to MyOutput or SimpleOutput and make it your practice to close any output object you create.

You can also take advantage of the finalize method to close files when you're done with them. However, this may not be the most reliable solution as finalize is not always called at appropriate times. (If you have not read about this method, don't worry; it's not very relevant to the current subject matter.)

Input

We've looked at some issues pertaining to output. But how do we deal with input? It turns out that some aspects of input are similar and others are different. As in the case of output, in order to do input from the keyword (or, more precisely, standard input), you will use a predefined value, in this case System.in. As in the case of output, you will most likely need to convert this InputStream to another class, in this case Reader.

What are the differences? Recall that there are a large number of print methods given by Writers. It turns out that Readers only provide methods for reading individual characters. Hence, we need to use more specialized versions and we need to find a way to convert characters or strings to numbers.

Input and System.in

System.in is Java's standard name for the standard input stream. If you read the documentation for Java, you will quickly learn that System.in is an InputStream. Input streams are intended to be used to read bytes rather than characters or strings. As you've seen from the programs that you've written, it is more common to read strings or numeric values.

We'll start with the simplest form of common input: reading strings. Java's BufferedReader class provides a readString() method that reads one line of text from input. Hence, we need to go from an InputStream to a BufferedInputReader. How? Once again, you need to do a series of conversions. This time, you will convert the InputStream to an InputStreamReader and then convert that InputStreamReader to a BufferedInputStream.

For example, if we use reader to refer to the BufferedReader that our input class will use, we might write the following constructor.

  /**
   * Create a new object that can read from standard input.
   */
  public MyInput() {
    // First, update System.in to a more modern class
    InputStreamReader tmp = new InputStreamReader(System.in);
    // Then, convert it to something that can read lines.
    this.reader = new BufferedReader(tmp);
  } // class MyInput

Once we have that constructor, we can add a readString method that simply calls reader's readString method, as in

    return reader.readLine();

In Experiment X3.5 you will begin creating your own input class.

Converting strings to other types

Unfortunately, BufferedReader only provides two input methods: read, which reads single characters, and readString, which reads one line of input text. What if we want to read an integer or other numeric value?

It turns out that the best solution is to read an input string and then convert that input string to an integer. How do you do conversion? As in many cases before, this will require a series of steps. First, you build a numeric object that corresponds to the string. For example, to build an Integer that corresponds to the string str, we would write

    Integer iobj = new Integer(str);

Similarly, to build a Float that corresponds to the same string, we would write

    Float fobj = new Float(str);

Both of these constructors can throw NumberFormatExceptions, so be careful to try the construction and catch any errors.

Next, we must convert the objects into the primitive types. We do this with the appropriate xxxValue method. For example,

    int i = iobj.intValue();
    float f = fobj.floatValue();

Putting this all together, we get

  /**
   * Read an integer on a line by itself.
   */
  public int readInt() {
    // Read a line of input
    String str = this.readString();
    // Convert it into an integer object
    try {
      Integer iobj = new Integer(str);
      // Convert it into an int value
      int i = iobj.intValue();
      return i;
    } // try to convert the string
    // Can't convert!
    catch (Exception e) {
      // Return a default value
      return -1;
    } // can't convert
  } // readInt()

or, more concisely,

  /**
   * Read an integer on a line by itself.
   */
  public int readInt() {
    // Read a line of input
    String str = this.readString();
    // Convert it into an integer object
    try {
      return (new Integer(str)).intValue();
    } // try to convert the string
    // Can't convert! Return a default value.
    catch (Exception e) {
      return -1;
    } // can't convert
  } // readInt()

You may have noted that readInt expects each numeric input to be on a line by itself. In the problems we explore a readInt method that can read an integer from part of a line.

In Experiment X3.6 you will experiment with reading integers.


Experiments

Name: ________________
ID:_______________

Experiment X3.1: Using System.out

Step 1. Using our reliable SimpleOutput class, write a program that prints your name. Enter your code here.


 
 
 
 
 
 

Step 2. Using System.out, write a program that prints your name. Enter your code here.


 
 
 
 
 
 

Step 3. What, if any, advantages do you see to using SimpleOutput? Why do you think we provided such a class?


 
 
 
 
 
 

After entering your answer, you may want to read the note on the subject.

Step 4. Using a PrintWriter, write a program that prints your name. It is likely that you will need to convert System.out to a PrintWriter with

PrintWriter out = new PrintWriter(System.out, true);

You will also need to import java.io.PrintWriter. Enter your code here.


 
 
 
 
 
 

Step 5. What do you see as the relative advantages and disadvantages of using PrintWriter objects versus using SimpleOutput objects?


 
 
 
 
 
 

Step 6. Create an ArrayPrinter class with a main method that contains the following lines:

    int[] stuff = new int[4];
    stuff[0] = 1;
    stuff[1] = 2;
    stuff[2] = 4;
    out.println(stuff);

Try to compile and execute the ArrayPrinter. If you succeed, record the output. If your fail, record and explain the error messages.


 
 
 
 
 
 

Experiment X3.2: Building your own output class

Step 1. Create a new class, MyOutput, that contains

Step 2. Create a new class, OutputTester, that will be used to test your MyOutput class. Your class should contain a main method with the following lines.

    MyOutput out = new MyOutput();
    out.println("Hello");

Compile your two classes and execute OutputTester. Record the results.


 
 
 

Step 3. Add the line

    out.println(7.2);

to the main method in OutputTester. Try to compile and execute the program. Record any error messages you receive.


 
 
 

Step 4. What do those messages suggest?


 
 
 

Step 5. Add a println(double) method to MyOutput and recompile both classes. Do you still receive the same error messages? If so, why? If not, why not?


 
 
 

Step 6. Add the line

    out.println(6);

to the main method in OutputTester. What do you expect to happen when you compile and execute your program?


 
 
 

Step 7. Try to compile and execute the program. If compilation and execution succeed, record the output. If either fails, record the error messages.


 
 
 

Step 8. Did what happened in step 7 correspond to what you suggested in step 6? Explain what happened in step 7.


 
 
 

Step 9. What do you expect would happen if we added the following lines to the main method in OutputTester?

    int[] stuff = new int[4];
    stuff[0] = 1;
    stuff[1] = 2;
    stuff[2] = 4;
    out.println(stuff);


 
 
 

Experiment X3.3: Output to files

Step 1. Add the following constructor to your MyOutput class. Note that you will need to import java.io.FileWriter.

  /**
   * Create an object that can write to a file.
   */
  public MyOutput(String filename) {
    // Build a FileWriter to write to the given file
    FileWriter fw = new FileWriter(filename);
    // Convert it to a PrintWriter
    writer = new PrintWriter(fw, true);
  } // MyOutput

Try to compile MyOutput and record the results.


 
 
 
 
 
 

When you are done, read the notes on this experiment.

Experiment X3.4: Output to files, revisited

Step 1. Update the MyOutput(String) constructor from the previous experiment to use try and catch. The body of the constructor will now be

    try {
      // Build a FileWriter to write to the given file
      FileWriter fw = new FileWriter(filename);
      // Convert it to a PrintWriter
      writer = new PrintWriter(fw, true);
    } // try
    catch (Exception e) {
      // Do nothing
    } // catch(Exception)

Try to compile the revised MyOutput and record the results.


 
 
 

Step 2. Add the following lines to the main routine of your OutputTester class. Recompile and execute OutputTester. What are the contents of the file stuff.txt?

    // Write some information to a file.
    file = new MyOutput("stuff.txt");
    file.println("Hello");
    file.println(2 + 3);


 
 
 

Step 3. Change the file name in the code from step 2 from stuff.txt to the empty string or to another illegal filename specified by your instructor. Recompile and execute OutputTester. What happens?


 
 
 
 
 
 

Step 4. Add the line

    throw new Exception("Cannot write to '" + file + "'");

to the MyOutput(String) constructor. Recompile MyOutput. If you are successful, execute OutputTester. Record either results or error messages.


 
 
 
 
 
 

Step 5. Change the method header for the MyOutput(String constructor to read

  public MyOutput(String filename) throws Exception {

Recompile MyOutput and TestOutput. If you are successful, execute OutputTester and record the results. If you are not successful, record the error messages.


 
 
 
 
 
 

Step 6. Update the main in TestOutput to use appropriate try/catch clauses. Summarize your changes.


 
 
 
 
 
 

Experiment X3.5: Starting to build your own input class

Step 1. Create a new class, MyInput, that contains

Here is the constructor we created earlier.

  /**
   * Create a new object that can read from standard input.
   */
  public MyInput() {
    // First, update System.in to a more modern class.
    InputStreamReader tmp = new InputStreamReader(System.in);
    // Then, convert it to something that can read lines.
    this.reader = new BufferedReader(tmp);
  } // class MyInput

Here is the body of the readString() method we developed earlier.

    return reader.readLine();

Compile your class. What error messages do you receive? What do these error messages suggest?


 
 
 
 
 
 

After answering this question, you may want to look at the notes on this step.

Step 2. Update readString so that it uses try and catch appropriately. Return the empty string if you catch an exception. Compile your class and correct any compilation errors. Record the code for your method here.


 
 
 
 
 
 

Step 3. Create a new class, InputTester, that will be used to test your MyOutput class. Your class should contain a main method with the following lines.

    SimpleOutput out = new SimpleOutput();
    MyInput in = new MyInput();
    out.println("Enter your name: ");
    String str = in.readString();
    out.println("You entered " + str);

Compile your two classes and execute InputTester. Enter your name when prompted to do so. Record the results.


 
 
 

Step 4. How would this code differ if you used SimpleInput rather than MyInput?


 
 
 
 
 
 

After writing your answer, you may wish to look at the notes on this step.

Step 5. Update InputTester to read two lines of text, such as a name and a major. Recompile and execute the program. Summarize your changes and record the output.


 
 
 
 
 
 

Step 6. Add the lines

  int i;
  i = in.readInt();

to the main method in InputTester. Try to compile and execute the program. Record any error messages you receive. What do these messages suggest?


 
 
 
 
 
 

Step 7. Try to update MyInput to include a readInt method that calls a corresponding method in its base reader. Try to compile the updated class. What do the error messages you receive suggest?


 
 
 
 
 
 

Experiment X3.6: Reading numeric input

Step 1. Add one of the readInt methods described above to your MyInput class. Recompile the class and correct any compilation errors. Here is one possible definition of readInt.

  /**
   * Read an integer on a line by itself.
   */
  public int readInt() {
    // Read a line of input
    String str = this.readString();
    // Convert it into an integer object
    try {
      return (new Integer(str)).intValue();
    } // try to convert the string
    // Can't convert! Return a default value.
    catch (Exception e) {
      return -1;
    } // can't convert
  } // readInt()

Step 2. Add the following lines to the main method of InputTester.

    int i;
    out.println("Enter an integer");
    i = in.readInt();
    out.println("Read " + i);

Recompile and execute InputTester. Enter the value 3. Record your results. What do those results suggest?


 
 
 
 
 
 

Step 3. Rerun InputTester using the following inputs

Record the results for each case. What do these results suggest?


 
 
 
 
 
 

Step 4. Add a readFloat method to your MyInput class. The design of this method is left to your imagination. Enter the code for the method here.


 
 
 
 
 
 
 
 
 
 
 
 
 
 

Step 5. Update InputTester to read in and print out a float. Run your program with the following inputs.

Record the results for each case. What do these results suggest?


 
 
 
 
 
 


Post-Laboratory Problems

Problem X3-A: Printing integer arrays

Add a println(int[] ints) method to the MyOutput class. This method should print the array nicely. For example, the code

    SimpleOutput out = new SimpleOutput();
    int[] stuff = new int[4];
    stuff[0] = 1;
    stuff[1] = 2;
    stuff[2] = 4;
    out.println(stuff);

should print

[1, 2, 4, 0]

Problem X3-B: Printing string arrays

Add a println(String[] strings) method to the MyOutput class. This method should print the array nicely.

Problem X3-C: Redirecting output to a different file

Add a setOutput(String filename) method to your MyOutput class that redirects output to a file. If the object was previously writing to standard output, it should now write to the given file. If the object was previously writing to another file, it should close that file and begin writing to a new file.

Problem X3-D: Redirecting output to standard output

Add a setOutput() method to your MyOutput class that allows one to stop writing to whatever was previously being written to and then begin writing to standard output. if the object was previously writing to a file, it should close that file.

Problem X3-E: Reading input from files

Update MyInput so that it can also read input from files. As in the case of MyOutput, you will need to add a constructor that takes a file name as a parameter. You will turn that file name into a FileReader and then into a BufferedReader.

Problem X3-F: Reading parts of lines

You may have noted that one significant difference between MyInput and SimpleInput is that SimpleInput objects allow you to read multiple numeric values from one line. Update MyInput to support these additional methods.

How will you do this? You'll need to keep track of the line most recently read. You can identify the positions of the spaces on that line, split the line at each space, and convert the substrings individually.

Problem X3-G: Redirecting input

Add setInput methods to MyInput that behave similarly to the setOutput methods described in previous problems.


Notes

Experiment X3.1, Step 3. Why did we provide a SimpleOutput class? Primarily because it seemed like a good way to introduce objects. Note that the first program we gave you used a SimpleOutput object. In addition, as you will see later, by using SimpleOutput objects, we can easily change our programs to write to files rather than to the screen. It is also possible to extend the SimpleOutput object to provide additional capabilities. For example, we can add a method to print arrays. Finally, by relying on a single class that we developed, we can easily update all of our programs when Java incorporates a new output mechanism.

Experiment X3.3, Step 1. It is likely that the compilation will fail. If your error message indicates that it did not understand about the FileReader, then you forgot to import that class. Once you've fixed that error, you will now receive an error about an exception that must be caught. We discuss this error in the section titled ``Exceptions''.

Experiment X3.5, Step 2. It turns out that readString throws an exception. Exceptions are typically caused when you attempt to read from beyond the end of input. Hence, we need to try the call to readString and catch an exception.

Experiment X3.5, Step 4. As long as the program only reads and writes lines of text, there should be no difference between a program that uses our newly developed classes and one that uses SimpleInput. On the other hand, since we have not yet written methods to permit reading of numeric values, there will be some differences.


Copyright (c) 1998 Samuel A. Rebelsky. All rights reserved.

Source text last modified Wed Oct 6 12:42:00 1999.

This page generated on Tue Oct 26 15:38:49 1999 by Siteweaver.

Contact our webmaster at rebelsky@math.grin.edu