prep

Classes and Objects

Learning Objectives

Classes and objects are the fundamental units of structure in Java. Every non-trivial Java program is a collaboration between objects.

Many early Java bugs come from misunderstandings about:

  • How many objects actually exist
  • Whether two variables refer to the same object
  • When modifying one thing unexpectedly affects another
  • One way of guarding against this is by using immutable objects, a concept we’ll introduce later in the sprint

This section helps you build a correct mental model of object creation and identity, which becomes especially important when the objects are passed between services.

Self Study

As you read through the resources below try to answer the following questions:

  • What is the difference between a class and an object in Java?
  • Explain how they relate to each other, and why many objects can be created from the same class.
  • What actually happens when you create an object using new and a constructor?
  • Include what the constructor is responsible for, and why a class might have more than one constructor.
  • When one object variable is assigned to another or passed into a method, what is being shared?
  • Explain how references work, and why changing an object through one variable can affect another.

Reading Materials

Video Materials

Exercises

✍️Exercise 1.1 - Product Class

Create a Product class with:

  • Fields: name, price, stockCount

  • Getters and setters for each field

  • A toString method that includes each of the fields

  • Two constructors:

  • Name and price only, defaulting stockCount to 0

  • Name, price and stock

Create several products and print them, then modify your first constructor so that it takes advantage of constructor chaining.

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • How did constructor overloading change the way Product objects could be created?
  • What problem does constructor chaining with this() solve?
  • When you created multiple Product objects, how did the constructor arguments affect the state of each object?

✍️Exercise 1.2 - Reference Behaviour

Starting with the following in your main method:

Product p1 = new Product("Laptop", 900);
Product p2 = p1;
p2.setPrice(1100);
System.out.println(p1.getPrice());
System.out.println(p2.getPrice());

Tasks:

  • Predict the output
  • Run the code to verify
  • Change how p2 is instantiated so that making changes to it does not affect p1

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • Why did changing p2 also change p1?
  • What does this tell you about what p1 and p2 actually store?
  • How did your fix prevent changes to p2 from affecting p1?
  • What new object(s) existed after your change compared to before?
  • How could this kind of reference behaviour cause bugs in a larger codebase if it’s not well understood?

The Four OOP Pillars - Encapsulation & Abstraction

Learning Objectives

The four pillars are descriptions of patterns that appear in well-designed systems. Understanding these principles gives you:

  • A shared vocabulary for talking about design
  • A way to reason about why code is structured the way it is
  • A foundation for understanding interfaces, collections, and services later in the course

We’re going to initially focus on two of these:

  • Encapsulation protects domain models and services
  • Abstraction underpins dependency injection and service boundaries

If these concepts feel abstract now, that’s normal, their value becomes clearer as you apply them repeatedly in code.

Self Study

As you read through the resources below try to answer the following questions:

  • In a sentence or two, describe abstraction and encapsulation
  • How might abstraction help if your current project became part of a larger system?
  • What problems arise when object state can be modified freely from outside the class?
  • How would private fields and the use of setters improve safety over public fields?
  • What problems do encapsulation and abstraction help prevent in real programs?

Reading Materials

Exercises

✍️Exercise 2.1 - Fix the direct balance access using encapsulation

Starting with:

class BankAccount {
  public double balance;
}
class BankService {
  void withdraw(BankAccount account, double amount) {
    account.balance = account.balance - amount;
  }
  void deposit(BankAccount account, double amount) {
    account.balance = account.balance + amount;
  }
}
public class Main {
  public static void main(String[] args) {
    BankAccount account = new BankAccount();
    account.balance = 100;
    BankService service = new BankService();
    service.withdraw(account, 150);
    System.out.println("Balance: " + account.balance);
  }
}

Tasks:

  • Make balance private in BankAccount

  • Add methods to BankAccount to allow for deposits, withdrawals and balance checks without direct field access

  • Add validation to these methods:

  • deposit amount must be > 0

  • withdraw amount must be > 0

  • balance must never go below 0

  • Update BankService so it no longer accesses account.balance directly

  • Update Main so it no longer reads/writes balance directly

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • How did making balance private change how the system had to interact with BankAccount?
  • Why is it better for BankAccount to manage its own deposits and withdrawals rather than BankService?
  • How does this refactor make misuse of BankAccount harder or impossible?

✍️Exercise 2.2 - Abstraction in Code

Start with this class, for a logging system:

class FileLogger {
  private boolean fileOpen = false;
  public void openFile() {
    if (fileOpen) {
      throw new IllegalStateException("File already open");
    }
    System.out.println("Opening log file...");
    fileOpen = true;
  }
  public void writeLine(String line) {
    if (!fileOpen) {
      throw new IllegalStateException("Cannot write - file not open");
    }
    System.out.println("LOG: " + line);
  }
  public void closeFile() {
    if (!fileOpen) {
      throw new IllegalStateException("File already closed");
    }
    System.out.println("Closing log file...");
    fileOpen = false;
  }
}

In your main method, use FileLogger directly to log a message:

  • Create a FileLogger instance
  • Call openFile()
  • Call writeLine() with a message
  • Call closeFile()

Think about the following:

  • Try calling writeLine() before openFile(), what happens and why?
  • If this were a real logger implementation, what might happen if another developer forgets to call closeFile()?

Create a new class called ApplicationLogger, which will provide an abstraction over FileLogger:

  • Create a single method in this logger which calls each method on FileLogger in the correct order
  • Update main() to use ApplicationLogger instead of FileLogger directly, checking that logging still works

We now have a new requirement for the ApplicationLogger - the ability to log out [INFO] and [ERROR] level logs:

  • Refactor the existing method you’ve created to prepend the logged message with [INFO] and call it logInfo
  • Create a new method called logError which prepends [ERROR]
  • Use these new methods in your main() method

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • What complexity does ApplicationLogger hide from other classes?
  • How did the main method change when switching from FileLogger to ApplicationLogger?
  • Why is the new version simpler to use correctly?
  • What mistakes can other developers no longer make when using ApplicationLogger?
  • Why is this important in large systems?
  • If the system later logged to a database instead of a file, which class would you change?
  • Why is it useful that main() does not know how logging works internally?
  • How does this demonstrate abstraction reducing system complexity?

The Four OOP Pillars - Inheritance & Polymorphism

Learning Objectives

Inheritance on its own is rarely the goal, polymorphism is.

Polymorphism allows you to:

  • Treat different objects in a common way, e.g. based on their common inheritance.
  • Write code that works with future extensions, for instance logging behaviour in classes that extend a base application logger class
  • Write classes that do not need to know about those extensions, operating only on the base class

Once polymorphism is understood, many Java features that previously seemed complex start to feel natural and predictable.

Self Study

As you read through the resources below try to answer the following questions:

  • How can inheritance help you avoid repeating the same behaviour in multiple classes?
  • What additional benefits does inheritance provide when it allows polymorphism? How does this affect the way objects are used in code?
  • What problem does method overriding solve that method overloading does not?
  • Why is frequent downcasting often a sign of a design problem

Reading Materials

Video Materials

Exercises

✍️Exercise 3.1 - From Downcasting to Polymorphism

Set up a classes for an animal show:

  • Create a base class Animal with a name field and getter, include the name of the animal in your constructor
  • Create Dog extends Animal with a method bark() that prints something using the name
  • Create Cat extends Animal with a method meow() that prints something using the name
  • Create an AnimalShow class with a method: public void perform(Animal animal)

Implement perform using instanceof and casting so that:

  • If the animal is a Dog, it calls bark()
  • If the animal is a Cat, it calls meow()
  • Otherwise it prints a default message

Confirm this runs as expected for each of your animal types, then:

  • Add a new animal type, e.g. Parrot extends Animal with method squawk()
  • Call show.perform(new Parrot("Polly"))
  • Observe the output - is the squawk() method called?

Refactor this design to use a single method on Animal that is overridden by all subclasses.

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • Why did the original instanceof and casting approach fail when you added a new animal type?
  • What does this reveal about how scalable this design is?
  • How did moving behaviour into the base Animal class change the design?
  • Why did the perform method stop needing to know the concrete animal types?
  • How does this refactor demonstrate the real value of polymorphism?

✍️Exercise 3.2 - Overloading vs Overriding

Starting with:

class PaymentMethod {
    void pay(double amount) {
        System.out.println("Paying " + amount ");
    }
}

Overload the pay method:

  • Add an overloaded pay(double amount, String currency)
  • Implement the method to reference the currency when paying
  • Instantiate a PaymentMethod class in your main method and call both methods of pay

Override the pay method:

  • Create DirectDebit extending PaymentMethod
  • Override pay(double amount)
  • Instantiate a DirectDebit class and call both methods of pay

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • How does Java choose between overloaded methods?
  • How does Java choose an overridden method at runtime?
  • Why can overloading sometimes be confusing or misleading when reading code?
  • How does this exercise reinforce the difference between “same method name” and “same behaviour”?

✍️Exercise 3.3 - Composition vs Inheritance

Building on the last exercise, we now need to support a checkout process. A checkout must be able to take a payment using a payment method.

Tasks:

  • Create a Checkout class.
  • Decide how Checkout should relate to PaymentMethod. Should Checkout extend PaymentMethod, or should it contain a PaymentMethod?
  • Implement the chosen relationship, if your Checkout class contains a PaymentMethod remember to instantiate it inside your constructor
  • Add a method processPayment(double amount) in Checkout that uses the payment method to make a payment.
  • In your main method:
  • Create a PaymentMethod
  • Create a Checkout
  • Call your processPayment method
  • Stretch task - implement a different extension of PaymentMethod, and add a method to Checkout that lets you update the PaymentMethod before making a payment

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • Why did you choose HAS-A (or IS-A) for the Checkout and PaymentMethod relationship?
  • What reasoning helped you decide which relationship makes sense?
  • How does your choice affect the design and functionality of the Checkout class?
  • What would happen if you chose the wrong relationship?
  • How do IS-A and HAS-A relationships help you think about code reuse and composition in real applications?
  • Can you imagine other classes where this distinction is important?

Packages, import, static & final

Learning Objectives

As projects grow, structure becomes just as important as logic. Without clear organisation, even correct code becomes difficult to understand, change, and extend.

Packages help you:

  • Group related concepts
  • Separate responsibilities
  • Communicate architectural intent to other developers

The static and final keywords let you express design decisions directly in code, rather than relying on comments or conventions.

Self Study

As you read through the resources below try to answer the following questions:

  • How do packages communicate structure and intent, beyond just avoiding name clashes?
  • What does it mean for a field or method to belong to a class rather than an instance?
  • Why can misuse of static be harmful to object-oriented design?
  • How does the meaning of final differ when applied to fields, methods, and classes?

Reading Materials

Exercises

✍️Exercise 4.1 - Static Instance Counter

Tasks:

  • Create a class: Create a new class named Planet
  • Instance Field: Give the class a non-static (instance) field: private String name;
  • Add a constructor that takes the name.
  • Static Field (Shared State): Add a private static field to track the number of Planet objects created: private static int planetCount = 0;
  • Modify the Constructor: In the Planet constructor, increment planetCount every time a new Planet object is created
  • Static Method (Class Behaviour): Add a public static method to retrieve the count: public static int getPlanetCount() that returns planetCount
  • What happens if you try to reference the instance’s name field in this method?
  • Test: In your main method (or a separate test file):
    • Create three different Planet objects: earth, mars, jupiter
    • Print the planetCount using only the class name (Planet.getPlanetCount())
    • What happens when you print earth.getPlanetCount(), why is this confusing?

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • Why does planetCount have to be static, while name should not be?
  • Why is it important that getPlanetCount() is static?
  • What are some of the dangers of using global static variables?
  • What would happen if planetCount were an instance field instead?

✍️Exercise 4.2 - Final Safety

This exercise explores the three main uses of final: fields, methods, and classes.

Final Fields

  • Create a class named Configuration in a package of your choice.
  • Class Constant: Add a public static final field for a universally constant value: public static final int MAX_USERS = 100;
  • Final Instance Field: Add a private final field: private final String systemName;
  • Initialise it in the constructor.
  • Test in main:
    • Try to reassign Configuration.MAX_USERS = 200; and observe the compiler error.
    • Create a Configuration instance. Try to reassign its systemName field (you’ll need to create a setter for this) and observe the compiler error

Final Methods and Classes

Create a parent class

  • Create a class named Polygon in your package.
  • Add a private final int field for numberOfSides.
  • Create a constructor that does not include this field, what happens?
  • Create a constructor that takes a numberOfSides parameter and initialises the field.
  • Add a getter method getNumberOfSides() to return the value of numberOfSides.
  • Add a public final method describe() that prints: ‘I am a polygon with X sides’ where X is the numberOfSides field
  • Try to create a setter for numberOfSides. Can you compile it?

Create a subclass

  • Create a class named Triangle that extends Polygon.
  • In the constructor, call the superclass constructor with 3 sides.
  • Try to override the describe() method in Triangle. What error does the compiler give you?

Make the parent class final

  • Change the Polygon class declaration to:
  • public final class Polygon
  • Try to compile the Triangle class. What error do you get?

Think about the following questions, make notes and be prepared to talk through your thoughts in the workshop.

  • Why can MAX_USERS never be reassigned?
  • Why can the systemName field not be reassigned, even via a setter?
  • Why can’t you add a setter for numberOfSides?
  • How do final fields in Configuration and Polygon serve different purposes?
  • Why might a designer choose to make a class final?