CSSE2002 — Week 3 Notes
Object Oriented Programming II
See csse2002 for course logistics. Continues 2026-03-05-object-oriented-programming-i.
Today’s outline
- Object Oriented Programming II
- Specification
Recap: the four (five) pillars
Last week covered Objects/Classes/OOP, Encapsulation, Inheritance, and Abstraction. This week adds Polymorphism, plus revisits/extends a few Week 2 topics in more depth.
Interface vs abstract class
Week 2 left the “when to use which” comparison as homework — now covered properly in java-abstraction.
Revising encapsulation
A quick recap of java-encapsulation’s Person example — bundling fields with the methods that access them, restricting direct access via private + getters.
Mutable vs immutable classes
See java-mutability — object state that can (mutable) or cannot (immutable) change after construction, and the arrays/lists gotcha where final only protects the reference, not the contents.
Polymorphism
See java-polymorphism — compile-time (overloading), runtime (overriding/dynamic dispatch), and subtype polymorphism.
Casting
See java-casting — primitive widening/narrowing casts, and reference upcasting/downcasting.
Specification
See java-specification — Javadoc, restrictiveness, generality, clarity, formality, contracts, and defensive programming.
Java Classes - Point Line Polynomial
Practical exercises for week 3, building up a small geometry class hierarchy (Point → Line → Polynomial) to practice java-encapsulation and java-mutability (note that movePoint/flipPoint/scale-style methods return a new object rather than mutating this).
On the HTML site, fill in each blank with your answer (as a quoted string, e.g. "5.0"), 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 — Points
Write a Point class representing an x,y-coordinate on a Cartesian plane, with:
Point(float x, float y)and a defaultPoint()(both 0).getX()/getY().Point movePoint(float deltaX, float deltaY)— returns a newPointat(getX() + deltaX, getY() + deltaY), without modifyingthis.static double distance(Point p, Point q)— Euclidean distance between two points.
If p1 = new Point() and p2 = p1.movePoint(4, 5), what does p2 print (assuming a toString of "(" + x + ", " + y + ")")?
public class Point {
private float x;
private float y;
public Point() {
this(0, 0);
}
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() { return this.x; }
public float getY() { return this.y; }
public Point movePoint(float deltaX, float deltaY) {
return new Point(this.x + deltaX, this.y + deltaY);
}
public static double distance(Point p, Point q) {
double x = p.x - q.x;
double y = p.y - q.y;
return Math.sqrt(x * x + y * y);
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}movePoint deliberately returns a new Point rather than mutating this — p1 is left unchanged at (0.0, 0.0).
Question 2 — Lines
Write a Line class storing a start Point, an end Point, and the (precomputed) double length between them, with:
Line(Point start, Point end)and a defaultLine()(both ends at the origin, length 0).getStart()/getEnd()/getLength().void moveStart(float deltaX, float deltaY)/void moveEnd(float deltaX, float deltaY)— mutate the line’s start/end in place and recompute the length.
For a Line from (0, 0) to (3, 4), what is getLength()?
public class Line {
private Point start;
private Point end;
private double length;
public Line() {
this.start = new Point();
this.end = new Point();
this.length = 0;
}
public Line(Point start, Point end) {
this.start = start;
this.end = end;
this.length = Point.distance(start, end);
}
public Point getStart() { return start; }
public Point getEnd() { return end; }
public double getLength() { return length; }
public void moveStart(float deltaX, float deltaY) {
start = start.movePoint(deltaX, deltaY);
length = Point.distance(start, end);
}
public void moveEnd(float deltaX, float deltaY) {
end = end.movePoint(deltaX, deltaY);
length = Point.distance(start, end);
}
@Override
public String toString() {
return start.toString() + " " + end.toString() + " " + this.getLength();
}
}(0,0) to (3,4) is a 3-4-5 right triangle, so the length is exactly 5.0 — unlike Point, moveStart/moveEnd mutate the Line in place (a deliberately different, mutable design from Point’s immutable movePoint).
Question 3 — Testing and toString
Unless overridden, printing an object prints its class name and memory location, e.g. Point@30f39991. Overriding toString() (as used above) replaces this with something readable — always worth adding when testing a class by eye.
Writing a quick main to sanity-check as you go (rather than writing a large class fully before ever running it) catches mistakes early:
Point p1 = new Point();
Point p2 = p1.movePoint(4, 5);
System.out.println(p1); // (0.0, 0.0)
System.out.println(p2); // (4.0, 5.0)Question 4 — Additional methods
Add to Point:
Line createLine(Point end)— aLinefromthistoend.Point flipPoint()— negates both coordinates, e.g.(-1, 2)becomes(1, -2).
Add to Line:
Point middle()— the midpoint of the start and end.Line flipLine()— aLinewith both endpoints flipped.
What does new Point(-1, 2).flipPoint() print?
public Line createLine(Point end) {
return new Line(this, end);
}
public Point flipPoint() {
return new Point(x * -1, y * -1);
}
public Point middle() {
return new Point((start.getX() + end.getX()) / 2, (start.getY() + end.getY()) / 2);
}
public Line flipLine() {
return new Line(start.flipPoint(), end.flipPoint());
}Question 5 — Polynomials (bonus)
A polynomial function of the restricted form \(f(x) = a + bx + cx^2\) can be represented by its coefficients. Write a Polynomial class with constructors Polynomial(), Polynomial(float a), Polynomial(float a, float b), Polynomial(float a, float b, float c), plus:
float valueAt(float x)— evaluates the polynomial atx.Polynomial add(Polynomial other)— adds two polynomials coefficient-wise, returning a newPolynomial.
For \(f(x) = 1 + 2x\), what is \(f(3)\)?
public class Polynomial {
private final float[] coefficients;
public Polynomial() { coefficients = new float[]{0}; }
public Polynomial(float a) { coefficients = new float[]{a}; }
public Polynomial(float a, float b) { coefficients = new float[]{a, b}; }
public Polynomial(float a, float b, float c) { coefficients = new float[]{a, b, c}; }
public Polynomial(float[] coefficients) { this.coefficients = coefficients; }
private static float index(float[] coefficients, int index) {
// Coefficients beyond the polynomial's degree are implicitly 0.
if (index >= coefficients.length) {
return 0;
}
return coefficients[index];
}
public Polynomial add(Polynomial other) {
int max = Math.max(coefficients.length, other.coefficients.length);
float[] newCoefficients = new float[max];
for (int i = 0; i < max; i++) {
newCoefficients[i] = index(coefficients, i) + index(other.coefficients, i);
}
return new Polynomial(newCoefficients);
}
public float valueAt(float x) {
float v = 0;
float xpower = 1;
for (int i = 0; i < coefficients.length; i++) {
v += coefficients[i] * xpower; // xpower == x^i
xpower *= x;
}
return v;
}
}\(f(3) = 1 + 2 \times 3 = 7\).
Challenge — representing the unrestricted polynomial \(f(x) = a_0 + a_1x + \dots + a_nx^n\) for any \(n \geq 0\) (rather than fixing the degree at 2) just needs the internal representation generalised to a float[] coefficients array of arbitrary length, as already used by add’s newCoefficients array and the extra Polynomial(float[]) constructor above — valueAt and add already work unchanged for any length.
Stack Heap and Class Design
Applied class exercises for week 3, covering 2026-03-12-object-oriented-programming-ii and reinforcing java-mutability (why reference types behave differently from primitives when passed to methods).
On the HTML site, fill in each blank with your answer (as a quoted string, e.g. "1"), 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 — Tracing the call stack and heap
Understanding the runtime call stack and object heap clarifies unexpected behaviour of reference types. Consider:
class Counter {
private int number;
public Counter() { this.reset(); }
public void increment(int amount) { this.number += amount; }
public void increment() { this.number++; }
public void reset() { this.number = 0; }
public int getValue() { return this.number; }
}
class DebugMe {
static void f(Counter counter, int param) {
counter.increment();
param++;
}
public static void main(String[] args) {
Counter first = new Counter();
int second = 0;
f(first, second);
System.out.println(first.getValue());
System.out.println(second);
}
}What does main print (first line, then second line)?
First line →
Second line →
State of the call stack/heap just before f finishes executing:
main |
|
|---|---|
args |
→ {} |
first |
→ Counter{number: 1} |
second |
0 |
f |
|
|---|---|
counter |
→ (same Counter object as first) |
param |
1 |
first and counter are two references to the same Counter object on the heap, so counter.increment() inside f is visible through first after f returns — first.getValue() prints 1. But param is a int, passed by value: param++ only changes f’s local copy, not main’s second — so second still prints 0. This is why mutating an object through a reference parameter is visible to the caller, but reassigning/incrementing a primitive parameter is not.
Question 2 — Designing a Vertex class
The Triangle class below represents a triangle as a 2D array of coordinates (vertexes[0] = x-coordinates, vertexes[1] = y-coordinates) — leading to a fiddly implementation:
public class Triangle {
private double[][] vertexes;
public Triangle(double a, double b, double c) {
double dividend = (square(a) - square(b) - square(c));
double cx = dividend / (-2 * c);
double cy = Math.sqrt(square(b) - square(cx));
this.vertexes = new double[][]{
new double[]{0, c, cx},
new double[]{0, 0, cy}
};
}
public double perimeter() {
double x0 = vertexes[0][0], x1 = vertexes[0][1], x2 = vertexes[0][2];
double y0 = vertexes[1][0], y1 = vertexes[1][1], y2 = vertexes[1][2];
return distance(x0, y0, x1, y1) + distance(x1, y1, x2, y2) + distance(x2, y2, x0, y0);
}
public Triangle scale(double multiplier) {
double[][] scaledUp = new double[2][3];
for (int x = 0; x < 2; x++) {
for (int y = 0; y < 3; y++) {
scaledUp[x][y] = vertexes[x][y] * multiplier;
}
}
return new Triangle(scaledUp);
}
}Design a Vertex class (signatures only, no implementation needed) that could simplify this — a triangle is really a collection of three vertices, each with an x and y coordinate.
class Vertex {
Vertex(); // Construct a new vertex at (0, 0)
Vertex(double, double);
double distance(Vertex); // distance to another vertex
Vertex scale(double); // multiply the coordinates by the given amount
}Rewriting Triangle to use three Vertex fields instead of a raw 2D array:
public class Triangle {
private Vertex a;
private Vertex b;
private Vertex c;
public Triangle(double a, double b, double c) {
double dividend = (square(a) - square(b) - square(c));
double cx = dividend / (-2 * c);
double cy = Math.sqrt(square(b) - square(cx));
this.a = new Vertex();
this.b = new Vertex(c, 0);
this.c = new Vertex(cx, cy);
}
public Triangle(Vertex a, Vertex b, Vertex c) {
this.a = a;
this.b = b;
this.c = c;
}
public double perimeter() {
return a.distance(b) + b.distance(c) + c.distance(a);
}
public Triangle scale(double multiplier) {
return new Triangle(a.scale(multiplier), b.scale(multiplier), c.scale(multiplier));
}
}Introducing Vertex moves the x/y bookkeeping into a single-responsibility class, so Triangle’s own methods read as “distance between vertices” and “scale each vertex” rather than juggling raw array indices.
Question 3 — Recursive call stack (bonus)
class Reverser {
static void reverse(int[] arr) {
int n = arr.length;
if (n == 1) {
return;
}
int[] rest = new int[n - 1];
for (int i = 1; i < n; i++) {
rest[i - 1] = arr[i];
}
arr[n - 1] = arr[0];
reverse(rest);
}
public static void main(String[] args) {
int[] numbers = {42, 24, 11};
reverse(numbers);
}
}Sketch the state of the call stack and heap just prior to the third call to reverse.
main |
|
|---|---|
args |
→ {} |
numbers |
→ {42, 24, 42} |
reverse (1st call) |
|
|---|---|
arr |
→ {42, 24, 42} |
n |
3 |
rest |
→ {24, 24} |
reverse (2nd call) |
|
|---|---|
arr |
→ {24, 24} |
n |
2 |
rest |
→ {11} |
Each recursive call copies everything except the first element into a new, smaller array (rest), and overwrites the last slot of the current array with its own first element — so by the time the base case (n == 1) is reached, the original array has been rebuilt back-to-front in place.
Reference material
Java Abstraction
Introduced in 2026-03-05-object-oriented-programming-i (Lecture, Week 2).
What is abstraction?
The ability to hide complex implementation details and expose only the essential behaviours. In Java, abstraction is often implemented using interfaces and abstract classes — both define methods that subclasses must implement, specifying a contract for what operations can be performed on an object without specifying how those operations are implemented.
Interfaces
An interface is a group of related methods with empty bodies (a contract that implementing classes must fulfil). To use an interface’s methods, a class must implement it — a class can implement multiple interfaces.
public interface PaymentProcessor {
void processPayment(double amount);
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment: " + amount);
}
}
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new PayPalProcessor();
processor.processPayment(100);
}
}The program only interacts with the PaymentProcessor interface — the implementation details are hidden inside the class that implements it.
Abstract classes
An abstract class cannot be instantiated directly. It can mix abstract methods (no body — declared with the abstract keyword) with fully-implemented methods, and subclasses must implement all its abstract methods.
public abstract class Payment {
public void validateTransaction() {
System.out.println("Validating transaction...");
}
abstract void processPayment(double amount);
}
public class PayPalPayment extends Payment {
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
public class Main {
public static void main(String[] args) {
Payment payment = new PayPalPayment();
payment.validateTransaction();
payment.processPayment(150.0);
}
}The abstract class defines the common behaviour of all payments (validateTransaction), while leaving the payment-specific logic (processPayment) to subclasses.
Interfaces vs abstract classes
Covered in 2026-03-12-object-oriented-programming-ii (Lecture, Week 3) — Week 2’s lecture left this comparison as a homework/self-study item.
- Use an interface to define a capability or contract, e.g.
Flyable,Runnable,Payable. A class can implement multiple interfaces. - Use an abstract class to define a shared base implementation (common behaviour), e.g. an
Animalabstract class shared byDogandCat. A class can only extend one (super)class.
abstract class Animal {
public void eat() {
System.out.println("Animal is eating");
}
public abstract void makeSound();
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat Meows");
}
}A class can combine both: extend at most one abstract/concrete class, while implementing as many interfaces as needed:
class Dog extends Animal implements Runnable, Swimmable
class Duck extends Animal implements Flyable, SwimmableJava 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 Encapsulation
Introduced in 2026-03-05-object-oriented-programming-i (Lecture, Week 2).
Classes and objects
- A class describes the contents of the objects that belong to it: an aggregate of data fields (properties) plus the operations (methods) defined on them.
- An object is an element (instance) of a class; objects have the behaviours of their class. The object is the actual component of a running program, while the class specifies how instances are created and how they behave.
Encapsulation
Encapsulation is the process of bundling code (methods/member functions) and data (member variables) together into a single unit — a class. It restricts direct access to some of an object’s components, preventing the accidental modification of data.
public class Person {
private String name;
private int age;
private String email;
private String[] phoneNumber;
public Person(String name, int age, String email, String[] phoneNumber) {
this.name = name;
this.age = age;
this.email = email;
this.phoneNumber = phoneNumber;
}
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
public String[] getPhoneNumber() { return phoneNumber; }
}Constructors
A constructor is a special method invoked when an object of the class is created (via new):
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public static void main(String[] args) {
Person person = new Person("John", 20);
System.out.println("The name of the person is " + person.name);
}
}If a class has no constructor, the Java compiler automatically creates a default constructor at runtime.
Notes on constructors:
- Constructors are invoked implicitly when objects are instantiated.
- The constructor’s name MUST be the same as the class.
- A constructor must not have a return type.
- A constructor can be overloaded but cannot be overridden.
Access modifiers
Access modifiers set the accessibility (visibility) of classes, interfaces, properties, methods, constructors, and data members:
| Modifier | Accessible from |
|---|---|
private |
Only within the declaring class |
public |
Anywhere |
protected |
Same package, plus subclasses (see java-inheritance) |
| (default, no keyword) | Only within the same package |
Attempting to access a private field from outside its class is a compile error (X has private access in Y).
Getters and setters
Getter and setter methods provide controlled access to an object’s private fields, keeping its internal representation hidden from the outside world:
- Getters (accessors) retrieve the value of a private field from outside the class.
- Setters (mutators) set/update the value of a private field from outside the class.
public class BankAccount {
private double balance;
private String accountNumber;
public BankAccount(String accountNumber) {
this.accountNumber = accountNumber;
}
public double getBalance() { return balance; }
public void setBalance(double balance) { this.balance = balance; }
public String getAccountNumber() { return accountNumber; }
}The static keyword
static is a non-access modifier for methods and attributes:
- Static methods/attributes can be accessed without creating an object of the class.
- Useful in memory management; can be applied to variables, methods, blocks, and nested classes.
Static variables (class variables) are shared among all instances of a class — useful for constants and shared properties, e.g. public static int totalAccounts = 0;.
Static methods can be called without creating an instance of the class, but cannot access non-static (instance) variables or methods directly:
class Bank {
static double interestRate = 5.0;
static double calculateInterest(double balance, int years) {
return (balance * interestRate * years) / 100;
}
}Class invariants
A class invariant specifies a condition (or set of conditions) that should always be true throughout the life of an object — used to ensure a system remains in a valid state. A class invariant:
- Must be established after the class constructor.
- May be assumed as a precondition of each method (excluding the constructor).
- Must be established after each method call.
class Counter {
private int count;
public Counter() { count = 0; }
public void increment() { count = count + 1; }
}
// Invariant: count >= 0Invariants complement preconditions (what must be true before a method executes) and postconditions (what must be true after) — together these define a contract for how a method should behave.
Protecting invariants
A specification can claim an invariant while the implementation still allows it to be broken. Two common leaks:
- Public fields — a directly-mutable field lets any caller bypass the class entirely and violate the invariant. Fix: make the field
private. - Returning an internal reference — a getter that
returns its internal mutable collection/object directly hands the caller a way to mutate it from outside, bypassing any checks the class’s own methods perform. Fix: return a defensive copy:
private List<String> files;
public List<String> getFiles() {
return new ArrayList<>(files); // copy, not the internal reference
}Preserving the invariant may also require adding preconditions to methods that could otherwise violate it (e.g. rejecting an addition that wouldn’t satisfy the invariant), or, when a method is likely to be called by code outside your control, defensively throwing an exception (e.g. IllegalArgumentException/IllegalStateException) rather than relying purely on the precondition contract (see java-specification — Defensive programming).
Method signatures
A method signature is a method’s unique identifier: its name plus its parameter types. The return type and parameter names do not count towards the signature, and a signature MUST be unique within a class:
public void print() { ... } // signature: print()
public void print(int parameter) { ... } // signature: print(int)Java Mutability
Introduced in 2026-03-12-object-oriented-programming-ii (Lecture, Week 3).
Mutable vs immutable classes
Objects in Java can either change their state after creation or remain fixed forever:
- Mutable class — object state can change after creation.
- Immutable class — object state cannot change after creation.
Mutable
public class Person {
private String name;
private int age;
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
}
Person p = new Person();
p.setName("Alice");
p.setAge(20);
p.setAge(30); // state changedImmutable
Made final (fields set once in the constructor, no setters):
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}A subclass can reintroduce mutability into an otherwise-immutable design (e.g. adding a setter, or exposing mutable internal state) — worth reading up on (Effective Java, Item 16) before relying on “final” alone as a guarantee.
Arrays and lists break naive immutability
final on an array/list field only prevents reassigning the reference — it does nothing to protect the array’s contents:
public final class Person {
private final String name;
private final String[] phoneNumbers;
public Person(String name) {
this.name = name;
this.phoneNumbers = new String[]{"111", "222"};
}
public String[] getPhoneNumbers() { return phoneNumbers; }
}
Person person = new Person("John");
person.getPhoneNumbers()[0] = "999999"; // mutates the "immutable" object!this.phoneNumbers = new String[]{"123"}; // NOT allowed — final prevents reassignmentEven though the class looks immutable (all fields final, no setters), the caller can still reach into the array returned by the getter and mutate it in place.
Defensive copying
To actually protect mutable internal state, return (and store) copies rather than the original reference (Effective Java, Item 50):
public final class Person {
private final String name;
private final String[] phoneNumbers;
public Person(String name, String[] phoneNumbers) {
this.name = name;
this.phoneNumbers = phoneNumbers.clone(); // defensive copy
}
public String[] getPhoneNumbers() {
return phoneNumbers.clone(); // defensive copy
}
}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.