Java & Type Checking of OO Languages

Introduction

We can think of Java as a nicer/cleaner version of C++, that has incorporated many of the nice features of Java, while keeping the basic C++ syntax. That said, it's not as purely OO as Smalltalk. Highlights:
  1. Most data structures and values are objects.
  2. Most methods are dynamically dispatched (aka virtual, in C++ lingo)
  3. Garbage collected, no explicit pointers; everything is an implicit pointer, as in Scheme, ML, Smalltalk
  4. Strong, static typing: subtype polymorphism
  5. No first-class functions (blocks in Smalltalk), but has anonymous classes that can help do the same thing.
  6. C-style control and syntax (if, for, while, etc).
Additionally, Java includes a rich framework of standard data structure and graphics/gui components. Also based on a portable virtual machine.

An example

Everyone's favorite, a stack:
public class Stack extends Object {
  int top;
  Object[] items;

  public Stack(int size) {
    this.top = 0;
    this.items = new Object[size];
  }

  public void push(Object obj) {
    this.items[top] = obj;
    top++;
  }

  public Object pop() {
    this.top = this.top - 1;
    return this.items[top];
  }
}
Observations: It looks a lot like C++. All instance methods are virtual. "this" is like "self" in Smalltalk. It's optional when talking about instance variables, but some people consider it good form... By default, every class inherits from Object. Arrays are bounds-checked. Ie. push and pop will raise exceptions at runtime if we access the items array out-of-bounds.

Using the Standard Collections

This is the "Java" way to define a Stack class. Use one of the provided collection classes:
import java.util.*;

public class Stack extends Object {
  ArrayList items;            // defined in the java.util package

  public Stack() {
    items = new ArrayList();  // growable array object
  }

  public void push(Object obj) {
    items.add(obj);
  }

  public Object pop() {
    return(items.remove(items.size() - 1));
  }

  // We override the toString method so that we can print this
  // object out nicely.
  public String toString() { 
    return "Stack(" + items + ")";   // + is overloaded for Strings!!
  }

  // Every class can have a "main" method.  It's a good place to
  // put test code...
  public static void main(String[] args) {
    Stack test = new Stack();
    test.push("hello");
    test.push("goodbye");
    test.push(new Integer(17));
    System.out.println(test);
    test.pop();
    test.pop();
    System.out.println(test);
  }
}
The above makes use of the "standard" class ArrayList to implement our Stack class. A large (over 3000 classes today) hierarchy of standard classes ships with the Java development kit. Understanding when/how to make use of these classes is a large part of becoming a fluent/effective Java programmer. The JDK also ships with a tool that automatically generates nice-looking HTML to document its source code. This documentation is then presented in a browser in a manner eerily similar to the ClassBrowser in Smalltalk.

Inheritance

Below we exploit inheritance to build a simple Farm simulation...
import java.util.*;

public class Thing {}

public class Cheese extends Thing {}
 
public abstract class Animal extends Thing {
  protected int weight;

  public int getWeight() {
    return weight;
  }

  public void setWeight(int pounds) {
    weight = pounds;
  }
  
  public abstract String getSound();
  public abstract void eat(Thing a);
}

public class Cat extends Animal {
  protected ArrayList stomach;

  public Cat() {
    stomach = new ArrayList();
  }

  public String getSound() {
    return "Meow";
  }

  public void eat(Thing a) {
    if (! (a instanceof Cat)) {
      stomach.add(a);
    }
  }
}

public class CanibalCat extends Cat {
  public void eat(Thing a) {
    // I eat everything
    stomach.add(a);
  }
}

public class Mouse extends Animal {
  boolean hasCheese = false;

  public String getSound() {
    return "Squeak";
  }

  public void eat(Thing a) {
    // I only eat cheese
    if (a instanceof Cheese) {
      hasCheese = true;
    }
  }
}
Draw the class hierarchy for the above. A client program might look like this:
import java.util.*;

public class AnimalFarm {
  public static void main(String[] args) {
    ArrayList things = new ArrayList();
    things.add(new CanibalCat());
    things.add(new Cat());
    things.add(new Mouse());
    // so on and so forth...
  }
}
Notice that all instance methods are "virtual", that is the method that is invoked is based on the runtime class of the receiver object. Above we also see an example of a static method. Static methods are like class methods in Smalltalk. New objects are created using "new" as in C++, but there are important differences: the objects are garbage collected when no longer reached, and there is no crazy pointer syntax. Names (such as "a3") are references to objects. Assignment just changes pointers. There is no need to deal with nasty things like copy constructors or destructors in Java.

Overloading

Methods can also be overloaded in java. Overloaded methods must have different numbers/types of parameters:
class Point {
  int x, y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public Point add(Point other) {
    return new Point(x + other.x, y + other.y);
  }

  public Point add(Point p1, Point p2) {
    return add(p1.add(p2));
  }
}
Java, unlike C++, doesn't allow overloading of operators. In overloading, the compiler must decide statically (at compile time) which version of the method will be invoked. (Very different from the dynamic dispatch of inherited/overriden methods!)

Interfaces

In Smalltalk, a class defines a type. Java (to some extent) decouples the notion of class and type. While classes also define types in Java, the language also provides a notion of an interface, which is pure type. A class extends exactly one superclass, but it may implement any number of interfaces. Example:
class MouseEvent { /* a Mouse Event class... */ }

public interface MouseListener {
  public void mousePressed(MouseEvent e);
  public void mouseReleased(MouseEvent e);
  // ...
}

class Frame { /* A base UI component... */ }

class SomeUIComponent extends Frame implements MouseListener {
  // ...

  public void mousePressed(MouseEvent e) {
    // actually handles the mouse event here ...
  }

  public void mouseReleased(MouseEvent e) {
    // actually handles the mouse event here ...
  }

}
An interface is like an abstract class where every method is abstract. However, abstract classes typically provide some implementation. Interfaces are pure abstraction. A common (but somewhat extreme) approach says that there should be interfaces for all major classes in your system, for example:
class Color { /* a color class */ }

interface Shape {
  public double area();       // every shape has an area
  public Color color();    // every shape has a color
  // etc
}

abstract class ShapeImpl {
  // fills in some details shared by all concrete shapes, such as color
  Color color;
  public Color color() { return color; }
}

// concrete classes must implement area!!!

class Rectangle extends ShapeImpl {
  int width, height;
  double area() { return width * height; }
}

class Circle extends ShapeImpl {
  int radius;
  double area() { return radius * radius * Math.PI; }
}

Polymorphism (Again)

Concepts:
  1. Parametric polymorphism: like ML
  2. Subtype polymorphism: like Smalltalk/Java
  3. Adhoc polymorphism: sometimes this term is used to describe overloading.

Static Typechecking

In Java we can use a subclass where a superclass is expected. But how do we do typechecking when types don't have to be the same? In a dynamically-checked language (like Smalltalk), we can have "doesNotUnderstand" errors at runtime, but we gain a certain flexibility with this approach. At the same time, it can be harder to understand code because the interfaces are not well specified. In a statically checked language (C++/Java), we have to make sure that method lookup will find a target method. If typechecking is successful, we assure that there will be no "doesNotUnderstand" errors at runtime. From a programmer's point of view, such a system is less flexible, but the object interfaces are well specified.

Subtyping

The key notion is subtyping. One type A is a subtype of another type B if values of type A can be used wherever values of type B are expected.

Each class defines a type. Hence, a subclass is always a subtype of its superclass(es) sine instances of the subclass can be used wherever instances of the superclass are expected:

Animal a1 = new Cat();     // ok
Food   f1 = new Cheese();  // ok
Animal a2 = f1;            // Error at compile time.
Animal a3 = (Cat)f1;       // Error at compile time.  It's just not true!
Cat    c1 = (Cat)a1;       // ok, but checked at runtime
The rhs of an assignment can be a subtype of the lhs. The same is true for arguments/results to/from methods. Arguments can be subtypes of the method's parameters. The result may be a subtype of what's expected by the context.

Subtyping and Overriding/Overloading

When you override a method in a subclass, you can't change the argument types. If you do, you are just overloading the method. Suppose:
// Cat inherits eat(Thing t) from Animal and overloads that
// definition with eat(Mouse m).  Example:
class Cat extends Animal {
  public void eat(Mouse m) { ... }
}
...
Mouse m = new Mouse();
Cat c = new Cat();
c.eat(m);             // which eat gets called?

Subtyping and Result types

In Java, the result type of a function must be the same (not a subtype) of that of an inherited function.
class Point { /* A normal 2D point class */ }
class Point3D extends Point { /* A 3D point class */ }

interface PointMaker {
  public Point make();
}

class P1 implements PointMaker {
  public Point make() {
    return new Point();
  }
}

class P2 implements PointMaker {
  public Point3D make() {     // This is an error!
    return new Point3D();
  }
}
The rules: S is a subtype of T if:
  1. S provides all of the operations that T does (and maybe more)
  2. For each operation in T, the corresponding operation in S has the same # of args.
  3. The types of the results of S's operations are the same as the types of the corresponding operations of T.
  4. The types of the arguments of S's operations are the same as the types of the corresponding arguments of T's operations.

The bulk of the content for these slides was stolen from Craig Chambers.