Lecture notes for Monday, 7/21

Throwing Exceptions

We talked very briefly about throwing your own exceptions on Thursday, but we didn't go into it in much depth. Mostly, you had to figure that out on your own as you worked your way through the homework assignment. But I figured it would be good to talk about it in the notes too.

So far, we've mostly been worrying about dealing with exceptions when they are thrown to us. But the exceptions have to start somewhere. When we call a method and an exception gets thrown, that means that somebody had to throw it. And by somebody, I mean some method.

So suppose we had a string named someText and we tried to get a substring using the command someText.substring(4,12), but it threw an exception. Let's say it threw an exception of type StringIndexOutOfBoundsException with the message "String index out of range: 12". This isn't something that magically appears. Somewhere in the code for the substring() method (or in one of the methods that substring() called), there is a line of code that tells the Java machine to throw an exception. It probably looks something like this:

throw new StringIndexOutOfBoundsException( "String index out of range: 12");

Actually, the number 12 will come from a variable storing the second argument for the method, but you get the idea. Exceptions happen when code explicitly throws them. And we can just as easily throw them ourselves.

In your homework assignment, you were asked to create a Fraction class and to define some methods for it, including a division method. Now if you were writing in a programming language that didn't allow you to generate your own errors or exceptions, there wouldn't necessarily be a good solution for what to do when the user tries to divide by zero. You could return some kind of null or not-a-number special type, but then you'd have to also add code for how to deal with arithmetic involving that special type too. When you've got exceptions, the solution is simple: you create an Arithmetic Exception object with a message like "divide by zero" and you throw it.

It's important not to confuse the throw keyword with the throws keyword. They look very similar, but they show up in completely different places. You use throws in the declaration of a method (before the method is even defined) to say that any exception (of that particular type) that shows up when running that method will just be passed on up the line to whatever function called your method. You use throw in the body of a method definition to declare that execution of the method needs to stop right there and a particular exception with a particular message is thrown up to whatever function called your method.

Make sure you know the difference!

You can even define your own classes of exceptions, but if there's a good existing exception, you should always use that one. Even if there isn't a good existing exception, you should find the one that's closest to what you want, and make your new exception class into a subclass of that. Anyway, defining your own exceptions is beyond the scope of this class, but throwing exceptions is not.

Reading and Writing from Files

I wasn't there to teach the lecture (I threw out my back and wouldn't have been able to walk far enough to make it to the classroom), so Karteek took over the lecture. He may have used a slightly different technique for reading and writing from text files than I would have, but that's not necessarily a bad thing. There are often many ways of doing the same thing in a programming language. There are often many good ways of doing the same thing, so it's good to see a variety of techniques for accomplishing the same task. For the record, here's the way I would write such a program:

import java.util.*;
import java.io.*;

public class Copier {
  public static void main(String[] args) {
  
    BufferedReader r = null;
    PrintWriter w = null;
  
    try {
    
      r = new BufferedReader(new FileReader("inputfile.txt"));
      w = new PrintWriter("outputfile.txt");
      String line;
      while ( r.ready()) {
        line = r.readLine();
        w.println(line);
      }
      
    } catch(IOException ex) {
    
      System.out.println("There was a problem with one of the files.");
      System.out.println(ex);
      
    }
    
    try {
      r.close();
    } catch(IOException ex) {
      System.out.println("There was a problem closing the input file.");
      System.out.println(ex);
    }

    w.close();
    
  }  
}

Much of this we've seen before, but some of it is worth talking about. There are two important new classes of objects here: PrintWriter and BufferedReader.

PrintWriters

We've seen something very similar to a PrintWriter before. System.out is technically a PrintStream, but PrintStreams and PrintWriters are very similar.* The difference that's important here, is that we've created a special PrintWriter that writes to a file. When we write the command new PrintWriter("outputfile.txt"), we're calling a special PrintWriter constructor which treats the string argument as the name of a file. It automatically opens a file named outputfile.txt (creating it if it doesn't already exist) and then prepares it to be written to.

*If you want to get technical about it, a PrintStream writes to a stream byte by byte, while a PrintWriter writes to a stream character by character. Unless you're worried about what kind of encoding you're using, you probably don't have to worry about the difference between them.

In class with Karteek, I believe you used a slightly different constructor for PrintWriter. You first created a File object with the line File output = new File("output.txt");, which created the file if it needed to, and then you created a FileWriter object with a command like new FileWriter(output). And finally, you used that FileWriter object to make the PrintWriter object. Both techniques work. The one I use has the advantage of being simpler to write, but it has the disadvantage that it does a lot of things behind the scenes, where you can't get at them if something goes wrong.

What's really imporant here is that we can use our PrintWriter w in almost the exact same way as we use the PrintStream System.out. In particular, we can use the methods print() and println(). Since we're going to copy our file one line at a time, we'll use println().

BufferedReaders

You're already familiar with the System.in object, which we use to get input from the keyboard. Technically, System.in is an InputStream, and any of the methods that work for System.in (such as nextLine() or nextFloat()) will work for any InputStream.

Unfortunately, the similarity between InputStreams and BufferedReaders (which are a subclass of Readers) isn't quite as exact as with PrintStreams and PrintWriters. But they are similar, and the method readLine() works in the same way that nextLine() does.

Note that unlike with the PrintWriter, the BufferedReader constructor can't just be called using a string (well, not if you want it to treat that string as a filename). We have to make a FileReader first, and use that to create the BufferedReader.

Working with Files and I/O Exceptions

We talked about exceptions in more detail last Thursday, but it's worth talking about why I laid out the try-catch blocks the way I did. Mostly it boils down to making sure that any input or output stream that gets opened also gets closed. I could have put essentially the whole program into one big try block, but then I'd be running the risk of leaving one of the streams open.

For small applications like the ones we're creating in class, this isn't such a big deal, but for bigger applications or applications with lots of users, this can be a problem. Why? Well, every open I/O stream uses up some resources on the computer (such as file handles), and if you leave enough of them open, you'll eventually run out of those resources. What this means for us is that you should get into the habit of always making sure that streams get closed when you're done with them.

So why didn't I just use one big block? Something like this:

import java.util.*;
import java.io.*;

public class Copier {
  public static void main(String[] args) {
  
    try {
    
      BufferedReader r = new BufferedReader(new FileReader("inputfile.txt"));
      PrintWriter w = new PrintWriter("outputfile.txt");
      String line;
      while ( r.ready()) {
        line = r.readLine();
        w.println(line);
      }
     
      r.close();
      w.close();
      
    } catch(IOException ex) {
    
      System.out.println("There was a problem with one of the files.");
      System.out.println(ex);
      
    }
    
  }  
}

To see how this might result in a stream being left open, let's walk through the code for a situation where an exception might occur. Suppose that the input and output files are successfully opened and the program starts reading lines from the input file and writing them to the output file. And then, when it's halfway through the input file, something happens to throw an I/O exception (maybe somebody else moved one of the files, or the disk that one of the files was on was unplugged). What does the program do? Well, as soon as that exception gets thrown, execution on the try block is immediately halted and the exception is caught by the catch block. The error message is printed, and the program ends without ever reaching the commands needed to close the streams.

"Okay," I hear you say. "That makes sense, but why don't we just put the closing commands outside of the try-catch block entirely? Or even in a finally block?" A good thought. That would look something like this:

import java.util.*;
import java.io.*;

public class Copier {
  public static void main(String[] args) {
  
    try {
    
      BufferedReader r = new BufferedReader( new FileReader("inputfile.txt") );
      PrintWriter w = new PrintWriter("outputfile.txt");
      String line;
      while ( r.ready()) {
        line = r.readLine();
        w.println(line);
      }
      
    } catch(IOException ex) {
    
      System.out.println("There was a problem with one of the files.");
      System.out.println(ex);
      
    }
     
    r.close();
    w.close();
    
  }  
}

If you try this one, it won't even compile. There are a couple reasons for that. The biggest reason is that there's a chance that r and w don't even exist! If an I/O exception gets thrown right at the very beginning (say while it's creating the FileReader object), then the block never gets around to defining r and w. The compiler recognizes that possibility, so it won't let you refer to r and w outside of that try block.

"A simple enough fix!" you declare. "We'll just move the declarations for r and w outside of the try block! Like this:"

import java.util.*;
import java.io.*;

public class Copier {
  public static void main(String[] args) {
  
    BufferedReader r;
    PrintWriter w;
    
    try {
    
      r = new BufferedReader( new FileReader("inputfile.txt") );
      w = new PrintWriter("outputfile.txt");
      String line;
      while ( r.ready()) {
        line = r.readLine();
        w.println(line);
      }
      
    } catch(IOException ex) {
    
      System.out.println("There was a problem with one of the files.");
      System.out.println(ex);
      
    }
     
    r.close();
    w.close();
    
  }  
}

It's a good solution to the problem, and yet it still won't compile. Now even though r and w have been declared, the compiler will complain that they might not be initialized.

"A trivial fix! Just initialize them to be null! Like so:"

import java.util.*;
import java.io.*;

public class Copier {
  public static void main(String[] args) {
  
    BufferedReader r = null;
    PrintWriter w = null;
    
    try {
    
      r = new BufferedReader( new FileReader("inputfile.txt") );
      w = new PrintWriter("outputfile.txt");
      String line;
      while ( r.ready()) {
        line = r.readLine();
        w.println(line);
      }
      
    } catch(IOException ex) {
    
      System.out.println("There was a problem with one of the files.");
      System.out.println(ex);
      
    }
     
    r.close();
    w.close();
    
  }  
}

Again, we're heading in the right direction, but unfortunately, the compiler is still going to complain. This time, it's about about an unchecked exception that might crop up while closing the BufferedReader.* What this means is that sometimes just closing a stream can go wrong and an exception might get thrown. We need to catch that exception.

I don't know why we don't have to worry about the PrintWriter misbehaving when we try to close it, but for whatever reason, the close() method for PrintWriter doesn't throw any IOExceptions, so we don't have to worry about that.

And I'll just jump ahead and point out that we should have a separate try-catch block for both close() statements, because if the first one fails, we still want to try to close the second one. And if we do that, we end up right back where I started, with three separate try-catch blocks. On the plus side, this means we can give more detailed error messages, depending on which try block fails. If we wanted to be really specific, we could break the code up into even more try-catch blocks with even more specific error messages. But what we have is good enough to guarantee that we at least try to close all the streams we opened.

I hear one last concern: "Why didn't you put the stream-closing statements into a finally block?"

This is an excellent question, by the way. Some people always put their close commands into a finally block, like this:

import java.util.*;
import java.io.*;

public class Copier {
  public static void main(String[] args) {
  
    BufferedReader r = null;
    PrintWriter w = null;
  
    try {
    
      r = new BufferedReader(new FileReader("inputfile.txt"));
      w = new PrintWriter("outputfile.txt");
      String line;
      while ( r.ready()) {
        line = r.readLine();
        w.println(line);
      }
      
    } catch(IOException ex) {
    
      System.out.println("There was a problem with one of the files.");
      System.out.println(ex);
      
    } finally {
    
      try {
        r.close();
      } catch(IOException ex) {
        System.out.println("There was a problem closing the input file.");
        System.out.println(ex);
      } finally {
        w.close();
      }
      
    }
    
  }  
}

And this would be just fine. It's probably even better from a good habits point of view. Don't be afraid to nest try-catch-finally blocks. Sometimes it's the right thing to do, even if it makes the code harder to read.

In this particular case, it isn't absolutely necessary, because none of the try or catch blocks have any commands that might cause the program to be interrupted before it gets to those last closing statements. If one of the blocks had a break, return, or continue command, then the program might never get around to running what happens after the first try-catch block. Similarly, if there were commands that might throw an exception after that first try-catch block but before the blocks that close the streams, then that might interrupt execution before they ever got a chance to close them. In either of those cases, it would definitely be better to do all the stream closing inside of a finally block. Doing so guarantees that they'll be run, no matter what else happens.