CSSE2002 — Week 2 Notes

Object Oriented Programming I

See csse2002 for staff, assessment, and course logistics.

Today’s outline

  1. Objects, Classes, & Object-Oriented Programming
  2. Encapsulation
  3. Inheritance
  4. Abstraction
  5. Polymorphism (next week)

Why OOP?

A single bank account example, grown in stages, motivates the four pillars below.

Stage 1 — one global balance. A single static bankBalance shared by the whole “bank” — fine for one account, but there’s no way to represent more than one account or customer at once.

Stage 2 — parallel arrays of accounts. Adding more accounts naively means adding a parallel array for every piece of account data (accountNumbers, balances, …) plus a manually-maintained accountCount, and a findAccountIndex helper to keep them in sync by position.

Stage 3 — parallel arrays of account holders too. Adding a holder’s name/id alongside each account means yet more parallel arrays (holderIds, holderNames) and another manually-maintained linkedHolderIds array to associate accounts with holders — the bookkeeping to keep every array in sync by index grows with every new piece of data.

This is the motivation for OOP: instead of scattering related data across parallel arrays and free functions, bundle a variable and the functions that act on it together — e.g. instead of a free function f(x), a method that belongs to x. This bundling is exactly what a class does.

The four pillars of object orientation

  1. Encapsulation — see java-encapsulation
  2. Inheritance — see java-inheritance
  3. Abstraction — see java-abstraction
  4. Polymorphism — next week

Java Basics Practical - Palindromes

Practical exercises for week 2. The practical also introduces IntelliJ IDEA (the course’s supported IDE — see the install guide on Blackboard) and the Java 21 API docs (in particular the String class in java.lang, needed below) — neither has gradable content, so isn’t repeated here.

On the HTML site, fill in each blank with your answer (as a quoted string, e.g. "true"), 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 — Palindromes

A palindrome reads the same forwards and backwards, e.g. "AaaA", "madamimadam", "racecar".

Implement four methods, all with the signature public static boolean isPalindromeN(String word) (for \(N=1..4\)), each using a different technique:

  1. isPalindrome1 — a for loop.
  2. isPalindrome2 — a while loop.
  3. isPalindrome3 — recursive, with no helper methods.
  4. isPalindrome4 — recursive, using a private helper method.

All four should agree on at least these cases. What does each return?

isPalindromeN("AaA")

isPalindromeN("A")

isPalindromeN("")

isPalindromeN("Abbb")

Task 0 — isPalindrome1 (for loop):

public static boolean isPalindrome1(String word) {
    int len = word.length();
    for (int i = 0; i < len / 2; i++) {
        if (word.charAt(i) != word.charAt(len - i - 1)) {
            return false;
        }
    }
    return true;
}

Task 1 — isPalindrome2 (while loop):

public static boolean isPalindrome2(String word) {
    int len = word.length();
    int i = 0;
    while (i < len / 2) {
        if (word.charAt(i) != word.charAt(len - i - 1)) {
            return false;
        }
        i++;
    }
    return true;
}

Task 2 — isPalindrome3 (recursive, no helper methods):

public static boolean isPalindrome3(String word) {
    if (word.length() < 2) {
        return true; // base case
    }
    if (word.charAt(0) != word.charAt(word.length() - 1)) {
        return false; // base case
    }
    return isPalindrome3(word.substring(1, word.length() - 1)); // recursive step
}

Task 3 — isPalindrome4 (recursive, with a private helper):

public static boolean isPalindrome4(String word) {
    return helper(word, 0);
}

private static boolean helper(String word, int i) {
    if (i >= word.length() / 2) {
        return true; // base case
    }
    if (word.charAt(i) != word.charAt(word.length() - i - 1)) {
        return false; // base case
    }
    return helper(word, i + 1); // recursive step
}

All four agree on the required test cases: "AaA"true, "A"true, ""true, "Abbb"false.

Code review

The practical’s second half is a paired activity, not a gradable exercise: swap solutions with a partner, and for each other’s code —

  • Understand it, and ask questions about anything unclear.
  • Check correctness against the test cases above (and a few of your own).
  • Check it follows the course style guide.
  • Discuss/suggest improvements if you find issues.

Then discuss which of the four isPalindromeN techniques you each preferred, and why.

Java Basics Applied Class

Applied class exercises for week 2, reviewing week 1’s 2026-02-26-course-overview-and-java-basics content (switch statements, arrays, recursion). Meant to be done without a computer — you’re welcome to look up Java/library information, but your group knowing the answer isn’t a substitute for individual competence.

On the HTML site, fill in each blank with your answer (as a quoted string, e.g. "46368"), 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 — Numbers to Numbers

Implement a method, nameOf, with the signature public static String nameOf(int value). If 0 < value < 10, return the name of the number, otherwise return "??".

What does each call return?

nameOf(4)

nameOf(7)

nameOf(24)

Task 0 — using switch:

public static String nameOf(int value) {
    return switch (value) {
        case 1 -> "One";
        case 2 -> "Two";
        case 3 -> "Three";
        case 4 -> "Four";
        case 5 -> "Five";
        case 6 -> "Six";
        case 7 -> "Seven";
        case 8 -> "Eight";
        case 9 -> "Nine";
        default -> "??";
    };
}

The modern -> switch expression syntax is preferred over a traditional switch statement because it avoids one case accidentally falling through into the next if a break is omitted.

Task 1 — using an array of strings (with an early-exit variant):

public static String nameOf(int value) {
    if (value <= 0 || value >= 10) {
        return "??";
    }
    String[] numbers = {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"};
    return numbers[value - 1];
}

Bonus — extending to 10-99, using tiered helpers:

/** Requires: value > 0 && value < 10 **/
private static String ones(int value) {
    String[] numbers = {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"};
    return numbers[value - 1];
}

/** Requires: value >= 10 && value < 20 **/
private static String teens(int value) {
    String[] numbers = {"Ten", "Eleven", /* ... */};
    return numbers[value - 10];
}

/** Requires: value >= 20 && value < 100 **/
private static String tens(int value) {
    String[] numbers = {"Twenty", "Thirty", /* ... */};
    return numbers[value / 10 - 2];
}

public static String nameOf(int value) {
    if (value > 0 && value < 10) {
        return ones(value);
    }
    if (value >= 10 && value < 20) {
        return teens(value);
    }
    if (value >= 20 && value < 100) {
        if (value % 10 == 0) {
            return tens(value);
        } else {
            return tens(value) + " " + ones(value % 10);
        }
    }
    return "??";
}

Question 2 — Fibonacci Sequence

The Fibonacci sequence (\(0, 1, 1, 2, 3, \ldots\)) is defined as \(\mathcal{F}(0)=0\), \(\mathcal{F}(1)=1\), \(\mathcal{F}(n) = \mathcal{F}(n-1) + \mathcal{F}(n-2)\) for \(n \geq 2\): 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

Implement public static int fib(int n), returning the \(n\)th element. What is fib(24)?

public static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

Desk check (manual, pen-and-paper trace of the recursive calls) for fib(6):

graph RL
    F6["F(6)"] -->|5| F5["F(5)"]
    F6 -->|3| F4a["F(4)"]
    F5 -->|3| F4b["F(4)"]
    F5 -->|2| F3a["F(3)"]
    F4a -->|2| F3b["F(3)"]
    F4a -->|1| F2a["F(2)"]
    F4b -->|2| F3c["F(3)"]
    F4b -->|1| F2b["F(2)"]
    F3a -->|1| F2c["F(2)"]
    F3a -->|1| F1a["F(1)"]
    F3b -->|1| F2d["F(2)"]
    F3b -->|1| F1b["F(1)"]
    F3c -->|1| F2e["F(2)"]
    F3c -->|1| F1c["F(1)"]
    F2a -->|1| F1d["F(1)"]
    F2a -->|0| F0a["F(0)"]
    F2b -->|1| F1e["F(1)"]
    F2b -->|0| F0b["F(0)"]
    F2c -->|1| F1f["F(1)"]
    F2c -->|0| F0c["F(0)"]
    F2d -->|1| F1g["F(1)"]
    F2d -->|0| F0d["F(0)"]
    F2e -->|1| F1h["F(1)"]
    F2e -->|0| F0e["F(0)"]

Question 3 — The Collatz Conjecture

Start with any positive integer \(n\): if even, divide by 2; if odd, multiply by 3 and add 1; repeat. The (unproven, but checked below \(2075 \times 2^{60}\) as of 2025) conjecture is that this always reaches 1.

Implement public static int collatz(int n), returning the number of steps to reach 1. What is collatz(3)?

public static int collatz(int n) {
    int steps = 0;
    while (n != 1) {
        if (n % 2 == 0) {
            n = n / 2;
        } else {
            n = 3 * n + 1;
        }
        steps++;
    }
    return steps;
}

Trace for collatz(3): 3 → 10 → 5 → 16 → 8 → 4 → 2 → 1, which is 7 steps.

Question 4 — Ackermann function (extra)

The Ackermann–Péter function: \(\mathcal{A}(m,n) = n+1\) if \(m=0\); \(\mathcal{A}(m-1, 1)\) if \(m>0, n=0\); \(\mathcal{A}(m-1, \mathcal{A}(m, n-1))\) if \(m>0, n>0\).

Implement public long ackermann(short m, short n), and explain why it takes short parameters but returns a long.

public long ackermann(short m, short n) {
    if (m == 0) {
        return n + 1;
    }
    if (m > 0 && n == 0) {
        return ackermann((short) (m - 1), (short) 1);
    }
    if (m > 0 && n > 0) {
        return ackermann((short) (m - 1),
                          (short) ackermann(m, (short) (n - 1)));
    }
    return -1; // Impossible case but required for coverage.
}

Note the explicit (short) casts on every recursive call — Java’s arithmetic (m - 1, n - 1) implicitly promotes short operands to int, so without the casts this wouldn’t compile against a short-typed parameter.

Why short in, long out? The Ackermann function grows extremely rapidly even for small inputs, so the parameters are restricted to short (deliberately small inputs) while the result is a long, so the (much larger) output can be represented without overflow.

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 Animal abstract class shared by Dog and Cat. 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, Swimmable

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:

  1. Must be established after the class constructor.
  2. May be assumed as a precondition of each method (excluding the constructor).
  3. Must be established after each method call.
class Counter {
    private int count;
    public Counter() { count = 0; }
    public void increment() { count = count + 1; }
}
// Invariant: count >= 0

Invariants 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:

  1. Public fields — a directly-mutable field lets any caller bypass the class entirely and violate the invariant. Fix: make the field private.
  2. 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 Inheritance

Introduced in 2026-03-05-object-oriented-programming-i (Lecture, Week 2) — “things you have because your parents have them”.

Subclasses and superclasses

Inheritance allows creating a new class from an existing class:

  • The new class is the subclass (child/derived class).
  • The existing class it’s derived from is the superclass (parent/base class).

extends is the keyword used to implement inheritance in Java:

public class Employee {
    private String name;
    private String address;

    public Employee(String name, String address) {
        this.name = name;
        this.address = address;
    }
    public String getName() { return name; }
    public String getAddress() { return address; }
    public void setAddress(String address) { this.address = address; }
    public String printDetails() { return name + " " + address; }
}

public class ContractEmployee extends Employee {
    private double hourlyRate;
    private int hoursWorked;

    public ContractEmployee(String name, String address, double hourlyRate) {
        super(name, address);
        this.hourlyRate = hourlyRate;
        this.hoursWorked = 0;
    }
    public void logHours(int hours) { this.hoursWorked += hours; }
    public double calculateWeeklyPay() { return hourlyRate * hoursWorked; }
}

The super keyword

super is used in a subclass to access superclass members (attributes, constructors, and methods) — e.g. super(name, address) above calls Employee’s constructor to initialise the inherited fields before the subclass’s own constructor body runs.

Method overriding

The subclass inherits the attributes and methods of its superclass. If the same method signature is defined in both, the subclass’s version overrides the superclass’s version:

public class ContractEmployee extends Employee {
    @Override
    public String printDetails() {
        return "Role: Contract Employee, Hourly Rate: $" + hourlyRate + ", Hours Worked: " + hoursWorked;
    }
}

public class FullTimeEmployee extends Employee {
    @Override
    public String printDetails() {
        return "Role: Full-Time Employee, Monthly Salary: $" + monthlySalary;
    }
}

Both the superclass and subclass method MUST share the same method signature (see java-encapsulation) for this to be overriding rather than a separate overload.

super.methodName(...) can be used inside an override to still call the superclass’s version of the method, e.g. to extend rather than replace its behaviour:

public class FullTimeEmployee extends Employee {
    @Override
    public String printDetails() {
        return super.printDetails() + ", Role: Full-Time Employee, Monthly Salary: $" + monthlySalary;
    }
}