CSSE2002 — Week 4 Notes
Exceptions
See csse2002 for course logistics.
Today’s outline
- Exceptions
- Specification (recap)
Exceptions
See java-exceptions — the exception hierarchy, checked vs unchecked exceptions, try/catch/throw/throws/finally, and custom exceptions.
Specification
This part of the lecture repeats Week 3’s java-specification material near-verbatim (Javadoc, restrictiveness, generality, clarity, formality, contracts, defensive programming) — see that note rather than duplicating it here. Two small additions from this week’s repeat have been folded into it: an early-return variant of the search generality example, and an added Effective Java citation for defensive programming/doc comments.
Method Dispatch and Casting
Applied class exercises for week 4, covering java-polymorphism (dynamic method dispatch) and java-casting in more depth.
On the HTML site, fill in each blank with your answer (as a quoted string, e.g. "Car.topSpeed()"), then click Run Code to check it. In the PDF, the Working callout is shown as a static answer key instead (interactive checking needs a browser).
Question 1 — Apparent type vs actual type
Every object has an apparent type (compile-time type, from variable/method declarations) and an actual type (runtime type, fixed by whichever class’s new constructed it). The compiler always ensures the apparent type is the actual type or one of its superclasses, decides whether a call is legal using the apparent type, but dispatches the call to the method as implemented by the actual type.
class Animal { void eat() {...} }
class Lion extends Animal {}
class Dog extends Animal implements Pet {
void eat() {...}
void eat(int howMuch) {...}
}
class Poodle extends Dog {}
interface Pet {}What happens when you call simba.eat(200), given Lion simba = new Lion();?
Lion’s apparent AND actual type is Lion, which has no eat(int) method (only the no-arg eat() inherited from Animal) — the compiler rejects the call outright, since legality is checked against the apparent type’s available methods.
Full worked example, one line at a time:
| Statement | Outcome |
|---|---|
Dog evie = new Dog(); |
OK |
Animal animal = evie; |
OK (upcast, implicit) |
Lion mufasa = animal; |
Must be cast. Will fail (an Animal referencing a Dog is never a Lion). |
Dog harlee = animal; |
Must be cast. Will succeed (the actual object really is a Dog). |
Poodle bruiser = evie; |
Must be cast. Will fail (evie’s actual type is Dog, not Poodle). |
Lion simba = new Lion(); |
OK |
Lion scar = new Poodle(); |
Will not compile — Poodle and Lion share no inheritance relationship. |
Pet charley = new Poodle(); |
OK (Poodle extends Dog, which implements Pet). |
Dog lassie = charley; |
Must be cast. Will succeed. |
Animal scooby = charley; |
Will not compile — Pet is not related to Animal (an interface doesn’t know about unrelated classes). |
simba.eat(); |
OK |
simba.eat(200); |
No method found — compile error. |
Question 2 — A larger hierarchy
class Location {...}
interface RoadLegal { int topSpeed(); boolean licenceRequired(); }
interface PublicTransport { double calculateFare(Location start, Location end); }
class Vehicle {
int travelTime(Location start, Location end) {...}
int topSpeed() {...}
}
class HumanPowered extends Vehicle {
int getWeight() {...}
int topSpeed() {...}
}
class Motorised extends Vehicle implements RoadLegal {
int travelTime(Location start, Location end) {...}
int topSpeed() {...}
boolean licenceRequired() {...}
}
class Skateboard extends HumanPowered {}
class Bicycle extends HumanPowered implements RoadLegal {
boolean licenceRequired() {...}
}
class Motorbike extends Motorised {
int topSpeed() {...}
int travelTime(Location location) {...} // overloaded, single-Location version
}
class Car extends Motorised { int topSpeed() {...} }
class Taxi extends Car {}
class Bus extends Motorised implements PublicTransport {
int topSpeed() {...}
double calculateFare(Location start, Location end) {...}
}classDiagram
Vehicle <|-- HumanPowered
Vehicle <|-- Motorised
HumanPowered <|-- Skateboard
HumanPowered <|-- Bicycle
RoadLegal <|.. Bicycle
Motorised <|-- Motorbike
Motorised <|-- Car
Motorised <|.. RoadLegal
Car <|-- Taxi
Motorised <|-- Bus
Bus <|.. PublicTransport
For each, what method implementation is actually called?
Vehicle vehicle = new Taxi(); vehicle.topSpeed(); →
Vehicle vehicle = new Bicycle(); vehicle.topSpeed(); →
RoadLegal roadLegal = new Bicycle(); roadLegal.topSpeed(); →
Vehicle vehicle = new Motorbike(); vehicle.travelTime(location1, location2); →
Even though roadLegal’s apparent type is the RoadLegal interface, dispatch always goes to the actual type’s implementation — Bicycle doesn’t override topSpeed() itself, so it’s inherited from HumanPowered. Similarly Motorbike doesn’t override the two-Location travelTime, only the single-Location overload, so the two-argument call dispatches to Motorised’s version.
Some other statements to check yourself against — whether each compiles, and why:
Vehicle bus = new Bus();— compiles (upcast).Motorised skateboard = new Skateboard();— does not compile: no relationship betweenMotorisedandSkateboard.Motorbike vehicle = new Vehicle();— does not compile: downcast needs an explicit cast.Motorised motorbike = new Motorbike(); motorbike.travelTime(destination);— does not compile: neitherMotorisednor its parents declare a single-argumenttravelTime(Location).Car car = new Taxi(); Taxi taxi = car;— does not compile: downcast needs an explicit cast.Vehicle bike = new Bicycle(); bike.getWeight();— does not compile:Vehiclehas nogetWeight()method (it’s declared onHumanPowered).Motorised bus = new Bus(); PublicTransport trip = bus;— does not compile:Motorisedcan’t be implicitly cast to the unrelatedPublicTransportinterface.
Question 3 — Casting rules
When changing a value’s apparent type:
- Casting to a superclass of the current apparent type can be implicit.
- Casting to a subclass should be checked via
instanceof(a runtime check). - If the two types share no inheritance relationship, Java refuses to compile at all.
For each instanceof pattern-match below (assuming each variable holds an instance of its own class, e.g. bus is a Bus), does it compile, and if so, could the match be guaranteed true at compile time ("implicit")?
| Expression | Compiles? | Guaranteed true? |
|---|---|---|
bus instanceof Vehicle vehicle |
Yes | Yes — Vehicle is a superclass of Bus |
car instanceof Taxi taxi |
Yes | No — Taxi is a subclass of Car, needs a runtime check |
motorised instanceof Bicycle bike |
No | — Motorised and Bicycle share no relationship (siblings under Vehicle) |
motorbike instanceof RoadLegal legal |
Yes | Yes — Motorbike extends Motorised, which implements RoadLegal |
taxi instanceof Vehicle vehicle |
Yes | Yes — Vehicle is a superclass all the way up the Taxi → Car → Motorised → Vehicle chain |
Where would an electric scooter fit into this hierarchy? (Discussion question — not every real-world model fits neatly into single inheritance.)
Reference material
Java Casting
Introduced in 2026-03-12-object-oriented-programming-ii (Lecture, Week 3). See also java-primitive-and-reference-types.
Casting converts a variable from one type to another.
Primitive casting
- Widening — converting a smaller type to a larger one:
byte -> short -> char -> int -> long -> float -> double. - Narrowing — converting a larger type to a smaller one:
double -> float -> long -> int -> char -> short -> byte.
Reference casting
- Upcasting (implicit) — casting a subclass reference to a superclass reference.
- Downcasting (explicit) — casting a superclass reference back to a subclass reference.
Given Object -> Animal -> {Mammal -> {Dog, Cat}, Bird -> Chicken}, upcasting moves up this hierarchy, downcasting moves down it.
Upcasting
Always safe, done implicitly:
public class Animal {
public void makeSound() { System.out.println("Animal makes a sound"); }
}
public class Cat extends Animal {
@Override public void makeSound() { System.out.println("Cat meows"); }
}
Animal myAnimal = new Cat(); // upcasting: Cat treated as an Animal
myAnimal.makeSound(); // "Cat meows"Downcasting
Must check the object is actually an instance of the target subclass with instanceof first, since not every Animal is a Cat:
public class Cat extends Animal {
@Override public void makeSound() { System.out.println("Cat meows"); }
public void climb() { System.out.println("I can climb trees"); }
}
Animal myAnimal = new Cat(); // upcasting
if (myAnimal instanceof Cat) {
Cat myCat = (Cat) myAnimal; // downcasting
myCat.climb(); // access methods specific to Cat
}Every class implicitly extends Object
A class with no explicit extends clause (e.g. a plain public class Student { ... }) still has a parent: Object, the root of the Java class hierarchy.
Java Exceptions
Introduced in 2026-03-19-exceptions (Lecture, Week 4).
What is an exception?
An unexpected event that occurs during program execution, e.g.:
System.out.println(5 / 0); // ArithmeticException: / by zero (NOT Infinity, unlike floating-point division)Common causes: invalid user input, loss of network connection, physical limitations (e.g. out of disk space), code errors, opening an unavailable file.
When an exception occurs, Java creates an exception object containing information about the failure (Effective Java, Item 75: include failure-capture information in detail messages).
Exception hierarchy
Throwable
├── Error
└── Exception
├── RuntimeException (unchecked)
└── IOException (checked)
RuntimeException — unchecked
Caused by a programming error; the compiler does not force you to handle these:
- Improper use of an API →
IllegalArgumentException - Null pointer access →
NullPointerException - Out-of-bounds array access →
ArrayIndexOutOfBoundsException - Dividing by 0 →
ArithmeticException
IOException — checked
Checked by the compiler at compile-time — the programmer is prompted (via throws) to handle these:
- Opening a file that doesn’t exist →
FileNotFoundException - Reading past the end of a file →
EOFException - A connection attempt to a remote host fails →
ConnectException
try/catch
Place code that might throw inside try; every try is followed by a catch:
int result;
try {
result = 5 / 0;
} catch (ArithmeticException e) {
System.out.println(e); // print out the error message
result = Integer.MAX_VALUE; // fallback value
}- When an exception occurs, the rest of the
tryblock is skipped. - The
catchblock catches it and its statements execute. - If nothing in
trythrows,catchis skipped entirely.
throw and throws
throwexplicitly throws a single exception:
public static void checkEntry(int age) {
if (age < 18) {
throw new IllegalArgumentException("Access denied");
}
}throws(in a method declaration) declares the exception types a method might produce, so callers are prompted to handle them:
public static void findFile() throws FileNotFoundException {
File newFile = new File("file.txt");
FileInputStream stream = new FileInputStream(newFile);
}finally
Runs regardless of whether an exception was thrown; optional.
try {
// code
} catch (ExceptionType1 e1) {
// catch block
} finally {
// always executes
}Worked example — what does this print?
try {
System.out.println("Good Morning");
throw new FileNotFoundException();
System.out.println("the earth says"); // never reached
} catch (Exception e) {
System.out.println("hello");
} finally {
System.out.println("world");
}Prints Good Morning, hello, world — the throw immediately skips the rest of try (so "the earth says" never prints), catch runs since FileNotFoundException is an Exception, and finally always runs last.
Custom exceptions
Extend Exception (or a subclass) to define your own:
public class ItemNotFoundException extends Exception {
public ItemNotFoundException() {
super("Item not found");
}
public ItemNotFoundException(String message) {
super(message);
}
}
public class Store {
private List<String> items;
public Store() {
items = new ArrayList<>();
items.add("apple");
items.add("banana");
items.add("orange");
}
public String findItem(String itemName) throws ItemNotFoundException {
for (String item : items) {
if (item.equals(itemName)) {
return item;
}
}
throw new ItemNotFoundException("Item '" + itemName + "' not found in the list.");
}
}Java Polymorphism
Introduced in 2026-03-12-object-oriented-programming-ii (Lecture, Week 3).
What is polymorphism?
From the Greek “poly” (many) and “morphe” (form). In programming, it allows the same method call to produce different behaviour depending on the object that executes it.
Compile-time polymorphism (method overloading)
Multiple methods share a name but differ in parameter list (type, number, or both). Which one runs is decided at compile time, based on the method signature (see java-encapsulation#method-signatures):
public class CompPolymorphism {
static String print(int value) { return "number"; }
static String print(String value) { return "text"; }
public static void main(String[] args) {
System.out.println(print(21)); // "number"
System.out.println(print("21")); // "text"
}
}Runtime polymorphism (dynamic method dispatch)
Also called dynamic method dispatch. The method that gets executed is determined at runtime, based on the object’s actual type, not the variable’s declared type — implemented through method overriding (see java-inheritance):
The subclass’s overriding method executes, regardless of the compile-time type of the subclass instance. — Effective Java
public class Animal {
public void makeSound() { System.out.println("Animal makes a sound"); }
}
public class Cat extends Animal {
@Override public void makeSound() { System.out.println("Cat meows"); }
}
public class Dog extends Animal {
@Override public void makeSound() { System.out.println("Dog barks"); }
}
Animal myAnimal = new Dog();
myAnimal.makeSound(); // "Dog barks" — even though the variable's type is AnimalSubtype polymorphism
Objects of different subclasses can be treated as objects of a common superclass or interface, since subclasses are subtypes of their superclass. Achieved through inheritance (extends) or interface implementation (implements) — code works with the abstraction (supertype) rather than a specific implementation:
public static void performAnimalSound(Animal animal) {
animal.makeSound();
}
performAnimalSound(new Dog()); // "Dog barks"
performAnimalSound(new Cat()); // "Cat meows"The same works via an interface instead of a shared superclass:
public interface Animal { void makeSound(); }
public class Cat implements Animal { ... }
public class Dog implements Animal { ... }
// Can pass any class that implements the interface
performAnimalSound(new Dog());
performAnimalSound(new Cat());Java Specification
Introduced in 2026-03-12-object-oriented-programming-ii (Lecture, Week 3, Part 2).
Javadoc
- Ordinary comments:
//and/* ... */. - Javadoc comments: begin with
/**(note the second*), end with*/, must sit immediately above the thing being documented, and use tags beginning with@(some take parameters, some just text).
/**
* Calculates a sum by combining the hash code of the provided string
* and the long value of the provided float.
*
* @param inputString the input string whose hash code will be used
* @param inputFloat the float value to be converted to long and combined with the string hash code
* @return a long value representing the sum of the string's hash code and the long value of the float
*/
public long doCalculation(String inputString, float inputFloat) { ... }Common tags:
| Tag | Meaning |
|---|---|
@param varname ... |
Describe a parameter |
@return ... |
Describe the return value |
@throws ExceptionType ... |
Describe when a particular exception is thrown |
@requires Precondition |
Assumptions for the method to execute properly |
@ensures Postcondition |
Effects of executing the method |
@author authorname |
Author of the class |
What makes a good specification
Ideally, a specification should:
- Allow a method to be used by only reading its specification, not its implementation.
- Allow a method to be re-implemented without requiring changes to its callers.
- Be restrictive enough to rule out unacceptable implementations.
- Be general enough to not preclude acceptable (alternative) implementations.
- Be clear enough for programmers to understand.
Restrictiveness — keep out incorrect implementations
/**
* Returns an index (i) of ar such that ar[i] == x, if any.
*/
public int search(int[] ar, int x) { ... }What happens if x isn’t in ar? The spec doesn’t say — by its silence it allows any return value, so a caller can’t distinguish “found at index 0” from “not found”. Adding else, return -1 fixes that, but if x appears multiple times, nothing requires the lowest index or a consistent answer across calls — the spec is still non-deterministic unless it says smallest index.
Generality — allow acceptable alternative versions
/**
* Examine ar[0], ar[1], ... in turn and return the index of the
* first one that is equal to x, if any, else return -1.
*/
public int search(int[] ar, int x) { ... }This is a bad spec even though it’s restrictive: it describes how (forward iteration order), not what. A backward-iterating implementation that returns the same first-match index would violate this over-specific wording despite being equally acceptable — specs should describe outcomes, not implementation strategy. Prefer wording like “return the smallest index such that…”, which both rules out bad implementations and permits any implementation strategy that achieves the same outcome. Both a backward-iterating and an early-return-on-first-match forward implementation satisfy this better wording equally well:
public int search(int[] ar, int x) {
for (int i = 0; i < ar.length; i++) {
if (ar[i] == x) {
return i; // early return, still finds the smallest index
}
}
return -1;
}Clarity
A specification should facilitate communication — it can fail either because the reader doesn’t understand, or because the reader only thinks they understand. Clarity improves by being concise (long specs are more likely to contain contradictions, be skipped, or be misread) and by marking any deliberate redundancy explicitly (e.g. with “e.g.” or “i.e.”).
Formality
Specifications range from informal to formal:
- Informal — plain comments, e.g.
// Withdraws an amount and returns how much is left.Leaves open questions (what ifamountis negative? bigger than the balance? is the balance changed on failure?). - Semi-formal — structured English via Javadoc tags (
@param,@return,@throws). - Formal — mathematical/boolean constraints, e.g.
@require/@ensureusing Java boolean-expression syntax:
/**
* Withdraws an amount from this account.
*
* @require amount >= 0
* @require amount <= getBalance()
* @ensure getBalance() == \old(getBalance()) - amount
*/
public int withdraw(int amount) { ... }Contracts
A specification can be written as a contract:
- If the caller satisfies the precondition, the method guarantees to satisfy the postcondition.
- If the caller does not satisfy the precondition, the method guarantees nothing — any behaviour is allowed, and the method body doesn’t need to check for it.
This contrasts with a defensive, “no contract” style that instead documents and handles every failure case explicitly (e.g. returning a sentinel value like -1 on invalid input) — contracts push that responsibility onto the caller instead.
Defensive programming
Explicitly checking for invalid inputs and bad situations, ensuring the software does not behave dangerously regardless of input.
Even with a documented precondition, some caller eventually won’t check it. When dealing with external input or guarding critical resources, it’s often safer to validate defensively at the system boundary (throwing on bad input, e.g. IllegalArgumentException for a null/empty array) while relying on well-defined contracts between internal methods:
/**
* Finds the maximum value in the given array of integers.
*
* @throws IllegalArgumentException if the array is null or empty.
* @requires numbers != null && numbers.length > 0
* @ensures the method returns the maximum value found in the array.
*/
public int findMax(int[] numbers) {
if (numbers == null || numbers.length == 0) {
throw new IllegalArgumentException("Array must not be null or empty.");
}
int max = Integer.MIN_VALUE;
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] > max) {
max = numbers[i];
}
}
return max;
}Further reading: Preconditions, Postconditions, and Class Invariants, Assertions, and Effective Java (3rd ed.), Item 56: Write doc comments for all exposed API elements.