OOP Encapsulation Fundamentals: Information Hiding & Visibility
Encapsulation is one of the four fundamental principles of object-oriented programming. It hides the internal state and implementation details of an object, exposing only well-defined interfaces to the outside world.
What Is Encapsulation?
Encapsulation bundles data and behavior into a single unit and protects the internal representation from unauthorized access. The goal is robust, maintainable, and secure software through clear boundaries of responsibility.
Core Encapsulation Principles
- Information Hiding: Internal details remain hidden
- Interface Control: Only defined access points are available
- Invariant Protection: Object state stays consistent
- Reduced Coupling: Fewer dependencies between components
Visibility Modifiers
Java Visibility Levels
public class VisibilityDemo {
// public: accessible from anywhere
public String publicField = "public";
// protected: accessible within the class and subclasses
protected String protectedField = "protected";
// package-private: accessible only within the package
String packageField = "package-private";
// private: accessible only within the class
private String privateField = "private";
// Private method - internal logic
private void validateInput(String input) {
if (input == null || input.trim().isEmpty()) {
throw new IllegalArgumentException("Input darf nicht leer sein");
}
}
// Public method with validation
public void processData(String data) {
validateInput(data); // Call private method
// Processing...
}
}
C# Access Modifiers
public class BankAccount
{
// public: accessible from anywhere
public string AccountNumber { get; }
// private: accessible only within the class
private decimal balance;
// protected: accessible within the class and derived classes
protected string AccountType { get; set; }
// internal: accessible only within the assembly
internal string BankCode { get; set; }
// protected internal: accessible within assembly or derived classes
protected internal string BranchCode { get; set; }
// Public property with private setter
public decimal Balance
{
get { return balance; }
private set { balance = value; }
}
}
Python Access Control
class BankAccount:
def __init__(self, account_number: str):
# Public attribute
self.account_number = account_number
# Protected attribute (convention)
self._balance = 0.0
# Private attribute (name mangling)
self.__transaction_history = []
def deposit(self, amount: float):
"""Public method with validation"""
if amount <= 0:
raise ValueError("Amount must be positive")
self._balance += amount
self.__add_transaction("deposit", amount)
def _validate_amount(self, amount: float):
"""Protected method for subclasses"""
return amount > 0
def __add_transaction(self, transaction_type: str, amount: float):
"""Private method - internal use only"""
self.__transaction_history.append({
'type': transaction_type,
'amount': amount,
'timestamp': datetime.now()
})
Getters and Setters
Effective Getter/Setter Implementation
public class BankAccount {
private String iban;
private int balanceInCents;
private boolean isActive = true;
// Constructor with validation
public BankAccount(String iban) {
if (iban == null || !isValidIban(iban)) {
throw new IllegalArgumentException("Ungültige IBAN");
}
this.iban = iban;
this.balanceInCents = 0;
}
// Getter for read access
public int getBalanceInCents() {
return balanceInCents;
}
// Getter with formatting
public String getFormattedBalance() {
return String.format("€%.2f", balanceInCents / 100.0);
}
// Setter with validation and business logic
public void setBalanceInCents(int balanceInCents) {
if (!isActive) {
throw new IllegalStateException("Konto ist deaktiviert");
}
if (balanceInCents < 0) {
throw new IllegalArgumentException("Negativer Kontostand nicht erlaubt");
}
this.balanceInCents = balanceInCents;
}
// Business method instead of simple setter
public void deposit(int cents) {
if (cents <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein");
}
this.balanceInCents += cents;
}
public boolean withdraw(int cents) {
if (cents <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein");
}
if (cents > balanceInCents) {
return false; // Insufficient funds
}
this.balanceInCents -= cents;
return true;
}
// Private validation method
private boolean isValidIban(String iban) {
// IBAN validation logic
return iban != null && iban.matches("[A-Z]{2}[0-9]{20}");
}
}
Python Properties
class BankAccount:
def __init__(self, iban: str):
self._iban = iban
self._balance = 0.0
@property
def balance(self) -> float:
"""Getter for account balance"""
return self._balance
@property
def iban(self) -> str:
"""Read-only property for IBAN"""
return self._iban
@balance.setter
def balance(self, value: float):
"""Setter with validation"""
if value < 0:
raise ValueError("Kontostand darf nicht negativ sein")
self._balance = value
@property
def formatted_balance(self) -> str:
"""Computed property"""
return f"€{self._balance:.2f}"
def deposit(self, amount: float):
"""Business method instead of direct setter"""
if amount <= 0:
raise ValueError("Betrag muss positiv sein")
self._balance += amount
Immutability
Immutable Objects in Java
// Immutable class with final fields
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies; // Defensive copy!
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = Objects.requireNonNull(name, "Name darf nicht null sein");
this.age = age;
// Defensive copy for mutable parameters
this.hobbies = List.copyOf(Objects.requireNonNull(hobbies));
}
// Getters - no setters!
public String getName() {
return name;
}
public int getAge() {
return age;
}
// Defensive copy for mutable returns
public List<String> getHobbies() {
return new ArrayList<>(hobbies);
}
// Methods create new instances
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.hobbies);
}
public ImmutablePerson addHobby(String hobby) {
List<String> newHobbies = new ArrayList<>(this.hobbies);
newHobbies.add(hobby);
return new ImmutablePerson(this.name, this.age, newHobbies);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutablePerson that = (ImmutablePerson) o;
return age == that.age &&
Objects.equals(name, that.name) &&
Objects.equals(hobbies, that.hobbies);
}
@Override
public int hashCode() {
return Objects.hash(name, age, hobbies);
}
@Override
public String toString() {
return "ImmutablePerson{name='" + name + "', age=" + age + ", hobbies=" + hobbies + "}";
}
}
Python Dataclasses for Immutability
from dataclasses import dataclass
from typing import List
import copy
@dataclass(frozen=True)
class ImmutablePerson:
name: str
age: int
hobbies: List[str] # Warning: the list itself is still mutable!
def __post_init__(self):
# Validation after initialization
if self.age < 0:
raise ValueError("Age cannot be negative")
if not self.name:
raise ValueError("Name cannot be empty")
# Defensive copy for mutable fields
object.__setattr__(self, 'hobbies', tuple(self.hobbies))
def with_age(self, new_age: int) -> 'ImmutablePerson':
"""Returns a new instance with updated age"""
return ImmutablePerson(self.name, new_age, list(self.hobbies))
def add_hobby(self, hobby: str) -> 'ImmutablePerson':
"""Returns a new instance with an additional hobby"""
new_hobbies = list(self.hobbies) + [hobby]
return ImmutablePerson(self.name, self.age, new_hobbies)
Defensive Copies
Protecting Against External Mutation
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
private final Customer customer;
public ShoppingCart(Customer customer) {
this.customer = Objects.requireNonNull(customer);
}
// Defensive copy on return
public List<Item> getItems() {
return new ArrayList<>(items); // Return a copy
}
// Defensive copy on parameter
public void addItems(List<Item> newItems) {
if (newItems != null) {
this.items.addAll(new ArrayList<>(newItems)); // Store a copy
}
}
// Unmodifiable view
public List<Item> getItemsUnmodifiable() {
return Collections.unmodifiableList(items);
}
// Stream API for safe access
public Stream<Item> itemStream() {
return items.stream();
}
}
Python Defensive Copies
class ShoppingCart:
def __init__(self, customer):
self._customer = customer
self._items = []
def get_items(self):
"""Return a defensive copy"""
return self._items.copy()
def add_items(self, items):
"""Store a defensive copy"""
if items:
self._items.extend(items.copy())
def get_items_immutable(self):
"""Return an immutable view"""
return tuple(self._items)
def items_iterator(self):
"""Iterator for safe access"""
return iter(self._items)
Law of Demeter
Avoiding Chained Method Calls
// Bad - violates Law of Demeter
public void processOrder(Order order) {
// Too many dots - tight coupling
String city = order.getCustomer().getAddress().getCity();
double tax = order.getCustomer().getAddress().getTaxRate();
// ...
}
// Good - decoupled
public void processOrder(Order order) {
String city = order.getCustomerCity();
double tax = order.getCustomerTaxRate();
// ...
}
// Better implementation
public class Order {
private Customer customer;
public String getCustomerCity() {
return customer.getAddress().getCity();
}
public double getCustomerTaxRate() {
return customer.getAddress().getTaxRate();
}
}
Validation and Invariants
Robust Validation Strategy
public class EmailAddress {
private final String value;
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
public EmailAddress(String email) {
String normalized = normalize(email);
validate(normalized);
this.value = normalized;
}
private String normalize(String email) {
if (email == null) {
throw new IllegalArgumentException("Email cannot be null");
}
return email.trim().toLowerCase();
}
private void validate(String email) {
if (email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be empty");
}
if (!EMAIL_PATTERN.matcher(email).matches()) {
throw new IllegalArgumentException("Invalid email format");
}
if (email.length() > 254) {
throw new IllegalArgumentException("Email is too long");
}
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}
Design by Contract
Preconditions and Postconditions
public class BankTransfer {
private final BankAccount fromAccount;
private final BankAccount toAccount;
public BankTransfer(BankAccount fromAccount, BankAccount toAccount) {
this.fromAccount = Objects.requireNonNull(fromAccount);
this.toAccount = Objects.requireNonNull(toAccount);
}
/**
* Transfers an amount from one account to another
*
* @param amount Transfer amount in cents
* @throws IllegalArgumentException if amount <= 0
* @throws IllegalStateException if source account has insufficient funds
* @throws IllegalStateException if either account is inactive
* @post fromAccount.getBalance() == old(fromAccount.getBalance()) - amount
* @post toAccount.getBalance() == old(toAccount.getBalance()) + amount
*/
public void transfer(int amount) {
// Check preconditions
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
if (!fromAccount.isActive() || !toAccount.isActive()) {
throw new IllegalStateException("Both accounts must be active");
}
if (fromAccount.getBalanceInCents() < amount) {
throw new IllegalStateException("Insufficient funds in source account");
}
// Store old state for postcondition verification
int oldFromBalance = fromAccount.getBalanceInCents();
int oldToBalance = toAccount.getBalanceInCents();
try {
// Atomic operation
fromAccount.withdraw(amount);
toAccount.deposit(amount);
// Verify postconditions
assert fromAccount.getBalanceInCents() == oldFromBalance - amount;
assert toAccount.getBalanceInCents() == oldToBalance + amount;
} catch (Exception e) {
// Rollback on error
throw new RuntimeException("Transfer failed", e);
}
}
}
Encapsulation in Practice
Example: E-Commerce Order System
public class Order {
private final String orderId;
private final Customer customer;
private final List<OrderItem> items;
private OrderStatus status;
private final LocalDateTime createdAt;
private LocalDateTime shippedAt;
// Private constructor - use factory method
private Order(String orderId, Customer customer) {
this.orderId = Objects.requireNonNull(orderId);
this.customer = Objects.requireNonNull(customer);
this.items = new ArrayList<>();
this.status = OrderStatus.PENDING;
this.createdAt = LocalDateTime.now();
}
// Factory method for object creation
public static Order create(Customer customer) {
if (customer == null || !customer.isActive()) {
throw new IllegalArgumentException("Invalid customer");
}
String orderId = generateOrderId();
return new Order(orderId, customer);
}
// Business method with state transitions
public void addItem(Product product, int quantity) {
if (product == null) {
throw new IllegalArgumentException("Product cannot be null");
}
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Order cannot be modified");
}
OrderItem item = new OrderItem(product, quantity);
items.add(item);
}
// State transition with validation
public void ship() {
if (status != OrderStatus.CONFIRMED) {
throw new IllegalStateException("Order must be confirmed");
}
if (items.isEmpty()) {
throw new IllegalStateException("Order is empty");
}
this.status = OrderStatus.SHIPPED;
this.shippedAt = LocalDateTime.now();
// Publish event
EventPublisher.publish(new OrderShippedEvent(orderId));
}
// Safe getters with defensive copies
public List<OrderItem> getItems() {
return new ArrayList<>(items);
}
public Customer getCustomer() {
return customer; // Immutable, no copy needed
}
// Computed property
public BigDecimal getTotalAmount() {
return items.stream()
.map(OrderItem::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Status getter - immutable
public OrderStatus getStatus() {
return status;
}
// Private helper method
private static String generateOrderId() {
return "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
}
}
Benefits of Encapsulation
1. Reduced Coupling
- Components depend less on each other
- Changes have limited ripple effects
- Refactoring becomes simpler
2. Higher Cohesion
- Related functionality is bundled together
- Clear responsibilities
- Easier to understand
3. Improved Security
- Controlled access to data
- Validation at boundaries
- Protection against inconsistent states
4. Better Testability
- Clear interfaces for unit tests
- Easier to create mocks
- Focus on behavior rather than implementation
Drawbacks and Challenges
1. Extra Effort
- More code for getters and setters
- Boilerplate in simple data classes
- Higher implementation overhead
2. Over-Encapsulation
- Too many small methods
- Unnecessary abstraction layers
- Complexity without benefit
3. Learning Curve
- Understanding good encapsulation takes practice
- Balancing openness and closure isn’t intuitive
- Experience needed to get granularity right
Exam-Relevant Questions
Typical IHK Questions
-
What’s the difference between encapsulation and abstraction?
- Encapsulation hides implementation details; abstraction reduces complexity
-
When do getters and setters make sense?
- Only when there’s a business reason, not for every private field
-
Why are public fields problematic?
- They bypass validation and break invariants
-
How does immutability support encapsulation?
- It guarantees stable invariants and simplifies concurrent use
-
What does the Law of Demeter say?
- Talk only to your direct neighbors; avoid chained method calls
Summary
Encapsulation is a fundamental principle for robust, maintainable software. It protects internal state, defines clear interfaces, and enables safe refactoring. Good encapsulation requires:
- Careful visibility planning
- Validation at boundaries
- Defensive programming
- Deliberate immutability
- Clear responsibilities
Striking the right balance between sufficient encapsulation and practical usability is key to successful software architecture.
Continue on the OOP Learning Path
All OOP articles are now complete. Return to the first article: Object-Oriented Programming OOP Fundamentals.

