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
- Baeldung - Classes & Objects - Focus on: fields, methods, constructors
- Baeldung - Constructors - Focus on: overloading and chaining
- W3Schools - Constructors - An alternative resource for classes, objects and constructor material
- Baeldung - Immutable Objects - Focus on: how objects can be immutable, and the benefits of that immutability
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
- Baeldung - OOP Concepts
- Focus on: abstraction and encapsulation
- Skip: advanced class types
- GeeksforGeeks - OOP Concepts
- Focus on: abstraction and encapsulation
- Baeldung - Access Modifiers
- Baeldung - Encapsulation & Information Hiding
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
FileLoggerinstance - Call
openFile() - Call
writeLine()with a message - Call
closeFile()
Think about the following:
- Try calling
writeLine()beforeopenFile(), 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
FileLoggerin the correct order - Update
main()to useApplicationLoggerinstead ofFileLoggerdirectly, 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
ApplicationLoggerhide from other classes? - How did the main method change when switching from
FileLoggertoApplicationLogger? - 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
- Baeldung - Inheritance
- Baeldung - Polymorphism
- GeeksForGeeks - Upcasting Vs Downcasting in Java
- Baeldung - Overloading vs Overriding
- Medium - Composition: Understanding Has-A vs. Is-A in Java
Video Materials
- Java Polymorphism Fully Explained In 7 Minutes
- Programming with Mosh
- Focus on the principles in the video rather than the javascript coding examples
- Java OOP in 10 minutes
- May be useful to help solidify concepts with java examples
Exercises
✍️Exercise 3.1 - From Downcasting to Polymorphism
Set up a classes for an animal show:
- Create a base class
Animalwith a name field and getter, include the name of the animal in your constructor - Create Dog extends
Animalwith a methodbark()that prints something using the name - Create Cat extends
Animalwith a methodmeow()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 callsbark() - If the animal is a
Cat, it callsmeow() - 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
Animalwith methodsquawk() - 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
Animalclass 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
PaymentMethodclass in your main method and call both methods of pay
Override the pay method:
- Create
DirectDebitextendingPaymentMethod - Override
pay(double amount) - Instantiate a
DirectDebitclass 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
- Baeldung - Packages
- Baeldung - Static
- Don’t worry about static code blocks or inner-classes at this point
- Medium - Hazards of global state
- Baeldung - Final
- Baeldung - Immutable objects
- For applications of the final keyword to guard against unwanted side-effects
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
Planetobjects created:private static int planetCount = 0; - Modify the Constructor: In the Planet constructor, increment
planetCountevery time a newPlanetobject is created - Static Method (Class Behaviour): Add a public static method to retrieve the count:
public static int getPlanetCount()that returnsplanetCount - 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
Planetobjects: earth, mars, jupiter - Print the planetCount using only the class name (
Planet.getPlanetCount()) - What happens when you print
earth.getPlanetCount(), why is this confusing?
- Create three different
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
Configurationin 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
systemNamefield (you’ll need to create a setter for this) and observe the compiler error
- Try to reassign
Final Methods and Classes
Create a parent class
- Create a class named
Polygonin 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
numberOfSidesparameter and initialises the field. - Add a getter method
getNumberOfSides()to return the value ofnumberOfSides. - Add a
public finalmethoddescribe()that prints: ‘I am a polygon with X sides’ where X is thenumberOfSidesfield - Try to create a setter for
numberOfSides. Can you compile it?
Create a subclass
- Create a class named
Trianglethat extendsPolygon. - In the constructor, call the superclass constructor with 3 sides.
- Try to override the
describe()method inTriangle. What error does the compiler give you?
Make the parent class final
- Change the
Polygonclass declaration to: - public final class
Polygon - Try to compile the
Triangleclass. 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_USERSnever be reassigned? - Why can the
systemNamefield not be reassigned, even via a setter? - Why can’t you add a setter for
numberOfSides? - How do final fields in
ConfigurationandPolygonserve different purposes? - Why might a designer choose to make a class final?