Access Modifiers
Access modifiers control the visibility and accessibility of class members. They are the foundation of encapsulation.
Access Levels
┌────────────┬─────────┬─────────┬────────────┬──────────┐
│ Modifier │ Class │ Package │ Subclass │ World │
├────────────┼─────────┼─────────┼────────────┼──────────┤
│ public │ ✓ │ ✓ │ ✓ │ ✓ │
│ protected │ ✓ │ ✓ │ ✓ │ ✗ │
│ (default) │ ✓ │ ✓ │ ✗ │ ✗ │
│ private │ ✓ │ ✗ │ ✗ │ ✗ │
└────────────┴─────────┴─────────┴────────────┴──────────┘
Usage Guidelines
public class BankAccount {
// Private: Internal state (never expose directly)
private String accountId;
private double balance;
private List<Transaction> transactions;
// Protected: Extension points for subclasses
protected void validateAmount(double amount) {
if (amount <= 0) throw new IllegalArgumentException();
}
// Public: API surface (what users of this class can do)
public void deposit(double amount) {
validateAmount(amount);
this.balance += amount;
}
public double getBalance() {
return this.balance;
}
}
The Principle of Least Privilege
Start with the most restrictive access and increase only when needed:
Step 1: Make everything private
Step 2: Add getters for read access
Step 3: Add setters with validation
Step 4: Only use protected for intentional extension
Step 5: Public only for the class API
When to Use Each Modifier
| Modifier | Use When |
|---|---|
private |
Default for all fields and helper methods |
protected |
Designing for inheritance, framework extension points |
default |
Classes in same package need access |
public |
Part of the class's external API |
Common Mistakes
- Public fields: No validation, no encapsulation
- Overly permissive: Everything public "for convenience"
- Anemic models: Only getters/setters, no behavior
- Leaking internals: Returning mutable internal collections
Getters and Setters
Getters and setters provide controlled access to private fields. They are not just accessors — they are opportunities for validation and logic.
Basic Getter/Setter
public class User {
private String name;
private int age;
private String email;
// Getter
public String getName() {
return this.name;
}
// Setter with validation
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = name.trim();
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age");
}
this.age = age;
}
}
Computed Properties
class Rectangle {
private double width;
private double height;
// Computed property (no field backing)
public double getArea() {
return width * height;
}
public double getPerimeter() {
return 2 * (width + height);
}
}
Read-Only Properties
public class Order {
private final String id;
private final LocalDateTime createdAt;
private OrderStatus status;
// Read-only: no setter for id and createdAt
public String getId() { return id; }
public LocalDateTime getCreatedAt() { return createdAt; }
// Mutable with controlled setter
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) {
// Business logic: validate transitions
if (!this.status.canTransitionTo(status)) {
throw new IllegalStateException("Invalid status transition");
}
this.status = status;
}
}
Defensive Copying
public class ShoppingCart {
private List<Item> items = new ArrayList<>();
// Bad: Returns reference to internal list
public List<Item> getItemsBad() {
return items; // Caller can modify!
}
// Good: Returns defensive copy
public List<Item> getItems() {
return new ArrayList<>(items);
}
// Good: Accepts defensive copy
public void setItems(List<Item> newItems) {
this.items = new ArrayList<>(newItems);
}
}
When to Skip Getters/Setters
| Situation | Approach |
|---|---|
| Value objects | Only getters, no setters (immutable) |
| Internal state | Private with no accessors |
| Derived values | Only getter (computed) |
| Write-only | Only setter (e.g., password hash) |
Data Hiding
Data hiding goes beyond access modifiers. It's about concealing implementation details so that classes can change internally without affecting consumers.
What to Hide
Public API (What users see) Internal Implementation
───────────────────────── ──────────────────────
User user = new User(); - Database schema
user.setName("John"); - Cache strategy
String name = user.getName(); - Serialization format
- Validation rules
- Internal state machines
Information Hiding Patterns
Pattern 1: Hide Data Source
public class UserRepository {
public User findById(String id) {
// Caller doesn't know if data comes from
// database, cache, or API
return cache.get(id);
}
}
Pattern 2: Hide Algorithm
public class PricingService {
public Money calculatePrice(Order order) {
// Caller doesn't know the pricing algorithm
Money base = calculateBasePrice(order);
Money discount = calculateDiscount(order);
Money tax = calculateTax(base.subtract(discount));
return base.subtract(discount).add(tax);
}
}
Pattern 3: Hide State Transitions
public class Order {
private OrderStatus status;
public void pay() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException();
}
this.status = OrderStatus.PAID;
}
public void ship() {
if (status != OrderStatus.PAID) {
throw new IllegalStateException();
}
this.status = OrderStatus.SHIPPED;
}
// Callers don't need to know the state machine
}
Benefits of Data Hiding
- Change freedom: Modify internals without breaking consumers
- Validation: Ensure invariants are maintained
- Security: Prevent unauthorized access
- Simplification: Reduce cognitive load for users
- Flexibility: Add logging, caching, or transactions transparently
Encapsulation in System Design
┌─────────────────────────────────────────────┐
│ Payment Service │
├─────────────────────────────────────────────┤
│ Public: processPayment(order), refund(order)│
├─────────────────────────────────────────────┤
│ Internal (hidden): │
│ - Fraud detection algorithm │
│ - Payment provider routing │
│ - Retry logic │
│ - Transaction management │
│ - idempotency key generation │
└─────────────────────────────────────────────┘
Users only see: processPayment() → success/failure
They don't see: fraud check → provider selection → retry → ...
Practice Problems
Design a scalable Encapsulation system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Encapsulation to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Encapsulation and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. Which access modifier allows access within the same package but not to subclasses outside the package?
2. Why should you return defensive copies of mutable internal collections?
3. What is the principle of least privilege in access control?
4. Which is NOT a benefit of data hiding?
5. When should you NOT provide a setter for a field?
Flashcards
Question
What is encapsulation?
Click to reveal answer
Answer
Bundling data and methods that operate on that data into a single unit (class), while restricting direct access to some components to protect object integrity.
Question
What are the 4 access modifiers in Java?
Click to reveal answer
Answer
public (everywhere), protected (same package + subclasses), default/package (same package only), private (same class only).
Question
What is defensive copying?
Click to reveal answer
Answer
Creating copies of mutable objects when returning them from getters or accepting them in setters, to prevent external modification of internal state.
Question
What should setters validate?
Click to reveal answer
Answer
Null values, range boundaries, format validity, and business rule compliance. Setters enforce class invariants.
Question
What is the difference between encapsulation and data hiding?
Click to reveal answer
Answer
Encapsulation is bundling data + methods. Data hiding is restricting access to internal details. Encapsulation is the mechanism; data hiding is the outcome.
Revision Notes
Key Takeaways
- 1.Encapsulation bundles data and methods while restricting access
- 2.Start with private and increase visibility only when needed
- 3.Setters provide validation opportunities to maintain invariants
- 4.Defensive copying prevents external modification of internal state
- 5.Data hiding enables internal changes without breaking consumers
Interview Tips
- •Explain why you're using specific access levels in your design
- •Show how setters validate data to maintain object invariants
- •Discuss how data hiding allows internal implementation changes
- •Mention defensive copying when returning mutable objects
Cheat Sheet
Encapsulation - Cheat Sheet
Access Modifiers:
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
| public | ✓ | ✓ | ✓ | ✓ |
| protected | ✓ | ✓ | ✓ | ✗ |
| default | ✓ | ✓ | ✗ | ✗ |
| private | ✓ | ✗ | ✗ | ✗ |
Getter/Setter Rules:
- Validate in setters (null, range, format)
- Return defensive copies of mutable collections
- Skip setters for immutable fields
- Computed properties: only getter
Data Hiding Benefits:
- Internal changes don't break consumers
- Invariants maintained through validation
- Security via restricted access
- Reduced coupling
Principle of Least Privilege:
Start private → increase only when needed