Blog

Why Public Classes Need Accessor Methods

A recent episode of the Fragmented Podcast tackled a classic Java rule from Effective Java: public classes should not expose fields directly. The discussion covered why encapsulation matters, what happens when you break the rule, and the one case where exposing fields is acceptable.

The Rule and Why It Matters

Item 14 in Effective Java states that public classes should use accessor methods rather than public fields. The reason is straightforward: public fields expose your internal representation, and that creates three problems. You cannot change the representation without changing the API. You cannot enforce invariants. You cannot take helper action when accessing a field.

Once a field is public, it becomes part of your published API. Anyone using your library depends on it. If you later need to change how that data is stored or calculated, you either break their code or live with the old design forever.

The Cost of Exposing Fields

Exposing fields as public means your class contract is locked to that specific implementation. Consider a library that exposes a field for a user's age. If that library later needs to calculate age dynamically from a birthdate stored elsewhere, it cannot without a breaking change. Anyone who wrote code against that public field is now stuck.

Accessor methods—getters and setters—give you a layer to change behind. The method signature stays the same; the implementation underneath can evolve. This flexibility is worth the extra code.

The Exception: Private Classes

The rule applies specifically to public classes. For private classes or package-private classes, the calculus changes. If the code outside your package cannot see the class at all, encapsulation problems are contained.

In that context, exposing fields directly can reduce visual clutter. Ten fields with generated accessors can balloon a class from 15 lines to 50. If no external code depends on that class, maintaining a leaner structure has value.

The trade-off is accepting that internal representation is locked for all code within that scope. For private classes with a small, controlled codebase, that risk is often acceptable.

Our Analysis

This advice matters most when you're building anything that other developers depend on—libraries, SDKs, or modular architectures where teams consume each other's code. The winner here is whoever maintains the code long-term: encapsulated classes give you room to refactor without coordinated rollouts across every consumer. The loser is the developer who inherits a public field and needs to change it.

The episode notes that Java itself provides cautionary examples. The Point and Dimension classes in the standard library exposed fields directly, creating performance problems that persist to this day. That's a reminder that even library authors at Oracle made this mistake—and you're not immune.

One practical shift: IDE tooling has reduced the friction of accessors dramatically. Android Studio and IntelliJ generate boilerplate in seconds. The verbosity argument that once justified public fields has largely disappeared.

When to default to accessors:

  • Classes used outside your package
  • Library or SDK code
  • Any class with invariants to maintain
  • Any field you might need to compute, validate, or lazy-load later

When public fields might be acceptable:

  • Private nested classes
  • Package-private classes with no external consumers
  • Immutable data structures where the field is final

Practical Takeaway

When you are defining a class that will be used outside your package, default to accessor methods. Treat public fields as a red flag asking you to justify the decision. For everything else, make an intentional choice rather than defaulting to either extreme.

Decision Checklist

Question If Yes If No
Will code outside this package use this class? Use accessor methods Public fields may be acceptable
Do you need to enforce invariants? Use accessor methods Public fields may be acceptable
Might the field representation change? Use accessor methods Public fields may be acceptable
Is the class private or package-private? Consider public fields for brevity Use accessor methods

Key takeaways

  • Public fields lock your API to a specific implementation
  • Accessor methods let you change behavior without breaking callers
  • Private and package-private classes have more flexibility, but the cost is still real
  • IDE tooling makes generating boilerplate trivial; use it

Frequently asked questions

Does this rule apply to data classes in Kotlin?

In Kotlin, data classes automatically generate getters and setters. If you mark a property as var, you get both; val gives you a getter only. This follows the principle automatically.

What is a package-private class?

A class declared without a public modifier is accessible only within its own package. It cannot be used by code in other packages, which limits the scope of any encapsulation problems.

Can you change a public field to an accessor method later?

You can add a method, but you cannot remove the public field without breaking existing code that uses it. This is why initial design decisions matter.

What about immutable public fields?

If a public field is final (immutable), the encapsulation problem is reduced because the representation cannot change. However, you still cannot add validation or computed behavior later. For public classes, accessor methods remain the safer default.

Does this advice apply to Android app code, not just libraries?

Yes, but the stakes are lower. In app code, "public" typically means accessible across modules or teams. Applying this discipline helps when refactoring later and when multiple developers work in the same codebase.

Related