Effective Java Application Development
Object-Oriented Design
Building with Blueprints
In programming, simply writing lines of code that run one after another works for small tasks. But to build complex, reliable applications—the kind that power businesses—we need a better way to organise our work. This is where object-oriented programming (OOP) comes in. It’s a method for structuring code around the concept of 'objects' and 'classes'.
Think of a class as a blueprint. An architect designs a blueprint for a house, detailing its properties (like the number of rooms and the colour of the walls) and its functions (like opening doors and turning on lights). The blueprint itself isn't a house, but it defines what a house is. In Java, a class defines the properties (fields or variables) and behaviours (methods) that its objects will have.
An object is the actual house built from that blueprint. You can build many houses from the same blueprint, and each one is a distinct entity. You could have a blue house with three bedrooms and a red house with five bedrooms, but they both follow the same fundamental design. Similarly, in code, you can create many object 'instances' from a single class, each with its own state.
This approach lets us model real-world things directly in our code. We can create a
Userobject, aProductobject, or aShoppingCartobject, making our software easier to understand, maintain, and expand.
Protecting Your Data
One of the most important principles in OOP is encapsulation a fancy word for bundling an object's data (its state) together with the methods that operate on that data. More importantly, it involves hiding the internal state of an object from the outside world. This isn't about secrecy; it's about control. You expose a clean, public interface while keeping the complex inner workings hidden and protected.
This is managed using access modifiers. These keywords determine which other parts of your code can access a class's members (its fields and methods).
| Modifier | Accessibility |
|---|---|
public | Accessible from any other class. |
protected | Accessible within its own package and by subclasses. |
private | Accessible only within the class it's declared in. |
Good practice dictates that you should make your fields private and provide public methods (often called getters and setters) to access or modify them. This prevents other parts of your code from putting an object into an invalid state.
public class BankAccount {
// This balance is hidden from the outside world.
private double balance;
public BankAccount(double initialBalance) {
if (initialBalance >= 0) {
this.balance = initialBalance;
}
}
// Public method to get the balance.
public double getBalance() {
return balance;
}
// Public method to deposit money.
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
}
In the example above, no outside code can directly set the balance to a negative number. The only way to change the balance is through the deposit method, which contains logic to ensure the amount is positive. This is the power of encapsulation.
Creating Flexible Objects
When an object is created, we need a way to set its initial state. This is done with a special method called a constructor. It has the same name as the class and doesn't have a return type. A constructor is called automatically whenever a new instance of the class is created with the new keyword.
Sometimes, you need more than one way to create an object. For instance, you might want to create a User object with just a username, or perhaps with a username and an email address. This is where constructor overloading comes in. You can define multiple constructors in the same class, as long as they have different parameter lists (either a different number of parameters or different types).
public class User {
private String username;
private String email;
private boolean isActive;
// Constructor for essential details.
public User(String username, String email) {
this.username = username;
this.email = email;
this.isActive = true; // A default value
}
// Another constructor for just a username.
public User(String username) {
// Calls the first constructor using `this`
this(username, "default@example.com");
}
// A third constructor with all details.
public User(String username, String email, boolean isActive) {
this.username = username;
this.email = email;
this.isActive = isActive;
}
}
This gives you the flexibility to create User objects in different ways depending on what information is available at the time. Notice the use of this() to call another constructor from within a constructor. This is a common pattern to avoid duplicating initialisation logic.
How Objects Relate
Individual objects are useful, but their real power emerges when they work together. There are two primary ways to model relationships between classes: composition and inheritance.
A simple rule of thumb: Favour composition over inheritance. It often leads to more flexible and maintainable designs.
Inheritance models an "is-a" relationship. A Manager is a type of Employee. An SUV is a type of Car. The more specific class (the subclass) inherits all the public and protected fields and methods from the more general class (the superclass). This promotes code reuse, as you don't have to rewrite common functionality.
Composition models a "has-a" relationship. A Car has an Engine. A Team has Players. Instead of inheriting properties, one class contains an instance of another class as one of its fields. This allows you to build complex objects out of simpler ones. A Car object might contain an Engine object, four Wheel objects, and a Radio object.
While inheritance can be powerful, it creates a tight coupling between the superclass and subclass. A change in the superclass can easily break the subclass. Composition is generally more flexible because the classes are more independent. You can swap out the Engine in a Car for a different type of Engine without affecting the Car class itself, as long as the new engine conforms to the expected interface.
Shared vs. Individual
Finally, let's look at the difference between instance members and static members. Everything we've discussed so far—fields like balance or username—are instance members. Each object instance gets its own separate copy of these fields. If you have two BankAccount objects, each has its own independent balance.
Static members, marked with the static keyword, are different. There is only one copy of a static field or method, and it belongs to the class itself, not to any individual object instance. All objects of that class share this single copy.
Static fields are useful for data that should be common to all instances, like a counter for how many objects have been created, or a constant value like Math.PI.
public class Counter {
// A static field shared by all instances.
private static int instanceCount = 0;
// An instance field unique to each object.
private int id;
public Counter() {
instanceCount++; // Increment the shared counter.
this.id = instanceCount; // Assign a unique ID.
}
public static int getInstanceCount() {
return instanceCount;
}
public int getId() {
return id;
}
}
// Usage:
Counter c1 = new Counter(); // count is 1, id is 1
Counter c2 = new Counter(); // count is 2, id is 2
System.out.println("Total instances: " + Counter.getInstanceCount()); // Prints 2
Notice how we call the static method getInstanceCount() directly on the Counter class, not on an instance like c1. Static methods can only access other static members; they have no access to instance fields like id because they aren't associated with a specific object.
In object-oriented programming, if a class is compared to a blueprint for a house, what does an object represent?
What is the primary goal of encapsulation in OOP?
These principles—encapsulation, flexible constructors, composition, and the distinction between static and instance members—are the bedrock of building scalable, maintainable Java applications. They allow you to create modular components that are easy to reason about and reuse, which is essential for working on large projects and with frameworks like Spring Boot.
