Lecture notes for Thursday, 7/17

Access Level Modifiers

Yesterday the subject of access level modifiers like private, protected, and public came up, and I promised to get back to you with better details. When it comes to the member variables and methods of a class, there are actually four different levels of access permissions. There are the three explicitly declared levels private, protected, and public, as well as the default level that you get when you don't specify one of these three (it's sometimes called package-private, but that's not a terribly common term).

We've already seen that when you declare a variable or method to be private, it can be only be used within the definition of the class that it belongs to. Yesterday, we made the priceInCents variable private to sort of hide the internal implementation details from anyone who wants to use our Book class. We also made most of the methods explicitly public, meaning that bit of code anywhere can use them. But for most of the member variables, we just left them without any explicit access level (I'll just call this the default level). At the default level, a variable can be accessed anywhere within the same package. We won't be getting into the details of writing packages, but for our purposes, all the stuff that compiles at once counts as the same package, so you aren't likely to notice any difference between public and default.

Now yesterday, I got the default and protected types backwards. In fact, protected members are more easily accessible than those with the default access level, but they're not quite as accessible as public members. In addition to being accessible from elsewhere in the package, they're also accessible outside of the package by any subclass of your class. So if you were to package up our Book class, and someone were to use that class as a starting point for a special kind of book (maybe a textbook), then they could directly access any protected variables from within their textbook class.

We'll talk more about subclasses later today, but here's a table summarizing the four different access levels in Java:

Has access?
Modifier Class Package Subclass Everyone
public Yes Yes Yes Yes
protected Yes Yes Yes No
no modifier Yes Yes No No
private Yes No No No

We also talked about the toString() method, but I've incorporated that into yesterday's lecture notes.

Inheritance

Speaking of subclasses, it's now time to talk about another important feature of object-oriented programming: inheritance. In OOP, inheritance refers to the way that subclasses retain the properties of the classes that they're based upon. There are many reasons why this is a useful idea, but the big one is that it cuts down on redundant code that is redundant. Whenever you find yourself writing a class and you realize that you're writing some of the same variables and members as in another class, you should consider making one of them a subclass of the other (or making them both subclasses of the same superclass).

Today, we're going back to the Book class we defined yesterday. For ease of reference, here's the class as we left it. (Or at least something very similar to that.)

public class Book {
  String isbn;
  String title;
  String author;
  private long priceInCents; 

  public Book(String i, String t, String a, double PriceInDollars) {
    this.isbn = i;
    this.title = t;
    this.author = a;
    this.price = Math.round(PriceInDollars * 100);
  }
  
  public Book(String i, String t, double PriceInDollars {
    this(i, t, "unknown", p);    
  }

  public String toString() {
    return this.title + ", written by " + this.author;
  }

  public void percentDiscount(double discount) {
    this.setPriceInDollars( (1-discount) * this.getPriceInDollars() );
  }

  public double getPriceInDollars() {
    return this.price / 100.0;
  }

  public void setPriceInDollars(double dollarPrice) {
    this.price = Math.round(dollarPrice * 100);
  }
}

Today, we're going to create a special kind of book, a graphic novel*, which has all of the features we defined in the Book class (ISBN, title, author, price), but also has an artist associated with it. We'll use inheritance to create a the new class GraphicNovel as a subclass of the class Book, and GraphicNovel objects will automatically get everything (except the constructors) that Book objects have. Then we'll add and modify whatever we need to add the extra information and functionality.

*If you're unfamiliar with the term "graphic novel", all you really need to know is that a graphic novel is essentially a comic book (or a bunch of comic books), collected into a book format.

Here is our definition of the GraphicNovel class, which goes into it's own file named GraphicNovel.java.

public class GraphicNovel extends Book {
        String artist;

        public GraphicNovel(String i, String t, String au, String ar, double p) {
                super(i,t,au,p);
                this.artist = ar;
        }

        public String toString() {
                return this.title + ", written by " + this.author + " and drawn by "
                         + this.artist;
        }
}

The extends syntax is pretty clear cut, but it's worth talking about what we've done (and what we haven't done) here. We don't have to redefine all the member variables (isbn, title, author, and priceInCents), nor all the methods (percentDiscount, the getter and setter for the price). Those come for free because this class is an extension of the Book class. (Extension is really just another word for "subclass".

A note about terminology: Never say that something "is a subclass" without mentioning what it's a subclass of. A subclass isn't a thing that a class can be; it's a way that one class is related to another. So we can say "GraphicNovel is a subclass of Book" or "Book is a superclass of GraphicNovel.

The main things that we don't automatically get from the Book class are its constructors. This makes sense because the Book constructors don't know anything about artists. But we can use the Book constructors by calling the method super() ("super" here is short for "superclass). This way, we can declare the new part (this.artist = ar;) and use the Book constructor to do the rest (super(i,t,au,p);

The other thing we did here was to make our own version of the toString() method, in order to work the artist into it. This is similar to overloading because we're giving a new definition of an existing method, but it's not exactly overloading because the arguments are exactly the same (in this case, there are no arguments). This is called overriding. It only makes sense to override a method from a superclass of the class you're defining. If you try to define two different methods in the same class with the same type and number of arguments, Java won't know which one to use when, and so you'll get an error (probably at compile time). But it does make sense for a subclass of to override the methods of its superclass. Java will use our new toString() method for converting GraphicNovel objects to strings, and it will use the old toString() method for converting to strings Book objects that are not also GraphicNovel objects.

You should add some lines to the tester to test these out, but I don't think you need me to walk you through that.

Abstract Classes

Now suppose that our book store wanted to branch out and start selling CD's in addition to books. We could create a CD class, with variables for title, recording artist, and price. We'd definitely want to reuse the code we've already written for the Book class (in our case, the price stuff, and maybe the title), but it's not clear which one should be a subclass of the other. CD shouldn't be a subclass of Book because CD's don't have authors or ISBN's. On the other hand Book shouldn't be a subclass of CD because books don't have recording artists. So if we want to avoid rewriting code, we need both CD's and Books to be subclasses of a different class, a class consisting primarily of things that can be bought. Since our store doesn't sell anything else, this class won't ever get any objects. It's just an abstract class that is only used to create subclasses. It might have a definition that looks like this: public abstract class Buyable { ... }. The abstract modifier ensures that no one can instantiate it. It's only there to act as a superclass for non-abstract subclasses to be extended from.

Multiple Inheritance

Some object-oriented programming languages (notably C++) allow subclasses to extend more than one superclass. So if you had classes Biography and GraphicNovel you could create a ComicBookBiography that was an extension of both classes. But this is not allowed in Java. You can accomplish something similar using interfaces, but that's a topic for later in the class.

Exceptions

Sorry it took me so long to write this up. You may also find it useful to look at the official tutorial from Oracle on Java exceptions.

In class, I used an example based on our Book class, but I've decided that this wasn't the best example, so I'm using different examples here in the notes.

We've seen exceptions many times before, but only when they cause our programs to crash. One of Java's important features is the set of tools it gives us to work with exceptions. If we write our code well, we can make it so that exceptions don't completely crash our programs.

There are many different types of exceptions and exception-like things (they're called throwables) in Java, and they all can be dealt with in the same way, but there are some important differences. There's a class for each type of exception, and many different subclasses that get more specific. They're all subclasses of the class Throwable. (When an exception happens, we say that it is thrown.) This table is nowhere near comprehensive, but it should give you some idea of the possibilities. I've given descriptions for some of the exceptions and errors that are relevant to what we're doing in class or that are generally important.

There's some confusion about what's a "checked" exception, and what's an "unchecked" exception. The issue is this: when you explicitly catch an exception with a try-catch block or explicitly pass it along using the throws keyword, the exception becomes "checked". So the "unchecked exception" error is what you get if you don't explicitly handle the exception. Exceptions that are not runtime exceptions need to be checked. So some people call those non-runtime exceptions "checked" exceptions, because the program won't compile unless they're checked. On the other hand, runtime exceptions are always automatically passed on up (even if you don't explicitly use the throws keyword). So other people call non-runtime exceptions "unchecked" because they're not automatically "checked": they have to be explicitly "checked". This is all terribly confusing, but what you need to know is that runtime exceptions and errors will be passed on up automatically, while every other kind of exception needs to be handled explicitly, or the program won't compile.

Passing the Buck (with the "throws" keyword)

What do I mean by "explicitly handled"? Well there are two ways that a Java method can deal with an exception when it is thrown. The first way is for the method to just stop whatever it's doing and pass the exception on up to whatever function called the method. If the method in question is the main() method, then that means that the program will crash and display the exception message. Even when it's not the main() method, this is a pretty drastic way of dealing with a problem because it means that the method doesn't get a chance to finish what it was doing. Of course, sometimes this is exactly what you want to happen. Sometimes when an exception is thrown, you want the method to stop whatever it's doing.

For example, suppose that you've got a web browser written in Java, and the browserOptions() method says that when the user clicks on the "display source code" button, it should call the method displaySourceCode(), which is supposed to read in the source code of a web page and then display the source code on the screen. And suppose that when displaySourceCode() tries to read the web page, it is thrown a FileNotFoundException saying that the web page doesn't exist. As soon as that happens, it no longer makes sense for displaySourceCode() to continue on and try to display the source code, since there isn't any source code to display! So displaySourceCode() shouldn't try to catch the exception. It should just pass the exception on up to the browserOptions() method, which will hopefully deal with the exception gracefully.

Fortunately, for many kinds of exceptions and errors, this is what happens automatically. For all unchecked exceptions (this includes all errors, and all runtime exceptions), you don't have to do anything to get this kind of behavior. But for any method that might throw a checked exception (that means anything that isn't an error or a runtime exception), we have to mention that this is a possibility when we declare the method. The declaration goes after the list of arguments, but before the {} block that defines the method itself. So in our displaySourceCode() example, the definition of the method might start like this:

public void displaySourceCode() throws FileNotFoundException {
  ...
}

Of course, there are a lot of other exceptions that might get thrown by this method, including UnknownHostException, MalformedURLException, and many others. We could list all of the possibilities like this:

public void displaySourceCode() throws FileNotFoundException, UnknownHostException, 
                                         MalformedURLException, ... {
  ...
}

Or, we could note that all the exceptions we want to pass on are subclasses of IOException, which means that we can just pass on any kind of IOException that might show up:

public void displaySourceCode() throws IOException {
  ...
}

Note that the compiler won't compile your class if you write a method without a throws declaration and you do something in your method that might throw a checked exception, unless you catch the exception (see below). Some common commands that throw checked exceptions include: creating a new URL object, doing just about anything with a file, and doing just about anything with a website.

If you declare that main() throws a particular kind of exception, you're saying that it's okay for the entire program to crash when that exception happens. This is rarely a good idea, but if you're just writing a really quick program to test something out, and you want the compiler to stop complaining about possible exceptions, well, then I won't tell anyone. Just don't do it in production.

Catching Exceptions (with try-catch-finally blocks)

The second way to deal with an exception is to catch it, providing an alternate set of instructions for what to do when the exception happens. This allows a method to keep on running, even when something unexpected happens. Using the same example as before, we might decide that the browserOptions() method should not simply pass on any I/O exception it encounters. Instead, it should display an error message and prompt the user to fix the URL, allowing them to try again. We would accomplish this, by enclosing all the commands that might throw an exception inside of a try block, and putting the commands for printing the error message and such into a catch block. A try-catch block has syntax very similar to an if-else if block:


try {
  currentURL.displaySourceCode();
} catch (IOException ex) {
  System.out.println("Couldn't access the current web page. Please enter a new URL.");
  //Code for changing the URL goes here...
}

Note that we gave the exception object a name (ex, in this case) in the declaration of the catch block. We can access information about the exception by referring to ex. For example, most error methods come with a "detail message" with some information about the specific kind of error and what code happened. We can retrieve that message with the getMessage() method. So if the URL was "htttp://www.google.com", then a MalformedURLException might get thrown, and we could access that detail message with the command ex.getMessage(). Another useful method for all throwable objects is printStackTrace(), which displays a trace of the stack of methods that led to this particular method being called. And since all throwable objects have a toString() method, we can display a bunch of information about the exception ex just by writing System.out.println(ex);.

If you want to do something different for different kinds of exceptions, you can just keep adding catch blocks, just like you can keep adding else if blocks:


try {
  currentURL.displaySourceCode();
} catch (MalformedURLException ex) {
  System.out.println("Invalid protocol. Please enter a new URL.");
  //Code for changing the URL goes here...
} catch (IOException ex) {
  System.out.println("Couldn't access the current web page. Please enter a new URL.");
  //Code for changing the URL goes here...
}

And finally, after the try block and all the catch blocks, you can put in a finally block, which will be executed after the try block or one of the catch blocks is executed, even if there was an exception. Now in many situations, there's no difference between putting something in a finally block and putting it after the try-catch block, but when the whole thing is inside a loop, it's common for one of the blocks to include a break statement that would leave the loop. In a situation like that, any code that came after the try-catch block (but still inside the loop) would never get executed. Whenever you have some sort of input or output stream that needs to be opened and closed, it's a good idea to put the commands for closing the stream into a finally block, so that the stream gets closed, no matter what else happens. You'll see an example of this in action in the notes for next Monday's lecture.

An Example

Here's a fully worked out example. You don't have to understand everything about it. (All the nonsense with the BufferedReader will make more sense after Monday's lecture.) But you should be able to see what's going on with the exceptions.

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

public class Browser {
  public static void main(String[] args) {

    System.out.println("Note that all features except the source code viewer are currently unimplemented.");
    
    Scanner s = new Scanner(System.in);
    
    String address = null;
    
    System.out.println("Please enter a URL.");
     
    while (address == null) {
      try {
        address = s.nextLine();
        displaySourceCode(address);
      } catch (IOException ex) {
        System.out.println("I couldn't access that URL.");
        System.out.println(ex);
        System.out.println(" Please try a different URL.");
        address = null;
      }
    }
    
  }
  
  public static void displaySourceCode(String addressString) throws IOException {
     
    URL currentAddress = new URL(addressString);

    BufferedReader r = new BufferedReader( new InputStreamReader( currentAddress.openStream() ) );
    
    while (r.ready()) {
      System.out.println(r.readLine());
    }
    
  }
}

Notice that the static method displaySourceCode() just passes along any exceptions that get thrown to it. But in the main method catches them using a try-catch block. The whole thing is enclosed in a while loop that keeps asking for input until it gets one that works. Every time an exception is caught, it prints out the exception's error message, asks for a new URL, and resets address to null, so that the while loop continues to loop. The while loop that inside the displaySourceCode() method just goes through the source code line by line, printing each line to System.out.