Mastering Java Application Development
Advanced OOP Design
Encapsulation and Access Control
You already know that encapsulation bundles data (fields) and methods within a class. But true encapsulation is about control. It’s about protecting a class's internal state from outside interference, which is crucial for building robust and maintainable systems. This is where access modifiers come in.
Access modifiers in Java determine the visibility of a class, constructor, field, or method. They are the gatekeepers of your code.
Let's look at the four types:
public: Accessible from anywhere.protected: Accessible within the same package and by subclasses in other packages.package-private(default, no keyword): Accessible only within its own package.private: Accessible only within the same class.
Good practice dictates making fields private and providing public methods (getters and setters) to access or modify them. This prevents direct, uncontrolled changes to an object's state.
public class BankAccount {
private double balance; // Accessible only within this class
public BankAccount(double initialBalance) {
if (initialBalance >= 0) {
this.balance = initialBalance;
}
}
// Public getter to safely access the balance
public double getBalance() {
return balance;
}
// Public method to control how the balance is changed
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
}
}
}
In the example above, the balance can't be set to a negative number from outside the class because the field is private and the deposit method contains validation logic. This is the essence of controlled access.
For simple data-carrier classes, Java 16 introduced Records. They are immutable classes that automatically generate private final fields, a public constructor, getters, equals(), hashCode(), and toString() methods, drastically reducing boilerplate code.
// A concise, immutable data carrier for a point on a 2D plane.
public record Point(int x, int y) {}
Inheritance and Polymorphism
Inheritance models an "is-a" relationship. A Dog is an Animal. This allows a subclass (or child class) to inherit fields and methods from a superclass (or parent class). It’s a powerful tool for code reuse, but its real strength lies in enabling polymorphism.
Polymorphism, which means "many forms," allows you to treat objects of different classes that share a common superclass as if they were objects of the superclass. A method can be written to accept a Shape object, and you can pass it a Circle, a Square, or a Triangle without changing the method's code.
This is often achieved through method overriding, where a subclass provides a specific implementation for a method already defined in its superclass. The decision on which method version to call is made at runtime, a concept known as dynamic binding
// Superclass
class Shape {
public void draw() {
System.out.println("Drawing a shape");
}
}
// Subclass
class Circle extends Shape {
@Override // Annotation to ensure this is an override
public void draw() {
System.out.println("Drawing a circle");
}
}
// Subclass
class Square extends Shape {
@Override
public void draw() {
System.out.println("Drawing a square");
}
}
public class DrawingBoard {
public static void main(String[] args) {
Shape myShape1 = new Circle(); // Polymorphism in action
Shape myShape2 = new Square();
myShape1.draw(); // Calls Circle's draw() method
myShape2.draw(); // Calls Square's draw() method
}
}
Here, myShape1 is a Shape reference, but it points to a Circle object. When draw() is called, Java's runtime system looks at the actual object and invokes the Circle's draw() method. The @Override annotation is optional but highly recommended; it tells the compiler you intend to override a method, and it will throw an error if you make a mistake, like misspelling the method name.
Abstract Classes vs. Interfaces
Both abstract classes and interfaces are used to define contracts that other classes must follow. They establish a common set of behaviours without specifying the full implementation. Choosing between them is a key architectural decision.
An abstract class can have a mix of abstract methods (without a body) and concrete methods (with an implementation). It can also have fields (instance variables). A class can only extend one abstract class.
// An abstract class can have state and implemented methods.
abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public void eat() { // Concrete method
System.out.println(name + " is eating.");
}
public abstract void makeSound(); // Abstract method
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
An is a pure contract. Historically, it could only contain abstract method signatures and constant fields (public static final). Since Java 8, interfaces can also include default and static methods with implementations, which adds flexibility. A class can implement multiple interfaces.
// An interface defines a set of abilities.
interface Flyable {
void fly(); // Abstract by default
default void land() { // Default method since Java 8
System.out.println("Landing on the ground.");
}
}
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Flying with wings.");
}
}
class Aeroplane implements Flyable {
@Override
public void fly() {
System.out.println("Flying with engines.");
}
}
| Feature | Abstract Class | Interface |
|---|---|---|
| Multiple Inheritance | Not supported (extends one class) | Supported (implements multiple interfaces) |
| Fields | Can have instance variables (state) | Only public static final constants |
| Constructor | Has a constructor (called via super()) | Does not have a constructor |
| Access Modifiers | Methods can be public, protected, private | Methods are public by default (or private) |
| When to Use | For an "is-a" relationship with shared code. | To define a capability or contract. |
Composition Over Inheritance
While inheritance is powerful, it can lead to rigid designs. If a base class changes, all its subclasses can be affected. A common design principle is to "favour composition over inheritance".
This means instead of inheriting behaviour (an "is-a" relationship), you create objects that contain other objects to provide functionality (a "has-a" relationship). This is known as composition.
A
Caris aVehicle(Inheritance). ACarhas anEngine(Composition).
Composition leads to more flexible and decoupled systems. You can swap out components at runtime. Instead of having PetrolCar and ElectricCar subclasses, you can have a single Car class that contains an Engine object. The engine itself could be an instance of PetrolEngine or ElectricEngine.
// Engine contract
interface Engine {
void start();
}
// Concrete implementations
class PetrolEngine implements Engine {
public void start() { System.out.println("Starting petrol engine."); }
}
class ElectricEngine implements Engine {
public void start() { System.out.println("Starting electric engine."); }
}
// Car class uses composition
class Car {
private final Engine engine; // "has-a" relationship
public Car(Engine engine) {
this.engine = engine; // Dependency is injected
}
public void startCar() {
engine.start(); // Delegation
}
}
// Usage
Car myPetrolCar = new Car(new PetrolEngine());
Car myElectricCar = new Car(new ElectricEngine());
myPetrolCar.startCar();
myElectricCar.startCar();
In this design, the Car class delegates the start behaviour to its Engine component. This makes the Car class independent of the specific engine type, leading to a much more adaptable and scalable architecture. These principles are fundamental to building complex software that is easy to test, maintain, and extend over time.
What is the primary purpose of making class fields private in Java?
When a method is called on an object reference, the decision on which specific method implementation to execute (e.g., the superclass's version or a subclass's overridden version) is made at runtime. What is this concept called?
These advanced concepts form the bedrock of good object-oriented design, helping you write code that is not just functional, but also clean, flexible, and ready for future challenges.
