Beyond the Basics: Understanding Java's OOP Philosophy
Thilan Dissanayaka Computing Concepts June 13, 2020

Beyond the Basics: Understanding Java's OOP Philosophy

I used to build software using the MERN stack and PHP for some time. Back then, I was a Java hater. I loved the freedom of JavaScript and PHP. It felt easy, flexible, and fast. No strict types, no rigid structures, just build and run.

Later, as I prepared to enter the industry, I realized that most large-scale, enterprise-level applications rely heavily on Java for their backend. During my WSO2 internship interviews, it became clear that a deep understanding of Java was not optional, it was essential.

But here’s what I didn’t understand back then: Java wasn’t trying to make my life harder. It was trying to solve a completely different problem. It wasn’t about quick hacks or rapid scripting; it was about predictable, maintainable, and scalable systems. It was about building software that could last years, survive dozens of developers touching it, and evolve without collapsing under complexity.

That’s when I started seeing the true power of Java’s Object-Oriented Programming. Not just the classes, objects, and inheritance you learn in tutorials, but the philosophy behind its design. And it changed the way I approach software development entirely.

But even now my favorite language is C. Yes The classical programming language. Since this is a post about Java I’m not going to describe the beauty of C here :)

Lets jump into Java again. First thing we are going to understand is the concept of Objects.

Classes and Objects. what are these and why they really matter?

Before we dive deep, let’s talk about what a class actually is.

A class is a blueprint. We can build objects using a class. Everyone says that. But what does that mean in practice?

Think about building houses. You don’t design every house from scratch. You create architectural plans—blueprints that specify how to build a certain type of house. The blueprint isn’t a house itself. It’s a template that describes what a house should look like and how it should be built.

In Java, a class is exactly that:

public class House {
    private int bedrooms;
    private int bathrooms;
    private String address;
    private boolean hasGarage;
    
    public House(int bedrooms, int bathrooms, String address) {
        this.bedrooms = bedrooms;
        this.bathrooms = bathrooms;
        this.address = address;
        this.hasGarage = false;
    }
    
    public void addGarage() {
        this.hasGarage = true;
    }
    
    public String getDescription() {
        return bedrooms + \" bed, \" + bathrooms + \" bath house at \" + address;
    }
}

This class is the blueprint. When you want an actual house, you create an object from this blueprint:

House myHouse = new House(3, 2, \"123 Main St\");
House yourHouse = new House(4, 3, \"456 Oak Ave\");

Now you have two houses. They’re built from the same blueprint (class), but they’re different houses with different data. My house has 3 bedrooms at 123 Main St. Your house has 4 bedrooms at 456 Oak Ave.

This is the fundamental concept: classes define the structure and behavior, objects are the actual instances that hold real data.

Why OOP Matters: The Real-World Problem

Let me tell you about the project that made me finally understand why OOP exists.

I was building an inventory management system in PHP. Started simple—just a few functions:

function addProduct($name, $price, $quantity) {
    // Add to database
}

function updateStock($productId, $newQuantity) {
    // Update database
}

function calculateTotalValue() {
    // Sum up all products
}

Easy, right? This worked fine for a month. Then the requirements changed.

"We need different types of products. Electronics have warranty periods. Perishables have expiration dates. Clothing has sizes and colors."

Okay, I added more parameters:

function addProduct($name, $price, $quantity, $type, $warranty = null, $expirationDate = null, $size = null, $color = null) {
    // Now this function is a mess
    if ($type === 'electronics' && $warranty === null) {
        throw new Exception(\"Electronics need warranty\");
    }
    // More validation...
}

Then came discounts. Some products have percentage discounts, others have fixed amount discounts. Some discounts only apply to bulk purchases. Some discounts expire.

My function-based code became unmaintainable. I had functions that took 10+ parameters. I had conditional logic everywhere checking what type of product I was dealing with. Adding a new product type meant changing 15 different functions.

This is where OOP saves you.

With OOP, each product type becomes its own class:

public abstract class Product {
    protected String name;
    protected BigDecimal basePrice;
    protected int quantity;
    
    public abstract BigDecimal calculatePrice();
    public abstract boolean needsRestock();
    public abstract String getStorageRequirements();
}

public class Electronics extends Product {
    private int warrantyMonths;
    private String manufacturer;
    
    @Override
    public BigDecimal calculatePrice() {
        // Electronics pricing logic
        BigDecimal price = basePrice;
        if (warrantyMonths > 12) {
            price = price.multiply(new BigDecimal(\"1.15\"));
        }
        return price;
    }
    
    @Override
    public String getStorageRequirements() {
        return \"Climate controlled, anti-static\";
    }
}

public class PerishableProduct extends Product {
    private LocalDate expirationDate;
    
    @Override
    public BigDecimal calculatePrice() {
        // Discount items near expiration
        long daysUntilExpiration = ChronoUnit.DAYS.between(LocalDate.now(), expirationDate);
        
        if (daysUntilExpiration < 3) {
            return basePrice.multiply(new BigDecimal(\"0.50\")); // 50% off
        } else if (daysUntilExpiration < 7) {
            return basePrice.multiply(new BigDecimal(\"0.75\")); // 25% off
        }
        
        return basePrice;
    }
    
    @Override
    public boolean needsRestock() {
        return quantity < 10 && ChronoUnit.DAYS.between(LocalDate.now(), expirationDate) > 14;
    }
    
    @Override
    public String getStorageRequirements() {
        return \"Refrigerated, 2-4°C\";
    }
}

public class Clothing extends Product {
    private String size;
    private String color;
    private String material;
    
    @Override
    public BigDecimal calculatePrice() {
        return basePrice;
    }
    
    @Override
    public String getStorageRequirements() {
        return \"Dry, room temperature\";
    }
}

Now when I need to calculate the total inventory value:

public class InventoryManager {
    private List<Product> products;
    
    public BigDecimal calculateTotalValue() {
        BigDecimal total = BigDecimal.ZERO;
        for (Product product : products) {
            BigDecimal itemValue = product.calculatePrice()
                .multiply(new BigDecimal(product.getQuantity()));
            total = total.add(itemValue);
        }
        return total;
    }
    
    public List<Product> getItemsNeedingRestock() {
        return products.stream()
            .filter(Product::needsRestock)
            .collect(Collectors.toList());
    }
}

Look at that calculateTotalValue method. It doesn’t care what type of product it’s dealing with. Electronics, perishables, clothing—it doesn’t matter. It just calls calculatePrice() on each product and sums them up.

When the boss says "We’re adding a new product type: Furniture," I don’t touch InventoryManager at all. I just create a new Furniture class that extends Product. That’s it.

This is polymorphism in action, and it’s the reason large systems don’t collapse under their own complexity.

The Restaurant Example: OOP in Action

Here’s another real-world scenario that demonstrates why OOP matters.

Imagine you’re building software for a restaurant chain. In a procedural approach, you might have:

$order = [
    'items' => [
        ['name' => 'Burger', 'price' => 12.99],
        ['name' => 'Fries', 'price' => 3.99]
    ],
    'customer' => 'John',
    'status' => 'pending'
];

function calculateTotal($order) {
    $total = 0;
    foreach ($order['items'] as $item) {
        $total += $item['price'];
    }
    return $total;
}

function applyDiscount($order, $discountPercent) {
    // Modify the order array
}

This works until reality hits. Different locations have different tax rates. Some customers have loyalty points. Some items have special preparation requirements. Delivery orders need address validation. Dine-in orders need table assignments.

With OOP:

public class Order {
    private List<MenuItem> items;
    private Customer customer;
    private OrderStatus status;
    private OrderType type; // DELIVERY, DINE_IN, TAKEOUT
    
    public void addItem(MenuItem item) {
        items.add(item);
        notifyKitchen(item);
    }
    
    public BigDecimal calculateTotal() {
        BigDecimal subtotal = items.stream()
            .map(MenuItem::getPrice)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
            
        BigDecimal tax = calculateTax(subtotal);
        BigDecimal discount = customer.getApplicableDiscount(subtotal);
        
        return subtotal.add(tax).subtract(discount);
    }
    
    private BigDecimal calculateTax(BigDecimal amount) {
        return amount.multiply(customer.getLocation().getTaxRate());
    }
    
    private void notifyKitchen(MenuItem item) {
        if (item.requiresPreparation()) {
            KitchenService.addToQueue(item, item.getPreparationTime());
        }
    }
}

public class MenuItem {
    private String name;
    private BigDecimal price;
    private List<Ingredient> ingredients;
    private int preparationMinutes;
    
    public boolean isAvailable() {
        return ingredients.stream()
            .allMatch(Ingredient::isInStock);
    }
    
    public List<String> getAllergens() {
        return ingredients.stream()
            .flatMap(i -> i.getAllergens().stream())
            .distinct()
            .collect(Collectors.toList());
    }
}

public class Customer {
    private String name;
    private LoyaltyAccount loyaltyAccount;
    private Location location;
    private List<String> dietaryRestrictions;
    
    public BigDecimal getApplicableDiscount(BigDecimal orderTotal) {
        if (loyaltyAccount.getPoints() > 100) {
            return orderTotal.multiply(new BigDecimal(\"0.10\")); // 10% off
        }
        return BigDecimal.ZERO;
    }
    
    public List<MenuItem> getRecommendations(Menu menu) {
        return menu.getItems().stream()
            .filter(item -> !hasAllergenConflict(item))
            .filter(MenuItem::isAvailable)
            .limit(5)
            .collect(Collectors.toList());
    }
    
    private boolean hasAllergenConflict(MenuItem item) {
        return item.getAllergens().stream()
            .anyMatch(dietaryRestrictions::contains);
    }
}

Now each object manages its own complexity. The Order doesn’t need to know how loyalty discounts work—that’s the Customer’s job. The MenuItem doesn’t need to know tax rates—that’s handled by the Location. Each class has a single, clear responsibility.

When the restaurant adds a new feature like "Buy 2 get 1 free on Tuesdays," you add logic to one place—probably a new discount strategy class. You don’t hunt through hundreds of functions trying to figure out where discount logic lives.

Composition

Everyone learns inheritance first. Dog extends Animal. It’s simple and intuitive. But in real-world applications, inheritance often becomes a trap.

Imagine you’re building an e-commerce system. You create a Product class. Then you need DigitalProduct and PhysicalProduct, so you use inheritance. Great! But then you need products that can be both digital and physical (like a book with an ebook code). Now what?

This is where composition shines. Instead of trying to create the perfect inheritance hierarchy, you compose objects from smaller, focused components:

public class Product {
    private PricingStrategy pricingStrategy;
    private ShippingStrategy shippingStrategy;
    private InventoryStrategy inventoryStrategy;
    
    public BigDecimal calculatePrice() {
        return pricingStrategy.calculatePrice(this);
    }
    
    public ShippingCost getShippingCost(Address address) {
        return shippingStrategy.calculate(this, address);
    }
}

Now you can mix and match behaviors without being locked into a rigid hierarchy. A product can have digital delivery, physical inventory tracking, and subscription-based pricing—all at once. The strategies are just objects you can swap out.

This was a revelation for me. In JavaScript, I was always passing functions around. In Java, I was passing objects around—but objects that encapsulate both data AND behavior. It’s the same idea, just more structured.

Interfaces

When I first encountered interfaces, they seemed pointless. Why write implements Serializable when you could just write the methods you need?

The answer became clear when I worked on a team of 20 developers. Interfaces are contracts. They’re promises about what your code can do, independent of how it does it.

Here’s a real scenario: You’re building a notification system. You need to send emails, SMS, push notifications, and Slack messages. Without interfaces, you might create four completely different classes with different method names:

emailService.sendEmail(user, message);
smsService.transmitSMS(phoneNumber, content);
pushService.notify(deviceToken, alert);
slackService.postMessage(channel, text);

How do you write code that sends a notification without knowing which service to use? You can’t, not cleanly.

But with interfaces:

public interface NotificationService {
    void send(Recipient recipient, Message message);
}

public class NotificationManager {
    private List<NotificationService> services;
    
    public void notifyUser(User user, String content) {
        Message message = new Message(content);
        services.forEach(service -> service.send(user, message));
    }
}

Now you can add a new notification method by just creating a new class that implements NotificationService. No changes to existing code. This is the Open/Closed Principle in action—open for extension, closed for modification.

Abstract Classes

Abstract classes confused me for years. They’re like interfaces but with implementation? Why not just use one or the other?

Abstract classes shine when you have common behavior that you want to enforce across subclasses, but you also need some flexibility.

Consider a payment processing system:

public abstract class PaymentProcessor {
    
    // Common logic all processors need
    public PaymentResult processPayment(PaymentRequest request) {
        if (!validateRequest(request)) {
            return PaymentResult.invalid(\"Invalid payment request\");
        }
        
        PaymentResult result = executePayment(request);
        
        logTransaction(request, result);
        notifyCustomer(request, result);
        
        return result;
    }
    
    // Subclasses must implement this
    protected abstract PaymentResult executePayment(PaymentRequest request);
    
    // Common validation logic
    private boolean validateRequest(PaymentRequest request) {
        return request.getAmount() > 0 && request.getCustomer() != null;
    }
    
    // Subclasses can override if needed
    protected void notifyCustomer(PaymentRequest request, PaymentResult result) {
        // Default email notification
    }
}

Now when you create StripePaymentProcessor or PayPalPaymentProcessor, you only implement the payment execution logic. The validation, logging, and notification all happen automatically. You can’t forget to log a transaction because the abstract class handles it.

This is the Template Method pattern. The abstract class provides the template; subclasses fill in the specific steps.

Encapsulation

Every tutorial tells you to make fields private and use getters and setters. But that’s not encapsulation—that’s just hiding data.

Real encapsulation is about hiding complexity and protecting invariants.

Bad encapsulation (what most tutorials show):

public class BankAccount {
    private BigDecimal balance;
    
    public BigDecimal getBalance() { return balance; }
    public void setBalance(BigDecimal balance) { this.balance = balance; }
}

This is useless. Anyone can call setBalance and set it to anything. You’ve just added boilerplate.

Good encapsulation:

public class BankAccount {
    private BigDecimal balance;
    private List<Transaction> transactions;
    
    public void deposit(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException(\"Deposit amount must be positive\");
        }
        balance = balance.add(amount);
        transactions.add(new Transaction(TransactionType.DEPOSIT, amount));
    }
    
    public void withdraw(BigDecimal amount) {
        if (amount.compareTo(balance) > 0) {
            throw new InsufficientFundsException(\"Cannot withdraw more than balance\");
        }
        balance = balance.subtract(amount);
        transactions.add(new Transaction(TransactionType.WITHDRAWAL, amount));
    }
    
    public BigDecimal getBalance() { 
        return balance; // Read-only access
    }
}

Now your balance can never be invalid. You can’t accidentally set it to a negative number. You can’t modify the balance without creating a transaction record. The class enforces its own rules.

This is what I missed in JavaScript. Sure, you can write defensive code, but nothing stops another developer (or future you) from directly modifying that object property. In Java, the compiler enforces your design decisions.

Inner Classes and Anonymous Classes

When I first saw inner classes, they looked like weird Java syntax quirks. Why would you nest a class inside another class?

Then I encountered event handling:

public class UserInterface {
    private Button submitButton;
    
    public void initialize() {
        submitButton.setOnClickListener(new ClickListener() {
            @Override
            public void onClick() {
                // This inner class has access to UserInterface's private members
                validateAndSubmitForm();
            }
        });
    }
    
    private void validateAndSubmitForm() {
        // Implementation
    }
}

Inner classes have access to their enclosing class’s private members. This is incredibly powerful for creating tightly coupled components that need to work together.

Java 8 made this cleaner with lambdas, but understanding inner classes helps you understand what lambdas are actually doing under the hood.

Static Nested Classes

Static nested classes are different from inner classes—they don’t have access to the outer class’s instance members. So why use them?

For organization and encapsulation:

public class HttpClient {
    
    public static class Builder {
        private int timeout = 30;
        private int maxRetries = 3;
        private boolean followRedirects = true;
        
        public Builder timeout(int seconds) {
            this.timeout = seconds;
            return this;
        }
        
        public Builder maxRetries(int retries) {
            this.maxRetries = retries;
            return this;
        }
        
        public HttpClient build() {
            return new HttpClient(this);
        }
    }
    
    private final int timeout;
    private final int maxRetries;
    
    private HttpClient(Builder builder) {
        this.timeout = builder.timeout;
        this.maxRetries = builder.maxRetries;
    }
}

// Usage
HttpClient client = new HttpClient.Builder()
    .timeout(60)
    .maxRetries(5)
    .build();

The Builder class is only relevant in the context of HttpClient, so it lives inside it. This keeps your code organized and makes the relationship explicit.

What I Learned

Java’s OOP features aren’t arbitrary restrictions. They’re tools for managing complexity in large systems with multiple developers over long time periods.

The strict typing that annoyed me? It’s documentation that never gets out of date and catches errors before they reach production.

The interfaces that seemed like boilerplate? They’re contracts that let teams work independently on different parts of the system.

The encapsulation that felt like overkill? It prevents the bugs that cost companies millions of dollars.

JavaScript and PHP are amazing for rapid development and prototyping. But when you need to build software that will be maintained for years, that will be worked on by dozens of developers, that absolutely cannot fail in production—Java’s approach starts to make sense.

I don’t hate JavaScript anymore. I just understand that it’s solving different problems. And now I can appreciate what Java brings to the table.

ALSO READ
Blockchain 0x000 – Understanding the Fundamentals
May 21, 2020 Web3 Development

Imagine a world where strangers can exchange money, share data, or execute agreements without ever needing to trust a central authority. No banks, no intermediaries, no single point of failure yet...

Identity and Access Management (IAM)
May 11, 2020 Identity & Access Management

Who are you — and what are you allowed to do? That's the fundamental question every secure system must answer. And it's exactly what Identity and Access Management (IAM) is built to solve.

How I built a web based CPU Simulator
May 07, 2020 Pet Projects

As someone passionate about computer engineering, reverse engineering, and system internals, I've always been fascinated by what happens "under the hood" of a computer. This curiosity led me to...

Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Access Control Models
Apr 08, 2020 Identity & Access Management

Access control is one of the most fundamental concepts in security. Every time you set file permissions, assign user roles, or restrict access to a resource, you're implementing some form of access control. But not all access control is created equal...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.