CMPU 102, Fall 2005 Lecture 3

This lecture introduces the idea of inheritance.  It also explains Java packages, and reviews Java I/O and exceptions.

Outline

  1. Is-A relationships
  2. Inheritance in Java
  3. Fields and methods in the superclass are inherited by subclasses
  4. Packages and imports
  5. Input and Output
  6. StringTokenizer
  7. Exceptions
  8. Handling exceptions
  9. Finally

Is-A relationships

Inheritance is a mechanism for expressing "Is-A" relationships in an Object-Oriented programming language.  Here is an example of two "Is-A" relationships:

A Dog Is An Animal

A Cat Is An Animal

The idea here is that both "Dog" and "Cat" have some common characteristics which are subsumed under the general notion of "Animal".  We can represent this pictorially in an inheritance diagram:

In Java terms, we would say that Animal, Dog, and Cat are all classes, and that Animal is the base class, superclass, or parent class.  (All of these terms are equivalent.)  Dog and Cat are derived classes, subclasses, or child classes.  (Again, these are equivalent terms.)

Generally, a superclass will define methods which subclasses override.  Overriding a method means that a subclass has defined its own implementation of the method.  This is one of the main ideas behind Object-Oriented programming.  The base class specifies what operations instances of subclasses can perform, and the subclasses themselves define how those operations are carried out.

Inheritance in Java

Here are some concrete examples of Java classes that use inheritance.  First's, let's define the base class Animal.  We will make this an abstract class, which means that it is not possible to create an instance of the Animal class using the new operator.  (This is often the case with base classes: we are just trying to develop the notion of some general category of classes, but we will leave the concrete implementation details to the subclasses themselves.)  Here is how we might define the Animal class:

public abstract class Animal {
    public abstract void vocalize();
}

The vocalize method will be defined by subclasses to make whatever kind of sound is appropriate for the kind of animal the subclass represents.

Now we can define the subclasses Cat and Dog.  We will make these classes concrete, meaning that it will be possible to create objects which are instances of Dog and Cat.

public class Dog extends Animal {
    public void vocalize() {
        System.out.println("woof");
    }
}

public class Cat extends Animal {
    public void vocalize() {
        System.out.println("meow");
    }
}
For a class to be concrete, it must override all abstract methods defined in any of its superclasses.

Now we can write code that uses the Dog and Cat classes.

Animal lassie = new Dog();
Animal felix = new Cat();

lassie.vocalize();  // Prints "woof"
felix.vocalize();   // Prints "meow"

Note that something interesting is happening: we are creating instances of classes called Dog and Cat, but we are assigning references to those objects to variables whose type is Animal.  This works because Dog is an Animal, and Cat is an Animal.  This illustrates one of the general principles of Object-Oriented programming:

An instance of a subclass may be used anywhere an instance of the superclass is allowed
This is known as the Liskov substitution principle.  This principle means that you can, for example:
  1. Pass a Dog object to a method which takes an Animal as a parameter
  2. Return a Dog object from a method which returns an Animal
  3. Assign a Dog to a reference of type Animal

Note that the converse is not true: instances of a superclass may not be substituted for instances of a subclass.  For example, the following code is illegal:

public void someMethod(Animal animal) {
    Dog dog = animal; // not allowed!
}

Fields and methods in the superclass are inherited by subclasses

A base class can define its own fields and methods.  These fields and methods are inherited by subclasses.  That's why it's called inheritance!  Let's say we want to change our Animal class to represent the idea that all animals have a name:

public abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public abstract void vocalize();
}

Now that our base class has an explicit constructor, we need to change the subclasses Dog and Cat so that they call it in their own constructors:

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    public void vocalize() {
        System.out.println("woof");
    }
}

public class Cat extends Animal {
    public class Cat(String name) {
        super(name);
    }

    public void vocalize() {
        System.out.println("meow");
    }
}

The syntax super(args...) allows the constructors in subclasses to invoke a constructor for their immediate superclass.

Because the subclasses inherit methods from the superclass, you can call superclass methods on an instance of a subclass.  For example:

Dog lassie = new Dog("Lassie");

System.out.println(lassie.getName() + " makes a sound: " + lassie.vocalize());
// Prints "Lassie makes a sound: woof"
Even though the getName method is defined in the base Animal class, it may be called on subclasses such as Dog.

Packages and imports

In Java, a package is a namespace for a collection of related classes.  The main purpose of packages is to allow Java classes written by many different people and organizations to be used together in the same program.  Each Java class belongs to a particular package, and the fully qualified name of a Java class includes the package name.

Examples:

Java package names consist of a series of words (usually lower case) separated by dots (the "." character).  The preferred way to name a package is to reverse the components of an internet domain name---for example, "cs.vassar.edu", the Vassar Computer Science Department, would become "edu.vassar.cs"---and then add additional words to describe the purpose of the package.

To define the package that a Java class is in, the first declaration in the source file defining the class should be:

package name;
where name is the name of the package.

So, why are packages useful?  Imagine that we want to use some Java classes produced by the XYZ company in a program.  For example, their classes might be in the package com.xyz.zoo, and one of the classes in that package might be called "Animal".  We can use this class in the same program as another class called "Animal" in the "edu.vassar.cs.cs102" package, because the fully qualified names of the two classes are different.

When you are implementing a class in Java, you can refer to other classes in the same package without doing anything special.  However, if you want to refer to a class in a different package, then you have several options.  Let's say you want to use the class Animal from the com.xyz.zoo package.  You could specify the fully qualified name of the class wherever you need to refer to it:

package edu.vassar.cs.cs102;

public class Example {
    public static void main(String[] args) {
        com.xyz.zoo.Animal animal = new com.xyz.zoo.Animal();
    }
}
As you can imagine, this approach is tedious.

An easier solution is to import the class you need:

package edu.vassar.cs.cs102;

import com.xyz.zoo.Animal;

public class Example {
    public static void main(String[] args) {
        Animal animal = new Animal();
    }
}

You can also import all of the classes in another package:

package edu.vassar.cs.cs102;

import com.xyz.zoo.*;

public class Example {
    public static void main(String[] args) {
        Animal animal = new Animal();
    }
}

There is one exception to the rule about referring to classes in another package: classes in the java.lang package are always visible.  You do not need to import them.  For example, when you declare a variable of type String, that really means java.lang.String.

Input and Output

Java provides a number of classes for input and output.  They are defined in the java.io package.

The InputStream and OutputStream classes, and their subclasses, perform input and output on bytes.  The Reader and Writer classes, and their subclasses, perform input and output on characters---in other words, text.  So, Reader and Writer objects are generally what you will want to use for human-readable input and output.

The InputStreamReader and OutputStreamWriter classes allow you to take an InputStream or OutputStream, and use it as a Reader or Writer, respectively.

The BufferedReader class is a particularly useful kind of Reader, because it allows you to read entire lines of input at a time.

Here is a complete example using a BufferedReader to read each line of input from the System.in object (which is an InputStream), and then print them out to System.out:

package edu.vassar.cs.cs102;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class ReaderDemo {

    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(System.in));

        while (true) {
            String line = reader.readLine();

            if (line == null) {
                break;
            }

            System.out.println(line);
        }
    }
}

StringTokenizer

The java.util.StringTokenizer class is useful for breaking up a String into tokens.  Usually, the tokens are chunks of non-space characters---i.e., words---separated by space characters.  Here is an example:

package edu.vassar.cs.cs102;

import java.util.StringTokenizer;

public class StringTokenizerDemo {
    public static void main(String[] args) {
        String s = "Hello CMPU 102";

        StringTokenizer tokenizer = new StringTokenizer(s);
        while (tokenizer.hasMoreTokens()) {
            String token = tokenizer.nextToken();
            System.out.println(token);
        }
    }
}

This program will print the lines

Hello
CMPU
102

Exceptions

An exception in Java is a special kind of object that represents an "exceptional" situation, where the current computation cannot proceed for some reason.  For example, many of the methods that perform input or output are defined to throw IOException.  An IOException might mean that the file you are reading from is corrupted.

In Java, exceptions must be dealt with in one of two ways.

  1. First, you can add a throws clause to the method you are writing, where an exception might be thrown.  This means that if a particular exception can arise inside the method, or in any other method called by the method, then the exception is allowed to be thrown out of the method.

  2. Second, you can write an exception handler.  An exception handler specifies some code that recovers from the exception, and potentially allows the program to continue executing.

In general, adding a throws clause is the best approach to dealing with possible exceptions.  For example, if you look carefully at the definition of the ReaderDemo class above, you will note that its main method has been defined to throw IOException.  This is because BufferedReader's readLine method can throw IOException.

Handling exceptions

However, sometimes you will want to handle an exception.  Let's say we wanted to rewrite ReaderDemo so that it actually handles any IOException that might be thrown.  We do so by adding a try/catch block:

package edu.vassar.cs.cs102;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

public class ReaderDemo {

    public static void main(String[] args) {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(System.in));

        try {
            while (true) {
                String line = reader.readLine();
    
                if (line == null) {
                    break;
                }
    
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Caught an IOException!");
        }
    }
}

The general form of a try/catch block is this:

try {
    code that can throw an exception
} catch (ExceptionType e) {
    code that handles ExceptionType
} catch (AnotherExceptionType e) {
    code that handles AnotherExceptionType
}

Multiple catch blocks can be added to catch whatever types of exceptions are thrown by the code in the try block.

Finally

Finally blocks may be used in addition to or in place of catch blocks (but in any case, the finally block must come after any catch blocks).  The code in a finally block is always executed following the execution of the try block, even if an exception is thrown from the try block.  Finally blocks are often used to run cleanup code.