Encapsulation — Practice Tasks¶
Twelve exercises in hiding state, protecting invariants, and choosing the right mutation primitives.
Task 1 — Convert public fields to encapsulated form¶
Refactor to a record. Verify: external code can't write user.name = "X".
Task 2 — Defensive copying¶
public class Polygon {
private List<Point> points;
public Polygon(List<Point> points) { this.points = points; }
public List<Point> points() { return points; }
}
Two leaks: caller can mutate points after construction; caller can mutate via points(). Fix both.
Task 3 — Tell, don't ask¶
class Account {
private long balance;
public long balance() { return balance; }
public void setBalance(long b) { this.balance = b; }
}
void withdraw(Account a, long amount) {
if (a.balance() >= amount) a.setBalance(a.balance() - amount);
}
Refactor so Account encapsulates the rule. The free function should disappear.
Task 4 — Invariant enforcement¶
Design Range(int min, int max) such that: - min <= max is always true - The class is immutable - Construction is the only place this is checked
Use a record with a compact constructor.
Task 5 — Read-only collection view¶
Library has a List<Book> of books. Expose: - A method to add books (validating: no duplicates by ISBN) - A method to get all books read-only
Use either List.copyOf (snapshot) or Collections.unmodifiableList (view) and explain which is appropriate.
Task 6 — Static factory¶
Replace this constructor with a static factory:
public Currency(String code) {
if (code.length() != 3) throw new IllegalArgumentException();
this.code = code;
}
The factory should: - Cache instances (use a ConcurrentHashMap) - Make the constructor private - Be named of(String code)
Two calls with the same code must return the same instance.
Task 7 — Sealed Result type¶
Define sealed interface Result<T> permits Success<T>, Failure<T> { } with two records. Add: - boolean isSuccess() - T value() — throws if Failure - String error() — throws if Success - <U> Result<U> map(Function<T, U> f)
All the encapsulation lives in the sealed interface + records. Test exhaustively.
Task 8 — Anemic refactor¶
public class Order { public List<Item> items; public boolean shipped; }
public class OrderService {
public void ship(Order o) {
if (o.shipped) throw new IllegalStateException();
// shipping logic
o.shipped = true;
}
}
Move the logic into Order. The service should call order.ship(). Fields become private.
Task 9 — Module-based encapsulation¶
Design a simple payment module with module-info.java: - com.example.payment.api — public types (Payment, Gateway interface) - com.example.payment.internal.stripe — StripeGateway impl, NOT exported
Verify that external code using your module can't new StripeGateway() — only the API types.
Task 10 — Detect missing encapsulation¶
Audit this class. List every encapsulation violation:
public class Cart {
public List<Item> items = new ArrayList<>();
public double tax;
public boolean isDirty;
public Map<String, Object> metadata;
public void addItem(Item i) {
items.add(i);
isDirty = true;
}
public double total() {
return items.stream().mapToDouble(Item::price).sum() * (1 + tax);
}
}
Refactor to a properly encapsulated form.
Task 11 — Volatile encapsulation¶
Implement class HitCounter { ... } that: - Counts calls to hit() - Returns the count via count() - Is thread-safe - Doesn't expose internal state
The user should not see volatile or AtomicLong — those are internal.
Task 12 — Builder + immutable target¶
Build a Pizza record with a Builder: - All fields private final in Pizza - Pizza.builder() returns a new Builder - Builder.size(...), Builder.addTopping(...), etc. - Builder.build() returns Pizza - Pizza constructor is private; only the builder can call it
Verify external code can't call new Pizza(...).
Validation¶
| Task | How |
|---|---|
| 1 | user.name = "X" should not compile |
| 2 | After refactor, polygon.points().add(p) throws UnsupportedOperationException |
| 3 | The free function disappears; account owns the rule |
| 4 | new Range(10, 5) throws |
| 5 | Justify which (snapshot vs view) is appropriate for your use case |
| 6 | Currency.of("USD") == Currency.of("USD") is true |
| 7 | result.map(...) propagates Failure correctly |
| 8 | order.ship(); order.ship(); second call throws |
| 9 | External new StripeGateway() is illegal access at compile time |
| 10 | List ≥ 4 violations; provide refactored form |
| 11 | Concurrent test with 1000 threads × 1000 hits should yield 1,000,000 |
| 12 | new Pizza(...) should not compile from outside the class |
Solutions sketch¶
Task 4:
public record Range(int min, int max) {
public Range {
if (min > max) throw new IllegalArgumentException();
}
}
Task 7 map:
default <U> Result<U> map(Function<T, U> f) {
return switch (this) {
case Success<T>(T v) -> new Success<>(f.apply(v));
case Failure<T> err -> (Result<U>) err;
};
}
Task 6:
public final class Currency {
private static final ConcurrentHashMap<String, Currency> CACHE = new ConcurrentHashMap<>();
private final String code;
private Currency(String c) { this.code = c; }
public static Currency of(String code) {
if (code.length() != 3) throw new IllegalArgumentException();
return CACHE.computeIfAbsent(code, Currency::new);
}
}
Memorize this: encapsulation = small public surface, validated mutation, hidden state. Use private constructors + factories for instance control. Use records for data. Use sealed types for closed hierarchies. Use modules for the largest scale. Test from outside the class to verify nothing leaks.
In this topic