Showing posts with label encapsulation. Show all posts
Showing posts with label encapsulation. Show all posts

Tuesday, August 19, 2025

Encapsulation in Java: Writing secure and maintainable code

# Encapsulation in Java: Writing secure and maintainable code

Encapsulation is one of the pillars of Object-Oriented Programming. It’s the foundation that makes your code secure, maintainable, and robust. But what exactly is encapsulation, and why should you care about it as a Software Developer?

What is Encapsulation?

Encapsulation is the practice of bundling data (attributes) and the methods (behavior) that operate on that data into a single unit (in this case, an object), while restricting direct access to the internal components. It is like a protective shield around your data that controls how it can be accessed and modified.

The key is: Hide the internal state and require all interactions to happen through well-defined interface.

Without encapsulation, your program becomes vulnerable to several issues:

  • Data corruption: Any part of your code could modify critical data inappropriately, and with this I don’t only mean hackers but even our fellow team members because the poor designed interface of the component
  • Inconsistent state: Objects might end up in invalid states that could cause horrendous errors
  • Debugging nightmares: When data can be modified anywhere, tracking down bugs becomes unbearable

A Banking Example

Let’s use a very simple use case, imagine we have to write software to handle bank accounts (yes! I know, yet another banking example 😂). Requirements are:

  • Bank Accounts must have an owner and a balance
  • Bank Accounts must maintain a history of all transactions performed on them
  • Users can deposit funds into Bank Accounts
  • Users can withdraw funds from Bank Accounts

Let’s see encapsulation at work with this simple, practical example:

Poor Encapsulation (Don’t do this, please!)

public class BadBankAccount {
    public String owner;
    public double balance;
    public List<String> transactions;

    public BadBankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance;
        this.transactions = new ArrayList<>();
    }
}

// Let's create an account, no issues here
BadBankAccount account = new BadBankAccount("John Doe", 1000.0);
// Oops! Negative balance allowed
account.balance = -500.0;
// Transaction history is lost!
account.transactions.clear();
// Invalid state
account.owner = null;

As you can see, this approach is problematic, because:

  • All internals are publicly accessible, which is not good
  • No control over what values can be set
  • No protection or access control to internal data, important information can be deleted!
  • No validation or business logic is enforced
  • No clear indication of how to use this component

With Encapsulation (not perfect, but better)

public class BankAccount {
    // Private fields - hidden from outside access
    private String owner;
    private double balance;
    private List<String> transactions;
    private static final double MINIMUM_BALANCE = 0.0;

    // Constructor with validation (This can be better implemented as a factory method)
    public BankAccount(String owner, double initialBalance) {
        if (owner == null || owner.trim().isEmpty()) {
            throw new IllegalArgumentException("Owner name cannot be null or empty");
        }
        if (initialBalance < MINIMUM_BALANCE) {
            throw new IllegalArgumentException("Initial balance cannot be negative");
        }

        this.owner = owner;
        this.balance = initialBalance;
        this.transactions = new ArrayList<>();
        addTransaction("Account opened with balance: $" + initialBalance);
    }

    // Controlled access to balance (read-only)
    public double getBalance() {
        return balance;
    }

    // Controlled access to owner (read-only)
    public String getOwner() {
        return owner;
    }

    // Safe way to view transactions (returns unmodifiable copy)
    public List<String> getTransactionHistory() {
        return Collections.unmodifiableList(transactions); 
    }

    // Business logic for deposits
    public boolean deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit amount must be positive");
            return false;
        }

        balance += amount;
        addTransaction("Deposited: $" + amount + " | New balance: $" + balance);
        return true;
    }

    // Business logic for withdrawals
    public boolean withdraw(double amount) {
        if (amount <= 0) {
            System.out.println("Withdrawal amount must be positive");
            return false;
        }

        if (balance - amount < MINIMUM_BALANCE) {
            System.out.println("Insufficient funds. Current balance: $" + balance);
            return false;
        }

        balance -= amount;
        addTransaction("Withdrew: $" + amount + " | New balance: $" + balance);
        return true;
    }

    // Private helper method - internal implementation detail
    private void addTransaction(String transaction) {
        String timestamp = java.time.LocalDateTime.now().toString();
        transactions.add(timestamp + " - " + transaction);
    }

    // Utility method for account summary
    public void printAccountSummary() {
        String str = String.format(
            "{ owner: %s, balance: %.2f, totalTransactions: %d }",
            owner, balance, transactions.size()
        );
        System.out.println(str);
    }
}

Using the Encapsulated Class

public class BankingDemo {
    public static void main(String[] args) {
        // Create account safely
        BankAccount account = new BankAccount("Alice Johnson", 1000.0);

        // All interactions go through controlled methods
        account.deposit(250.0);    
        account.withdraw(100.0);   
        account.withdraw(2000.0);  

        // Data access is safe and controlled
        System.out.println("Current balance: $" + account.getBalance());
        System.out.println("Account owner: " + account.getOwner());

        // Transaction history is safely accessible
        List<String> history = account.getTransactionHistory();
        System.out.println("\nTransaction History:");
        for (String transaction : history) {
            System.out.println(transaction);
        }
        // This will throw an exception, you cannot modify the list
        // history.clear();

        account.printAccountSummary();
    }
}

Achieving Encapsulation with Java

1. Access Modifiers

The first step to achieve encapsulation is to use correctly the access modifiers.

  • private: Only accessible within the same class
  • protected: Accessible within the same package and subclasses
  • public: Accessible from anywhere
  • Package-private (no modifier): Accessible within the same package

2. Getter Methods

Provide controlled read access to private data:

public String getName() {
    return name;
}

// For collections, return copies (and unmodifiable) to prevent external modification
public List<String> getItems() {
    return new ArrayList<>(items);
    // or
    return Collections.unmodifiableList(items); 
}

3. Setter Methods with Validation

Provide controlled write access with business logic:

public void setAge(int age) {
    if (age < 0 || age > 150) { // business rule
        throw new IllegalArgumentException("Age must be between 0 and 150");
    }
    this.age = age;
}

4. Immutable Objects

For some cases, make objects unchangeable after creation:

public final class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    // No setters - object cannot be changed after creation
}

Best Practices for Encapsulation

  1. Always start by declaring everything private: Classes, fields, methods, always start by making them private, then as needed, make them protected or even public.
  2. Provide public methods only when needed: Don’t create getters/setters automatically
  3. Validate inputs: Always check parameters in public methods
  4. Return copies of mutable internal data or make it immutable: Prevent external modification of internal data
  5. Use meaningful method names: Methods should describe business operations, not just data access
  6. Keep internal logic private: Helper methods should be private, they only have meaning inside the class.

Conclusion

If you need a frontline defender against bugs, data corruption, and maintenance headaches, encapsulation should be part of your go-to strategy. The investment in designing good encapsulation pays dividends throughout the lifetime of your application.

There is a quote I read somewhere and after some research I found who said it:

“Make it easy to do right, and hard to go wrong” - Gretchen Rubin

Design your components with that quote in mind: Design your components to do the “right thing” easy and to do the “wrong thing” hard. Encapsulation is your friend here.

Wednesday, November 30, 2022

A Sip of Java: Encapsulate collection attributes

Encapsulation is the ability we give our classes (and objects created from them) to hide the implementation details of their inner workings and control the consistency of the internal state and invariants.

It happens all the time that we expose some attribute in our class to the outside world in the form of a collection like the following:

This is actually pretty common and I think a bad practice, we tend to create private fields in our classes and as a reflex create setters and getters immediately without thinking about the consequences of exposing the state that way.

If we simply return the reference to the attribute of the object there is a lot of danger there because now others have a copy of the reference to that attribute that is supposed to be private and under the control of the owner object.

Imagine if some code that gets that list of emails from our class now tries to modify the content of that list by adding or removing some objects from it. How can we control that? what if there is some kind of limit on the number of emails a Contact can have? How can we ensure only valid emails are added?

What if, instead, we write the following:

Now users of our class will get a list of emails that cannot be modified. New Email objects cannot be added or removed (but still the objects in the list can be changed if the public methods in the Email class allow that 😉). You could also create a copy of the list of emails by instantiating a new ArrayList sending the list of emails as an argument to the constructor new ArrayList<>(this.emails) but I still think it is better to fail when trying to modify the list.

Probably it is even better to just expose public methods with just the exact functionality that we want to allow like the following:

With the last implementation, we are hiding more and then have even more control. For example, let's say a Contact can have thousands of Emails (I know, it is unlikely, but let's say that is possible), then we can even change internally the way we store Emails from a List to a Map and then be able to have better performance for some operations, like removing an Email.

Do you have suggestions on how to improve the information hiding here? please comment!