Files
Computer-Fundamentals/java/02-object-oriented-programming-in-java.md
T
tarun-elango be31df2d44 more text
2026-04-26 14:09:04 -04:00

554 lines
16 KiB
Markdown

# File 2: Object-Oriented Programming in Java
Object-oriented programming is where Java starts to feel like Java. The syntax from the fundamentals file matters, but OOP is what gives Java its shape in real applications. Backend systems are rarely a loose collection of standalone functions. They are usually modeled as collaborating objects: controllers, services, repositories, domain entities, validators, clients, schedulers, and configuration components.
If you only memorize the four textbook pillars of OOP, you will miss the point. The real question is this: how do we organize code so behavior stays understandable as the system grows? This file answers that question from a Java engineer's perspective.
## Classes and Objects
### Intuition
A class is a blueprint for state and behavior. An object is a concrete instance created from that blueprint.
That definition is technically correct, but the useful mental model is stronger:
- a class defines a unit of responsibility
- an object is one live participant carrying out that responsibility at runtime
For example, in an order-processing service:
- `Order` might represent business data
- `OrderService` might represent business behavior
- `EmailNotifier` might represent an integration with an external system
### Example
```java
public class BankAccount {
private final String accountNumber;
private double balance;
public BankAccount(String accountNumber, double openingBalance) {
this.accountNumber = accountNumber;
this.balance = openingBalance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
}
public void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
}
}
public double getBalance() {
return balance;
}
}
```
### Why This Matters
Notice that the account is not just a bag of fields. It contains rules. That is one of the key ideas in OOP: data and behavior should often live together so the invariants are easier to protect.
In real systems, that helps prevent invalid state changes from being scattered across the codebase.
## Encapsulation
Encapsulation means hiding internal state and exposing only the operations that make sense.
### Why It Exists
If every part of the system can freely mutate every field, the object stops being trustworthy. You can no longer tell where a bug came from because the object's state could have been changed anywhere.
Encapsulation creates a boundary:
- internal representation is private
- changes happen through controlled methods
- validation and invariants live close to the data they protect
### Example
Bad design:
```java
public class UserProfile {
public String email;
public boolean verified;
}
```
Any caller can put this object into inconsistent states.
Better design:
```java
public class UserProfile {
private String email;
private boolean verified;
public UserProfile(String email) {
changeEmail(email);
this.verified = false;
}
public void changeEmail(String newEmail) {
if (newEmail == null || newEmail.isBlank()) {
throw new IllegalArgumentException("Email is required");
}
this.email = newEmail;
this.verified = false;
}
public void markVerified() {
this.verified = true;
}
public String getEmail() {
return email;
}
public boolean isVerified() {
return verified;
}
}
```
### Real-World Use Case
Encapsulation is critical in domains such as:
- payments, where balance or ledger state must obey strict rules
- authentication, where token and session transitions must remain valid
- inventory, where stock cannot drop below zero because random code mutated a field directly
### Common Misconception
Encapsulation is not the same as "always write getters and setters for every field." Blindly exposing setters often destroys the protection that encapsulation is supposed to provide. A class should expose behavior, not just field mutation.
## Inheritance
Inheritance lets one class reuse and extend behavior from another class.
```java
public class Employee {
protected final String name;
public Employee(String name) {
this.name = name;
}
public void work() {
System.out.println(name + " is working");
}
}
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " is planning and reviewing work");
}
}
```
### Intuition
Inheritance represents an "is-a" relationship.
- a `Manager` is an `Employee`
- a `Circle` is a `Shape`
The subclass gets access to behavior from the parent and may override methods.
### How It Works Internally
When you call an overridden method on a parent reference, Java uses dynamic dispatch at runtime to choose the actual implementation based on the real object type, not just the variable type.
```java
Employee employee = new Manager("Asha");
employee.work();
```
This calls `Manager.work()`, not `Employee.work()`.
### Where Inheritance Helps
Inheritance is useful when subclasses truly share a stable conceptual model and substantial reusable behavior. Typical examples:
- framework base classes
- exception hierarchies
- UI component hierarchies in some systems
- domain modeling where a clear subtype relationship exists
### Why Engineers Are Careful with It
Inheritance creates tight coupling. Once a subclass depends on parent implementation details, changes become harder. Deep inheritance trees are often brittle and confusing.
That is why experienced Java engineers often prefer composition unless the inheritance relationship is very natural.
## Polymorphism
Polymorphism means code can work against a general type while runtime behavior changes based on the actual object implementation.
### Example
```java
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}
public class CardPaymentProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult("card-authorized");
}
}
public class WalletPaymentProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult("wallet-authorized");
}
}
```
Now calling code can depend on `PaymentProcessor` without caring which concrete implementation is active.
```java
public class CheckoutService {
private final PaymentProcessor paymentProcessor;
public CheckoutService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public PaymentResult checkout(PaymentRequest request) {
return paymentProcessor.process(request);
}
}
```
### Production Relevance
This pattern shows up everywhere:
- different payment providers behind one contract
- multiple notification channels behind one interface
- storage implementations for local disk, S3, or blob storage
- authentication strategies for API key, OAuth, or internal service token
Polymorphism reduces branching and keeps behavior extensible.
## Abstraction
Abstraction means exposing the essential behavior while hiding unnecessary implementation detail.
### Intuition
When you drive a car, you use the steering wheel and pedals, not the combustion process details. In code, abstraction gives callers the same kind of high-level interface.
### Abstract Class Example
```java
public abstract class ReportGenerator {
public final String generate() {
fetchData();
transformData();
return renderOutput();
}
protected abstract void fetchData();
protected abstract void transformData();
protected abstract String renderOutput();
}
```
This is useful when you want a common workflow with customizable steps.
### Real-World Use Case
Frameworks use abstraction heavily. A framework might define the overall lifecycle and let your code implement only the project-specific pieces.
That idea appears in:
- servlet filters
- batch job processing
- event handlers
- test frameworks
- template methods inside enterprise applications
## Interfaces vs Abstract Classes
This is a classic Java question, but it is more valuable when framed as a design decision rather than a quiz answer.
### Interface
An interface defines a contract. It says what behavior a type must support.
```java
public interface Notifier {
void send(String destination, String message);
}
```
### Abstract Class
An abstract class is a partial implementation. It is useful when related types share both behavior and state.
```java
public abstract class BaseNotifier {
protected final AuditLogger auditLogger;
protected BaseNotifier(AuditLogger auditLogger) {
this.auditLogger = auditLogger;
}
protected void recordAudit(String destination) {
auditLogger.record("sent to " + destination);
}
}
```
### Practical Comparison
| Topic | Interface | Abstract Class |
| --- | --- | --- |
| Main purpose | Define capability or role | Share base behavior and state |
| Multiple inheritance | A class can implement many | A class can extend only one |
| Instance fields | No regular instance state | Yes |
| Constructor | No | Yes |
| Best use | Flexible contracts | Common base workflow or shared implementation |
### Design Intuition
Use an interface when you want callers to depend on behavior and stay decoupled from implementation details.
Use an abstract class when multiple related types genuinely share implementation and that shared base is stable enough to matter.
In many backend codebases, interfaces are common for boundaries and abstract classes are used more selectively.
## Composition vs Inheritance
This is one of the most important design instincts in Java.
### Composition
Composition means building objects by combining other objects.
```java
public class OrderService {
private final PaymentProcessor paymentProcessor;
private final InventoryService inventoryService;
private final NotificationService notificationService;
public OrderService(
PaymentProcessor paymentProcessor,
InventoryService inventoryService,
NotificationService notificationService) {
this.paymentProcessor = paymentProcessor;
this.inventoryService = inventoryService;
this.notificationService = notificationService;
}
}
```
The service is not inheriting behavior. It is coordinating collaborators.
### Why Composition Usually Wins
Composition is often more flexible because:
- dependencies are explicit
- behavior can be swapped at runtime or in tests
- classes stay focused on one role
- you avoid fragile parent-child coupling
### Relationship Diagram
```mermaid
classDiagram
class OrderService {
-PaymentProcessor paymentProcessor
-InventoryService inventoryService
-NotificationService notificationService
+placeOrder()
}
class PaymentProcessor {
<<interface>>
+process(request)
}
class CardPaymentProcessor {
+process(request)
}
class InventoryService {
+reserve(items)
}
class NotificationService {
+sendConfirmation(order)
}
OrderService --> PaymentProcessor : uses
OrderService --> InventoryService : uses
OrderService --> NotificationService : uses
CardPaymentProcessor ..|> PaymentProcessor
```
### Inheritance Diagram
```mermaid
classDiagram
class Employee {
+work()
}
class Manager {
+work()
+approveBudget()
}
class Engineer {
+work()
+buildFeature()
}
Employee <|-- Manager
Employee <|-- Engineer
```
### Rule of Thumb
If the relationship is naturally "has-a," use composition.
If the relationship is naturally "is-a" and the base behavior is stable and meaningful, inheritance may be fine.
### Common Pitfall
Do not use inheritance just to avoid writing a few lines twice. Duplication can often be refactored in other ways. Wrong inheritance is more expensive than small duplication.
## Object Construction and Lifecycle
In Java, objects are created with `new`, by factories, or by frameworks that manage dependency injection.
### Basic Construction
```java
UserProfile profile = new UserProfile("user@example.com");
```
### Why Construction Matters in Real Systems
As applications grow, object creation itself becomes design-relevant.
- should every caller build the object manually?
- should construction enforce invariants?
- should expensive resources be shared?
- should a framework control creation for testability and lifecycle management?
This is why design patterns such as builder, factory, and dependency injection appear so often in Java engineering.
## `this`, `super`, and Method Overriding
### `this`
`this` refers to the current object.
```java
public class Customer {
private final String name;
public Customer(String name) {
this.name = name;
}
}
```
### `super`
`super` refers to the parent class portion of the object.
```java
public class PremiumCustomer extends Customer {
public PremiumCustomer(String name) {
super(name);
}
}
```
### Overriding Rules Worth Remembering
- method name and parameters must match
- return type must be compatible
- access level cannot be more restrictive
- static methods are hidden, not overridden
- constructors are not overridden
These rules matter because subtle mistakes can lead to code that compiles but does not behave the way you expected.
## OOP Design Intuition
Strong OOP design is less about showing every language feature and more about assigning responsibility well.
### Good Design Questions
- which object should own this rule?
- which class is holding too much knowledge?
- is this behavior better represented as a method, a collaborator, or a separate strategy?
- are we modeling domain concepts clearly, or just dumping data into DTO-like objects?
### Signs of Weak OOP Design
- giant service classes that do everything
- data classes with no behavior and business logic scattered elsewhere
- excessive inheritance used for convenience rather than meaning
- public mutable fields everywhere
- interfaces added for every class even when no abstraction need exists
### Production Perspective
In real backend systems, the best OOP often looks boring in a good way:
- classes have narrow responsibilities
- collaborators are injected explicitly
- domain rules live near the domain model
- interfaces define important boundaries
- composition is preferred over deep inheritance
That style scales better than clever class hierarchies.
## How OOP Shows Up in a Backend Request
When an HTTP request arrives in a Java service, several objects usually collaborate:
```mermaid
flowchart LR
A[HTTP Request] --> B[Controller]
B --> C[Service]
C --> D[Repository]
C --> E[PaymentProcessor]
C --> F[NotificationService]
D --> G[(Database)]
E --> H[External Provider]
```
Each object has a distinct role. That separation is one of the main benefits of object-oriented design in Java. The controller handles transport concerns, the service handles business workflow, the repository handles persistence, and integrations are wrapped behind focused components.
## Key Takeaways
- A class should represent a coherent responsibility, and an object is a runtime participant carrying out that responsibility.
- Encapsulation protects invariants and is more valuable than blindly generating getters and setters.
- Inheritance enables reuse and polymorphism, but it also creates tight coupling and should be used carefully.
- Interfaces are excellent for contracts and system boundaries, while abstract classes are better for shared base behavior and state.
- Composition is usually more flexible and maintainable than inheritance in production Java systems.
- Good OOP design is about clear responsibility and collaboration, not about maximizing the number of classes or language features used.