Encapsulation

IB Syllabus: B3.1: Apply encapsulation by using access modifiers and accessor/mutator methods.


Key Concepts

What is Encapsulation?

Encapsulation means putting variables and methods together into one unit (the class) and restricting direct access to the variables by making them private. External code can only interact with the object’s data through its public methods: the class controls how its data is read and modified.

This principle is also called information hiding: outside code depends only on the class’s public interface, never on its internal representation, which protects the integrity of the object’s state.

Think of a vending machine: you cannot reach inside and grab items directly. You interact through the defined interface: insert money, press a button, receive the product. The machine’s internal state (stock levels, cash stored) is hidden and protected.

Why Encapsulate?

Without encapsulation, any code anywhere can change any variable to any value:

// WITHOUT encapsulation -- attributes are public
public class BankAccount {
    public String owner;
    public double balance;
}

BankAccount acc = new BankAccount();
acc.balance = -999999.99;  // nothing stops this
acc.owner = "";            // empty string is allowed
# WITHOUT encapsulation - attributes are public (no underscore)
class BankAccount():
    def __init__(self):
        self.owner = ""
        self.balance = 0.0

acc = BankAccount()
acc.balance = -999999.99   # nothing stops this
acc.owner = ""             # empty string is allowed

With encapsulation, the class controls what values are acceptable:

// WITH encapsulation -- attributes are private, access through methods
public class BankAccount {
    private String owner;
    private double balance;

    public BankAccount(String owner, double balance) {
        this.owner = owner;
        this.balance = (balance >= 0) ? balance : 0;
    }

    public double getBalance() { return balance; }

    public void deposit(double amount) {
        if (amount > 0) {
            balance = balance + amount;
        }
    }

    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance = balance - amount;
            return true;
        }
        return false;
    }
}
# WITH encapsulation - attributes private (underscore), access through methods
class BankAccount():
    def __init__(self, owner, balance):
        self._owner = owner
        if balance >= 0:            # Java's one-line ?: check becomes a full if/else
            self._balance = balance
        else:
            self._balance = 0

    def get_balance(self):
        return self._balance

    def deposit(self, amount):
        if amount > 0:
            self._balance = self._balance + amount

    def withdraw(self, amount):
        if amount > 0 and amount <= self._balance:
            self._balance = self._balance - amount
            return True
        return False

Now balance can never go negative: the class enforces this rule in every method that modifies it. External code cannot bypass this protection.

Encapsulation provides four key advantages:

Advantage How
Data hiding Internal representation is hidden; other classes interact only through the public interface
Validation Setters and mutators check that values are valid before accepting them
Flexibility Internal implementation can change without affecting code that uses the class
Security Prevents accidental or malicious modification of an object’s state

Encapsulation = “putting variables and methods into one unit; variables private; accessed only through public members.” This is the exact phrasing IB mark schemes use. Memorise it.

Accessor Methods (Getters)

An accessor method returns the value of a private instance variable without modifying it. It provides read-only access to the object’s data.

Naming convention: getAttributeName() for most types, isAttributeName() for booleans.

public class Student {
    private String name;
    private int grade;
    private boolean graduated;

    // Accessor methods
    public String getName() { return name; }
    public int getGrade() { return grade; }
    public boolean isGraduated() { return graduated; }
}
class Student():
    # (constructor creating _name, _grade, _graduated not shown)

    # Accessor methods
    def get_name(self):
        return self._name

    def get_grade(self):
        return self._grade

    def is_graduated(self):
        return self._graduated

Getters are simple: they just return the value. They do not change anything.

Mutator Methods (Setters)

A mutator method modifies the value of a private instance variable. It provides controlled write access: typically with validation to ensure only valid values are accepted.

Naming convention: setAttributeName(newValue) or a descriptive verb like deposit(), enrol().

public class Student {
    private String name;
    private int grade;
    private boolean graduated;

    // Constructor
    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
        this.graduated = false;
    }

    // Mutator with validation
    public void setGrade(int grade) {
        if (grade >= 0 && grade <= 100) {
            this.grade = grade;
        }
    }

    // Mutator with logic
    public void graduate() {
        if (grade >= 50) {
            this.graduated = true;
        }
    }

    // Accessors
    public String getName() { return name; }
    public int getGrade() { return grade; }
    public boolean isGraduated() { return graduated; }
}
class Student():
    # Constructor
    def __init__(self, name, grade):
        self._name = name
        self._grade = grade
        self._graduated = False

    # Mutator with validation
    def set_grade(self, grade):
        if grade >= 0 and grade <= 100:
            self._grade = grade

    # Mutator with logic
    def graduate(self):
        if self._grade >= 50:
            self._graduated = True

    # Accessors
    def get_name(self):
        return self._name

    def get_grade(self):
        return self._grade

    def is_graduated(self):
        return self._graduated

Not every attribute needs a setter. Some attributes should be read-only after construction: for example, a student’s name or ID should not change after the object is created. Only provide setters for attributes that legitimately need to be modified.

The Encapsulation Pattern

The standard pattern for encapsulated classes:

  1. Attributes are private: never public
  2. Getters are public: provide read access
  3. Setters are public with validation: provide controlled write access
  4. Constructor initialises all attributes, with validation if needed
public class Temperature {
    private double celsius;  // 1. private attribute

    public Temperature(double celsius) {
        if (celsius >= -273.15) {
            this.celsius = celsius;
        } else {
            this.celsius = -273.15;
        }
    }

    public double getCelsius() {       // 2. public getter
        return celsius;
    }

    public void setCelsius(double celsius) {  // 3. public setter with validation
        if (celsius >= -273.15) {
            this.celsius = celsius;
        }
    }

    public double toFahrenheit() {     // public method using the data
        return celsius * 9.0 / 5.0 + 32;
    }
}
class Temperature():
    def __init__(self, celsius):        # 1. private attribute, set with validation
        if celsius >= -273.15:
            self._celsius = celsius
        else:
            self._celsius = -273.15

    def get_celsius(self):              # 2. public getter
        return self._celsius

    def set_celsius(self, celsius):     # 3. public setter with validation
        if celsius >= -273.15:
            self._celsius = celsius

    def to_fahrenheit(self):            # public method using the data
        return self._celsius * 9.0 / 5.0 + 32

What Happens Without Encapsulation

Consider a CircularBuffer class where the internal index must always be between 0 and capacity-1. Without encapsulation:

// Dangerous: public fields
buffer.index = 9999;  // way beyond capacity -- corrupts memory
buffer.index = -1;    // invalid -- causes ArrayIndexOutOfBoundsException
# Dangerous: public attributes
buffer.index = 9999   # way beyond capacity - corrupts the buffer's state
buffer.index = -1     # invalid - causes an IndexError later

With encapsulation, the class protects itself:

public void advanceIndex() {
    index = (index + 1) % capacity;  // always stays in range
}
def advance_index(self):
    self._index = (self._index + 1) % self._capacity   # always stays in range

No external code can set index to an invalid value because index is private.


Worked Examples

Example 1: Designing an Encapsulated Class

Scenario: A Product class for an online store. Each product has a name, price, and stock quantity. Prices must be positive. Stock cannot go negative.

public class Product {
    private String name;
    private double price;
    private int stock;

    public Product(String name, double price, int stock) {
        this.name = name;
        this.price = (price > 0) ? price : 0.01;
        this.stock = (stock >= 0) ? stock : 0;
    }

    // Getters
    public String getName() { return name; }
    public double getPrice() { return price; }
    public int getStock() { return stock; }
    public boolean isInStock() { return stock > 0; }

    // Setters with validation
    public void setPrice(double price) {
        if (price > 0) {
            this.price = price;
        }
    }

    // Business methods
    public boolean sell(int quantity) {
        if (quantity > 0 && quantity <= stock) {
            stock = stock - quantity;
            return true;
        }
        return false;
    }

    public void restock(int quantity) {
        if (quantity > 0) {
            stock = stock + quantity;
        }
    }
}
class Product():
    def __init__(self, name, price, stock):
        self._name = name
        if price > 0:
            self._price = price
        else:
            self._price = 0.01
        if stock >= 0:
            self._stock = stock
        else:
            self._stock = 0

    # Getters
    def get_name(self):
        return self._name

    def get_price(self):
        return self._price

    def get_stock(self):
        return self._stock

    def is_in_stock(self):
        return self._stock > 0

    # Setter with validation
    def set_price(self, price):
        if price > 0:
            self._price = price

    # Business methods
    def sell(self, quantity):
        if quantity > 0 and quantity <= self._stock:
            self._stock = self._stock - quantity
            return True
        return False

    def restock(self, quantity):
        if quantity > 0:
            self._stock = self._stock + quantity

What encapsulation prevents:

  • Setting price to 0 or negative
  • Setting stock to negative
  • Selling more items than available
  • Restocking with a negative amount
  • Directly modifying name after creation (no setter provided: read-only)

Example 2: Before and After

Without encapsulation (broken):

Product p = new Product();
p.price = -50.0;    // invalid price
p.stock = -10;      // impossible stock
// The object is now in an impossible state
p = Product()      # imagine a version with public attributes and no validation
p.price = -50.0    # invalid price
p.stock = -10      # impossible stock
# The object is now in an impossible state

With encapsulation (safe):

Product p = new Product("Widget", -50.0, -10);
// Constructor clamps: price becomes 0.01, stock becomes 0
System.out.println(p.getPrice());  // 0.01
System.out.println(p.getStock());  // 0

p.setPrice(-20.0);  // rejected -- price stays 0.01
p.sell(5);           // rejected -- not enough stock
// The object is always in a valid state
p = Product("Widget", -50.0, -10)
# Constructor clamps: price becomes 0.01, stock becomes 0
print(p.get_price())   # 0.01
print(p.get_stock())   # 0

p.set_price(-20.0)   # rejected - price stays 0.01
p.sell(5)            # rejected - not enough stock
# The object is always in a valid state

Quick Check

Q1. What is encapsulation?

Q2. Which of the following is NOT an advantage of encapsulation?

Q3. What does an accessor method do?

Q4. A Product has price 19.99. What happens when setPrice(-5) is called?

Q5. Should every private attribute have a public setter?


Spot the Error

This class compiles but violates encapsulation. Click the buggy line, then pick the fix.

1public class BankAccount { 2 private String owner; 3 public double balance; 4 public void deposit(double amount) { 5 if (amount > 0) { balance += amount; } 6 } 7}

Pick the fix:


Fill in the Blanks

Complete the encapsulated class:

public class Student {
     String name;
     int grade;

    public Student(String name, int grade) {
         = name;
         = grade;
    }

    // Accessor method
    public String () {
        return name;
    }

    // Mutator method with validation
    public void setGrade(int grade) {
        if (grade >= 0 && grade <= 100) {
             = grade;
        }
    }
}

Predict the Output

Using the Product class from Example 1:

Product p = new Product("Widget", 9.99, -5);
p.sell(3);
System.out.println(p.getStock());

What is printed?

What is printed?

Product p = new Product("Gadget", 29.99, 10);
p.sell(3);
p.sell(5);
p.sell(5);
System.out.println(p.getStock());

What is printed?


Practice Exercises

Core

  1. Explain why: A student writes public int age; in their Person class. Explain two problems this causes and how encapsulation solves each one.

  2. Write an encapsulated class: Create a BankAccount class with private owner (String) and balance (double). Include a constructor, getters, deposit(double), and withdraw(double) with validation (no negative amounts, cannot withdraw more than the balance). Test with several scenarios including invalid operations.

  3. Read-only attributes: Create a Student class where name and studentID have getters but NO setters (read-only after construction), while grade has both a getter and a validated setter (0-100).

Extension

  1. Encapsulation audit: The following class has three encapsulation violations. Find and fix all of them:
    public class Ticket {
        public String event;
        public double price;
        private boolean used;
        public void useTicket() { used = true; }
        public void setPrice(double p) { price = p; }
    }
    
  2. Design challenge: A Password class stores a hashed password. Design the class so that: the password can be set (with minimum length validation) but NEVER read back (no getter for the actual password). Provide a checkPassword(String attempt) method that returns true/false.

Challenge

  1. Full encapsulated system: Design a Library class that manages an array of Book objects. The library should encapsulate its book collection (private array) and provide methods to: add a book, remove by ISBN, check out (sets a flag), return (clears the flag), and count available books. No external code should be able to access the array directly or modify books without going through Library methods.

Connections

  • Prerequisites: The this Keyword: this enables the getter/setter pattern
  • Prerequisites: Constructors: constructors initialise private attributes
  • Next: Access Modifiers. Deeper dive into private, public, protected
  • Related: UML Class Diagrams. - means private, + means public
  • Forward: Inheritance. Encapsulated attributes are inherited but accessed through getters in child classes

© EduCS.me — A resource hub for Computer Science education

This site uses Just the Docs, a documentation theme for Jekyll.